From bda35541bb87a8dcc40054b642e783f731bb21e6 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 19 Dec 2025 16:09:10 -0300 Subject: [PATCH 001/101] feat: endpoint exposer feat: implement external dns and httproute feat: change logic to get specification id feat: add switch for different dns types feat: add switch for different dns types feat: add endpoint exposer service specs feat: add domain editable feat: add domain editable feat: implement new route schema feat: implement new route schema feat: implement new route schema feat: implement new route schema feat: implement new route schema feat: implement new route schema fix: get scope fix: get scope fix: path fix: get httproute if exists fix: get httproute if exists fix: get httproute rules feat: implement echo for link and unlink feat: implement echo for link and unlink feat: implement echo for link and unlink feat: implement echo for link and unlink feat: implement echo for link and unlink chore: change service slug feat: update flow feat: update flow feat: update flow feat: update flow feat: update flow feat: update flow feat: update flow feat: update flow feat: update flow feat: accept prefix and reg ex --- .gitignore | 3 + endpoint-exposer/configure | 162 ++++++++ .../deployment/sync_exposer | 134 +++++++ .../deployment/workflows/blue_green.yaml | 5 + .../deployment/workflows/delete.yaml | 5 + .../deployment/workflows/finalize.yaml | 5 + .../deployment/workflows/initial.yaml | 5 + .../deployment/workflows/rollback.yaml | 5 + .../deployment/workflows/switch_traffic.yaml | 5 + .../container-scope-override/values.yaml | 3 + endpoint-exposer/entrypoint/entrypoint | 57 +++ endpoint-exposer/entrypoint/link | 34 ++ endpoint-exposer/entrypoint/service | 28 ++ endpoint-exposer/examples/create-link.json | 65 ++++ endpoint-exposer/examples/create.json | 64 ++++ endpoint-exposer/examples/delete-link.json | 71 ++++ endpoint-exposer/examples/delete.json | 50 +++ endpoint-exposer/examples/read.json | 50 +++ endpoint-exposer/examples/refresh.json | 61 +++ endpoint-exposer/scripts/alb/apply | 25 ++ endpoint-exposer/scripts/alb/build_context | 36 ++ endpoint-exposer/scripts/alb/build_ingress | 22 ++ .../scripts/alb/build_ingress_with_rule | 199 ++++++++++ .../scripts/alb/build_ingress_without_rule | 47 +++ endpoint-exposer/scripts/alb/build_rule | 22 ++ .../scripts/alb/fetch_provider_data | 22 ++ endpoint-exposer/scripts/alb/read_ingress | 5 + endpoint-exposer/scripts/alb/update_service | 21 + .../scripts/alb/validate_unique_rule | 12 + endpoint-exposer/scripts/common/echo | 3 + endpoint-exposer/scripts/dns/build_dns | 15 + .../scripts/dns/build_dns_endpoint | 42 ++ endpoint-exposer/scripts/istio/apply | 25 ++ endpoint-exposer/scripts/istio/build_context | 45 +++ endpoint-exposer/scripts/istio/build_ingress | 22 ++ .../scripts/istio/build_ingress_with_rule | 361 ++++++++++++++++++ ...ld_ingress_with_rule_virtualservice_backup | 278 ++++++++++++++ .../scripts/istio/build_ingress_without_rule | 62 +++ endpoint-exposer/scripts/istio/build_rule | 179 +++++++++ .../scripts/istio/clean_httproute_rules | 30 ++ .../scripts/istio/fetch_httproute | 45 +++ .../scripts/istio/fetch_provider_data | 22 ++ endpoint-exposer/scripts/istio/process_routes | 118 ++++++ endpoint-exposer/scripts/istio/read_ingress | 5 + .../scripts/istio/update_httproute_hostname | 29 ++ endpoint-exposer/scripts/istio/update_service | 21 + .../scripts/istio/validate_unique_rule | 12 + endpoint-exposer/specs/actions/read.json.tpl | 25 ++ endpoint-exposer/specs/link-spec.json.tpl | 28 ++ endpoint-exposer/specs/links/refresh.json.tpl | 25 ++ endpoint-exposer/specs/service-spec.json.tpl | 138 +++++++ endpoint-exposer/templates/alb/empty.yaml.tpl | 32 ++ .../templates/istio/dns-endpoint.yaml.tpl | 16 + .../templates/istio/empty.yaml.tpl | 24 ++ endpoint-exposer/values.yaml | 2 + endpoint-exposer/workflows/alb/create.yaml | 44 +++ endpoint-exposer/workflows/alb/delete.yaml | 6 + endpoint-exposer/workflows/alb/link.yaml | 56 +++ endpoint-exposer/workflows/alb/read.yaml | 32 ++ endpoint-exposer/workflows/alb/refresh.yaml | 7 + endpoint-exposer/workflows/alb/unlink.yaml | 47 +++ endpoint-exposer/workflows/istio/create.yaml | 56 +++ endpoint-exposer/workflows/istio/delete.yaml | 6 + endpoint-exposer/workflows/istio/link.yaml | 6 + endpoint-exposer/workflows/istio/read.yaml | 32 ++ endpoint-exposer/workflows/istio/refresh.yaml | 7 + endpoint-exposer/workflows/istio/unlink.yaml | 6 + endpoint-exposer/workflows/istio/update.yaml | 60 +++ 68 files changed, 3192 insertions(+) create mode 100755 endpoint-exposer/configure create mode 100755 endpoint-exposer/container-scope-override/deployment/sync_exposer create mode 100644 endpoint-exposer/container-scope-override/deployment/workflows/blue_green.yaml create mode 100644 endpoint-exposer/container-scope-override/deployment/workflows/delete.yaml create mode 100644 endpoint-exposer/container-scope-override/deployment/workflows/finalize.yaml create mode 100644 endpoint-exposer/container-scope-override/deployment/workflows/initial.yaml create mode 100644 endpoint-exposer/container-scope-override/deployment/workflows/rollback.yaml create mode 100644 endpoint-exposer/container-scope-override/deployment/workflows/switch_traffic.yaml create mode 100644 endpoint-exposer/container-scope-override/values.yaml create mode 100755 endpoint-exposer/entrypoint/entrypoint create mode 100755 endpoint-exposer/entrypoint/link create mode 100755 endpoint-exposer/entrypoint/service create mode 100644 endpoint-exposer/examples/create-link.json create mode 100644 endpoint-exposer/examples/create.json create mode 100644 endpoint-exposer/examples/delete-link.json create mode 100644 endpoint-exposer/examples/delete.json create mode 100644 endpoint-exposer/examples/read.json create mode 100644 endpoint-exposer/examples/refresh.json create mode 100644 endpoint-exposer/scripts/alb/apply create mode 100644 endpoint-exposer/scripts/alb/build_context create mode 100644 endpoint-exposer/scripts/alb/build_ingress create mode 100644 endpoint-exposer/scripts/alb/build_ingress_with_rule create mode 100644 endpoint-exposer/scripts/alb/build_ingress_without_rule create mode 100644 endpoint-exposer/scripts/alb/build_rule create mode 100755 endpoint-exposer/scripts/alb/fetch_provider_data create mode 100644 endpoint-exposer/scripts/alb/read_ingress create mode 100644 endpoint-exposer/scripts/alb/update_service create mode 100644 endpoint-exposer/scripts/alb/validate_unique_rule create mode 100755 endpoint-exposer/scripts/common/echo create mode 100755 endpoint-exposer/scripts/dns/build_dns create mode 100755 endpoint-exposer/scripts/dns/build_dns_endpoint create mode 100644 endpoint-exposer/scripts/istio/apply create mode 100644 endpoint-exposer/scripts/istio/build_context create mode 100644 endpoint-exposer/scripts/istio/build_ingress create mode 100755 endpoint-exposer/scripts/istio/build_ingress_with_rule create mode 100644 endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup create mode 100644 endpoint-exposer/scripts/istio/build_ingress_without_rule create mode 100644 endpoint-exposer/scripts/istio/build_rule create mode 100755 endpoint-exposer/scripts/istio/clean_httproute_rules create mode 100755 endpoint-exposer/scripts/istio/fetch_httproute create mode 100755 endpoint-exposer/scripts/istio/fetch_provider_data create mode 100755 endpoint-exposer/scripts/istio/process_routes create mode 100644 endpoint-exposer/scripts/istio/read_ingress create mode 100755 endpoint-exposer/scripts/istio/update_httproute_hostname create mode 100644 endpoint-exposer/scripts/istio/update_service create mode 100644 endpoint-exposer/scripts/istio/validate_unique_rule create mode 100644 endpoint-exposer/specs/actions/read.json.tpl create mode 100644 endpoint-exposer/specs/link-spec.json.tpl create mode 100644 endpoint-exposer/specs/links/refresh.json.tpl create mode 100644 endpoint-exposer/specs/service-spec.json.tpl create mode 100644 endpoint-exposer/templates/alb/empty.yaml.tpl create mode 100644 endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl create mode 100644 endpoint-exposer/templates/istio/empty.yaml.tpl create mode 100644 endpoint-exposer/values.yaml create mode 100644 endpoint-exposer/workflows/alb/create.yaml create mode 100644 endpoint-exposer/workflows/alb/delete.yaml create mode 100644 endpoint-exposer/workflows/alb/link.yaml create mode 100644 endpoint-exposer/workflows/alb/read.yaml create mode 100644 endpoint-exposer/workflows/alb/refresh.yaml create mode 100644 endpoint-exposer/workflows/alb/unlink.yaml create mode 100644 endpoint-exposer/workflows/istio/create.yaml create mode 100644 endpoint-exposer/workflows/istio/delete.yaml create mode 100644 endpoint-exposer/workflows/istio/link.yaml create mode 100644 endpoint-exposer/workflows/istio/read.yaml create mode 100644 endpoint-exposer/workflows/istio/refresh.yaml create mode 100644 endpoint-exposer/workflows/istio/unlink.yaml create mode 100644 endpoint-exposer/workflows/istio/update.yaml diff --git a/.gitignore b/.gitignore index 4f27a85..4c3570f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ *.iml out gen + +# VSCode project files +.vscode/ \ No newline at end of file diff --git a/endpoint-exposer/configure b/endpoint-exposer/configure new file mode 100755 index 0000000..380957d --- /dev/null +++ b/endpoint-exposer/configure @@ -0,0 +1,162 @@ +#!/bin/bash + +set -euo pipefail + +# Colors +GREEN="\033[0;32m" +BLUE="\033[0;34m" +YELLOW="\033[1;33m" +RED="\033[0;31m" +NC="\033[0m" + +# Spinner state +SPINNER_PID="" +SPINNER_MSG="" + +start_spinner() { + SPINNER_MSG="$1" + echo -ne "${BLUE}==>${NC} $SPINNER_MSG..." + ( + while true; do + for c in / - \\ \|; do + echo -ne "\r${BLUE}==>${NC} $SPINNER_MSG... $c" + sleep 0.1 + done + done + ) & + SPINNER_PID=$! + disown +} + +stop_spinner_success() { + kill "$SPINNER_PID" >/dev/null 2>&1 || true + wait "$SPINNER_PID" 2>/dev/null || true + echo -ne "\r\033[K" + echo -e "${GREEN}✔${NC} $SPINNER_MSG" +} + +stop_spinner_error() { + kill "$SPINNER_PID" >/dev/null 2>&1 || true + wait "$SPINNER_PID" 2>/dev/null || true + echo -ne "\r\033[K" + echo -e "${RED}✖${NC} $SPINNER_MSG" + exit 1 +} + +# --- Step 1: Environment Validation --- + +start_spinner "Validating that the NRN has been loaded into the environment." +if [ -z "${NRN:-}" ]; then + stop_spinner_error "NRN is not set. Please export the NRN environment variable before running this script." +fi +stop_spinner_success "NRN found and loaded successfully." + +start_spinner "Validating that the ENVIRONMENT has been loaded into the environment." +if [ -z "${ENVIRONMENT:-}" ]; then + stop_spinner_error "ENVIRONMENT is not set. Please export the ENVIRONMENT environment variable before running this script." +fi +stop_spinner_success "ENVIRONMENT found and loaded successfully." + +start_spinner "Validating that the REPO_PATH has been loaded into the environment." +if [ -z "${REPO_PATH:-}" ]; then + stop_spinner_error "REPO_PATH is not set. Please export the REPO_PATH environment variable before running this script." +fi +stop_spinner_success "REPO_PATH found and loaded successfully." + +# --- Step 2: Generate and Create Service Specification --- + +SERVICE_SPEC_PATH="specs/service-spec.json" +LINK_SPEC_PATH="specs/link-spec.json" +LINK_ACTION_DIR="specs/link-actions" +SERVICE_ACTION_DIR="specs/service-actions" + +gomplate --file "$SERVICE_SPEC_PATH.tpl" --out "$SERVICE_SPEC_PATH" + +start_spinner "Creating the service specification in the platform." +{ + SERVICE_SPEC_BODY=$(cat "$SERVICE_SPEC_PATH") + SERVICE_SPEC=$(np service specification create --body "$SERVICE_SPEC_BODY" --format json) + SERVICE_SPECIFICATION_ID=$(echo "$SERVICE_SPEC" | jq -r .id) + SERVICE_SLUG=$(echo "$SERVICE_SPEC" | jq -r .slug) +} || stop_spinner_error "Failed to create or parse the service specification." +stop_spinner_success "Service specification created successfully (id=$SERVICE_SPECIFICATION_ID, slug=$SERVICE_SLUG)." + +rm "$SERVICE_SPEC_PATH" +export SERVICE_SPECIFICATION_ID +export SERVICE_SLUG + +# --- Step 3: Generate and Create Link Specification --- +gomplate --file "$LINK_SPEC_PATH.tpl" --out "$LINK_SPEC_PATH" + +start_spinner "Creating the link specification in the platform." +{ + LINK_SPEC_BODY=$(cat "$LINK_SPEC_PATH") + LINK_SPEC=$(np link specification create --body "$LINK_SPEC_BODY" --format json) + LINK_SPECIFICATION_ID=$(echo "$LINK_SPEC" | jq -r .id) + LINK_SLUG=$(echo "$LINK_SPEC" | jq -r .slug) +} || stop_spinner_error "Failed to create or parse the service specification." +stop_spinner_success "Link specification created successfully (id=$LINK_SPECIFICATION_ID, slug=$LINK_SLUG)." + +rm "$LINK_SPEC_PATH" +export LINK_SPECIFICATION_ID +export LINK_SLUG + +# --- Step 4: Create Action Specifications --- + +find "$LINK_ACTION_DIR" -type f -name "*.tpl" | while read -r TEMPLATE_FILE; do + REL_PATH="${TEMPLATE_FILE#$LINK_ACTION_DIR/}" + OUTPUT_PATH="$LINK_ACTION_DIR/${REL_PATH%.tpl}" + + gomplate --file "$TEMPLATE_FILE" --out "$OUTPUT_PATH" + + ACTION_SPEC_BODY=$(cat "$OUTPUT_PATH") + + start_spinner "Registering action specification: ${REL_PATH%.json.tpl}." + { + ACTION_SPEC=$(np link specification action specification create \ + --linkSpecificationId "$LINK_SPECIFICATION_ID" \ + --body "$ACTION_SPEC_BODY" \ + --format json) + ACTION_SPEC_ID=$(echo "$ACTION_SPEC" | jq -r .id) + } || stop_spinner_error "Failed to create action specification: $REL_PATH." + + rm "$OUTPUT_PATH" + stop_spinner_success "Action specification created successfully (id=$ACTION_SPEC_ID)." +done + +find "$SERVICE_ACTION_DIR" -type f -name "*.tpl" | while read -r TEMPLATE_FILE; do + REL_PATH="${TEMPLATE_FILE#$SERVICE_ACTION_DIR/}" + OUTPUT_PATH="$SERVICE_ACTION_DIR/${REL_PATH%.tpl}" + + gomplate --file "$TEMPLATE_FILE" --out "$OUTPUT_PATH" + + ACTION_SPEC_BODY=$(cat "$OUTPUT_PATH") + + start_spinner "Registering action specification: ${REL_PATH%.json.tpl}." + { + ACTION_SPEC=$(np service specification action specification create \ + --serviceSpecificationId "$SERVICE_SPECIFICATION_ID" \ + --body "$ACTION_SPEC_BODY" \ + --format json) + ACTION_SPEC_ID=$(echo "$ACTION_SPEC" | jq -r .id) + } || stop_spinner_error "Failed to create action specification: $REL_PATH." + + rm "$OUTPUT_PATH" + stop_spinner_success "Action specification created successfully (id=$ACTION_SPEC_ID)." +done + +# --- Step 5: Create Notification Channel --- + +NOTIFICATION_CHANNEL_PATH="specs/notification-channel.json" + +gomplate --file "$NOTIFICATION_CHANNEL_PATH.tpl" --out "$NOTIFICATION_CHANNEL_PATH" + +start_spinner "Creating the notification channel." +{ +NOTIFICATION_CHANNEL_BODY=$(cat "$NOTIFICATION_CHANNEL_PATH") +NOTIFICATION_CHANNEL=$(np notification channel create --format json --body "$NOTIFICATION_CHANNEL_BODY") +NOTIFICATION_CHANNEL_ID=$(echo "$NOTIFICATION_CHANNEL" | jq -r .id) +} || stop_spinner_error "Failed to create the notification channel." + +rm "$NOTIFICATION_CHANNEL_PATH" +stop_spinner_success "Notification channel created successfully (id=$NOTIFICATION_CHANNEL_ID)." diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer new file mode 100755 index 0000000..570373c --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -0,0 +1,134 @@ +#!/bin/bash + +echo "=== DEBUG: Starting sync_exposer script ===" +echo "DEBUG: CONTEXT=$CONTEXT" +echo "DEBUG: SERVICE_SPECIFICATION_SLUG=$SERVICE_SPECIFICATION_SLUG" + +SCOPE_NRN=$(echo "$CONTEXT" | jq -r .scope.nrn) + +echo "SERVICE SPECIFICATION SLUG: $SERVICE_SPECIFICATION_SLUG, SCOPE_NRN: $SCOPE_NRN" + +# Step 1: Get service specification by slug +echo "DEBUG: Fetching service specifications..." +SERVICE_SPECS=$(np service specification list --nrn "$SCOPE_NRN" --type dependency --format json) +echo "DEBUG: Service specifications fetched: $SERVICE_SPECS" + +SERVICE_SPEC=$(echo "$SERVICE_SPECS" | jq -c --arg slug "$SERVICE_SPECIFICATION_SLUG" ' + .results + | map(select(.slug == $slug)) + | .[0] +') +echo "DEBUG: Filtered service spec: $SERVICE_SPEC" + +SERVICE_SPEC_ID=$(echo "$SERVICE_SPEC" | jq -r .id) + +if [[ -z "$SERVICE_SPEC_ID" || "$SERVICE_SPEC_ID" == "null" ]]; then + echo "Error: Could not find service specification with slug '$SERVICE_SPECIFICATION_SLUG'" + exit 1 +fi + +echo "DEBUG: SERVICE_SPEC_ID=$SERVICE_SPEC_ID" + +# Step 2: Get link specification with slug "publish" +echo "DEBUG: Fetching link specifications for service spec..." +LINK_SPECS=$(np service specification link specification list --id "$SERVICE_SPEC_ID" --format json) +echo "DEBUG: Link specifications fetched: $LINK_SPECS" + +PUBLISH_LINK_SPEC=$(echo "$LINK_SPECS" | jq -c ' + .results + | map(select(.slug == "publish")) + | .[0] +') +echo "DEBUG: Filtered publish link spec: $PUBLISH_LINK_SPEC" + +LINK_SPECIFICATION_ID=$(echo "$PUBLISH_LINK_SPEC" | jq -r .id) + +if [[ -z "$LINK_SPECIFICATION_ID" || "$LINK_SPECIFICATION_ID" == "null" ]]; then + echo "Error: Could not find link specification with slug 'publish' for service specification '$SERVICE_SPEC_ID'" + exit 1 +fi + +echo "DEBUG: LINK_SPECIFICATION_ID=$LINK_SPECIFICATION_ID" + +echo "DEBUG: Fetching links for scope..." +LINKS=$(np link list --nrn "$SCOPE_NRN" --format json --status active) +echo "DEBUG: Links fetched. Response: $LINKS" + +EXPOSER_LINKS=$(echo "$LINKS" | jq -c --arg spec "$LINK_SPECIFICATION_ID" ' + .results + | map(select(.specification_id == $spec)) +') +echo "DEBUG: EXPOSER_LINKS filtered: $EXPOSER_LINKS" + +COUNT=$(echo "$EXPOSER_LINKS" | jq 'length') +echo "DEBUG: COUNT=$COUNT" + +if [[ "$COUNT" -eq 0 ]]; then + echo "This scope is not public, skipping public ingress synchronization" + return +fi + +echo "This is a public scope, starting public ingress synchronization" + +ACTIONS=$(np link specification action specification list --linkSpecificationId "$LINK_SPECIFICATION_ID" --format json) + +REFRESH_ACTION=$(echo "$ACTIONS" | jq -c ' + .results + | map(select(.slug == "refresh")) + | .[0] +') + +REFRESH_ACTION_ID=$(echo "$REFRESH_ACTION" | jq -r .id) +echo "DEBUG: REFRESH_ACTION_ID=$REFRESH_ACTION_ID" + +# Iterate over matches +echo "DEBUG: Starting iteration over exposer links..." +echo "$EXPOSER_LINKS" | jq -c '.[]' | while read -r link; do + echo "DEBUG: Processing link: $link" + + # Extract useful values + LINK_ID=$(echo "$link" | jq -r '.id') + echo "DEBUG: LINK_ID=$LINK_ID" + + echo "DEBUG: Creating link action..." + ACTION=$(np link action create --linkId "$LINK_ID" --body "{\"name\": \"refresh\", \"parameters\":{}, \"specification_id\": \"$REFRESH_ACTION_ID\"}" --format json) + echo "DEBUG: Action created: $ACTION" + + ACTION_ID=$(echo "$ACTION" | jq -r .id) + echo "DEBUG: ACTION_ID=$ACTION_ID" + + echo "Created public ingress synchronization action[id=$ACTION_ID], waiting for its completion" + + MAX_ITERATIONS=20 + iteration=0 + + echo "DEBUG: Starting polling loop for action status..." + while true; do + ((iteration++)) + echo "DEBUG: Iteration $iteration/$MAX_ITERATIONS" + + if [ "$iteration" -gt $MAX_ITERATIONS ]; then + echo "Error: Maximum number of iterations (${MAX_ITERATIONS}) reached. Could not refresh the exposer." + exit 1 + fi + + echo "DEBUG: Reading action status..." + ACTION_STATUS=$(np link action read --linkId "$LINK_ID" --id "$ACTION_ID" --format json | jq -r .status) + + echo "Checking public ingress synchronization action[id=$ACTION_ID, status=$ACTION_STATUS]" + + if [[ "$ACTION_STATUS" == "success" ]]; then + echo "✅ Public ingress successfully synchronized" + break + elif [[ "$ACTION_STATUS" == "failed" ]]; then + echo "❌ Could not synchronize public ingress, deployment will be rollbacked" + exit 1 + fi + + echo "DEBUG: Sleeping for 5 seconds..." + sleep 5 + done + echo "DEBUG: Finished processing link $LINK_ID" +done + +echo "=== DEBUG: sync_exposer script completed successfully ===" diff --git a/endpoint-exposer/container-scope-override/deployment/workflows/blue_green.yaml b/endpoint-exposer/container-scope-override/deployment/workflows/blue_green.yaml new file mode 100644 index 0000000..6d43a5e --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/workflows/blue_green.yaml @@ -0,0 +1,5 @@ +steps: + - name: sync_exposer + type: script + file: "$OVERRIDES_PATH/deployment/sync_exposer" + after: apply diff --git a/endpoint-exposer/container-scope-override/deployment/workflows/delete.yaml b/endpoint-exposer/container-scope-override/deployment/workflows/delete.yaml new file mode 100644 index 0000000..6d74ddd --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/workflows/delete.yaml @@ -0,0 +1,5 @@ +steps: + - name: sync_exposer + type: script + file: "$OVERRIDES_PATH/deployment/sync_exposer" + after: apply traffic \ No newline at end of file diff --git a/endpoint-exposer/container-scope-override/deployment/workflows/finalize.yaml b/endpoint-exposer/container-scope-override/deployment/workflows/finalize.yaml new file mode 100644 index 0000000..6d43a5e --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/workflows/finalize.yaml @@ -0,0 +1,5 @@ +steps: + - name: sync_exposer + type: script + file: "$OVERRIDES_PATH/deployment/sync_exposer" + after: apply diff --git a/endpoint-exposer/container-scope-override/deployment/workflows/initial.yaml b/endpoint-exposer/container-scope-override/deployment/workflows/initial.yaml new file mode 100644 index 0000000..6d43a5e --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/workflows/initial.yaml @@ -0,0 +1,5 @@ +steps: + - name: sync_exposer + type: script + file: "$OVERRIDES_PATH/deployment/sync_exposer" + after: apply diff --git a/endpoint-exposer/container-scope-override/deployment/workflows/rollback.yaml b/endpoint-exposer/container-scope-override/deployment/workflows/rollback.yaml new file mode 100644 index 0000000..6d43a5e --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/workflows/rollback.yaml @@ -0,0 +1,5 @@ +steps: + - name: sync_exposer + type: script + file: "$OVERRIDES_PATH/deployment/sync_exposer" + after: apply diff --git a/endpoint-exposer/container-scope-override/deployment/workflows/switch_traffic.yaml b/endpoint-exposer/container-scope-override/deployment/workflows/switch_traffic.yaml new file mode 100644 index 0000000..6d43a5e --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/workflows/switch_traffic.yaml @@ -0,0 +1,5 @@ +steps: + - name: sync_exposer + type: script + file: "$OVERRIDES_PATH/deployment/sync_exposer" + after: apply diff --git a/endpoint-exposer/container-scope-override/values.yaml b/endpoint-exposer/container-scope-override/values.yaml new file mode 100644 index 0000000..784fe86 --- /dev/null +++ b/endpoint-exposer/container-scope-override/values.yaml @@ -0,0 +1,3 @@ +configuration: + SERVICE_SPECIFICATION_SLUG: service-exposer-v2 + \ No newline at end of file diff --git a/endpoint-exposer/entrypoint/entrypoint b/endpoint-exposer/entrypoint/entrypoint new file mode 100755 index 0000000..81c3f2b --- /dev/null +++ b/endpoint-exposer/entrypoint/entrypoint @@ -0,0 +1,57 @@ +#!/bin/bash + +# Check if NP_ACTION_CONTEXT is set +if [ -z "$NP_ACTION_CONTEXT" ]; then + echo "NP_ACTION_CONTEXT is not set. Exiting." + exit 1 +fi + +CLEAN_CONTEXT=$(echo "$NP_ACTION_CONTEXT" | sed "s/^'//;s/'$//") + +export NP_ACTION_CONTEXT="$CLEAN_CONTEXT" + +# Parse the JSON properly - remove the extra quotes +export CONTEXT=$(echo "$CLEAN_CONTEXT" | jq '.notification') +export SERVICE_ACTION=$(echo "$CONTEXT" | jq -r '.slug') +export SERVICE_ACTION_TYPE=$(echo "$CONTEXT" | jq -r '.type') +export NOTIFICATION_ACTION=$(echo "$CONTEXT" | jq -r '.action') + +export LINK=$(echo "$CONTEXT" | jq '.link') + +ACTION_SOURCE=service + +IS_LINK_ACTION=$(echo "$CONTEXT" | jq '.link != null') + +if [ "$IS_LINK_ACTION" = "true" ]; then + ACTION_SOURCE=link +fi + +export WORKING_DIRECTORY="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +SERVICE_PATH="" +OVERRIDES_PATH="" + +for arg in "$@"; do + case $arg in + --service-path=*) + SERVICE_PATH="${arg#*=}" + ;; + --overrides-path=*) + OVERRIDES_PATH="${arg#*=}" + ;; + *) + echo "Unknown argument: $arg" + exit 1 + ;; + esac +done + +OVERRIDES_PATH="${OVERRIDES_PATH:-$SERVICE_PATH/overrides}" + +export SERVICE_PATH +export OVERRIDES_PATH + +# export util functions +#eval "$WORKING_DIRECTORY"/$ACTION_SOURCE + +np service-action exec --live-output --live-report --script="$WORKING_DIRECTORY/$ACTION_SOURCE" diff --git a/endpoint-exposer/entrypoint/link b/endpoint-exposer/entrypoint/link new file mode 100755 index 0000000..253ef01 --- /dev/null +++ b/endpoint-exposer/entrypoint/link @@ -0,0 +1,34 @@ +#!/bin/bash + +echo "Executing link action=$SERVICE_ACTION type=$SERVICE_ACTION_TYPE" + +ACTION_TO_EXECUTE="$SERVICE_ACTION_TYPE" + +case "$SERVICE_ACTION_TYPE" in + "custom") + ACTION_TO_EXECUTE="$SERVICE_ACTION" + ;; + "create") + ACTION_TO_EXECUTE="link" + ;; + "delete") + ACTION_TO_EXECUTE="unlink" + ;; +esac + +INGRESS_TYPE="${INGRESS_TYPE:-alb}" + +echo "INGRESS_TYPE is set to '$INGRESS_TYPE'" + +WORKFLOW_PATH="$SERVICE_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" +OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" +VALUES_PATH="$SERVICE_PATH/values.yaml" + +CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH" + +if [[ -f "$OVERRIDES_WORKFLOW_PATH" ]]; then + CMD="$CMD --overrides $OVERRIDES_WORKFLOW_PATH" +fi + +echo "Executing command: $CMD" +eval "$CMD" diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service new file mode 100755 index 0000000..ad77e32 --- /dev/null +++ b/endpoint-exposer/entrypoint/service @@ -0,0 +1,28 @@ +#!/bin/bash + +echo "Executing service action=$SERVICE_ACTION type=$SERVICE_ACTION_TYPE" + +ACTION_TO_EXECUTE="$SERVICE_ACTION_TYPE" + +case "$SERVICE_ACTION_TYPE" in + "custom") + ACTION_TO_EXECUTE="$SERVICE_ACTION" + ;; +esac + +INGRESS_TYPE="${INGRESS_TYPE:-alb}" + +echo "INGRESS_TYPE is set to '$INGRESS_TYPE'" + +WORKFLOW_PATH="$SERVICE_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" +OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" +VALUES_PATH="$SERVICE_PATH/values.yaml" + +CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH" + +if [[ -f "$OVERRIDES_WORKFLOW_PATH" ]]; then + CMD="$CMD --overrides $OVERRIDES_WORKFLOW_PATH" +fi + +echo "Executing command: $CMD" +eval "$CMD" diff --git a/endpoint-exposer/examples/create-link.json b/endpoint-exposer/examples/create-link.json new file mode 100644 index 0000000..2f86a2e --- /dev/null +++ b/endpoint-exposer/examples/create-link.json @@ -0,0 +1,65 @@ +{ + "id": "ba21e8e6-dcc1-4b9f-87a4-3610c61dfda4", + "source": "service", + "event": "service:action:create", + "created_at": "2025-06-18T13:01:20.204Z", + "notification": { + "action": "service:action:create", + "id": "5b426145-cc25-429e-88d2-83407a6f08b3", + "name": "create-the-link-name-from-the-user", + "slug": "create-the-link-name-from-the-user", + "status": "pending", + "created_at": "2025-06-18T13:01:20.207Z", + "updated_at": "2025-06-18T13:01:20.207Z", + "parameters": { + "path": "/users" + }, + "results": {}, + "type": "create", + "specification": { + "id": "0dc917a7-180c-4155-ad84-767dc5be61a4", + "slug": "create-publish" + }, + "service": { + "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", + "slug": "the-service-name-from-the-user", + "attributes": { + "domain": "some-domain.com" + }, + "specification": { + "id": "e820250b-62e3-4e20-8647-dd41b20d1bb9", + "slug": "publish" + }, + "dimensions": { + "environment": "development", + "country": "argentina" + } + }, + "link": { + "id": "64cc013c-9545-495e-a2ac-40df06af84d6", + "slug": "the-link-name-from-the-user", + "attributes": { + "path": "/users", + "scope": "dev-us" + }, + "specification": { + "id": "39d2d131-50b5-41b9-a739-1c3edde913ec", + "slug": "publish" + }, + "dimensions": {} + }, + "tags": { + "organization_id": "1255165411", + "organization": "kwik-e-mart", + "account_id": "95118862", + "account": "kwik-e-mart-main", + "namespace_id": "1068437903", + "namespace": "fede-m", + "application_id": "857154851", + "application": "scope-exposer", + "scope_id": "704207903", + "scope": "public-argentina" + }, + "entity_nrn": "organization=1255165411:account=95118862:namespace=1249051863:application=454567105:scope=704207903" + } +} \ No newline at end of file diff --git a/endpoint-exposer/examples/create.json b/endpoint-exposer/examples/create.json new file mode 100644 index 0000000..f6912ce --- /dev/null +++ b/endpoint-exposer/examples/create.json @@ -0,0 +1,64 @@ +{ + "action": "service:action:create", + "id": "d7f7698d-ce17-4dd5-9cb6-02dc39e7a726", + "name": "create-test-javi", + "slug": "create-test-javi", + "status": "pending", + "created_by": "1621446846", + "created_at": "2025-12-30T16:34:40.773Z", + "updated_at": "2025-12-30T16:34:40.773Z", + "parameters": { + "routes": [ + { + "method": "GET", + "path": "/health", + "scope": "DevelopmentScope", + "visibility": "public" + } + ], + "public_domain": "javi.edenred.nullimplementation.com", + "private_domain": "javi.edenred.nullimplementation.com" + }, + "results": { + "routes": [ + { + "method": "GET", + "path": "/health", + "scope": "DevelopmentScope", + "visibility": "public" + } + ], + "public_domain": "javi.edenred.nullimplementation.com", + "private_domain": "javi.edenred.nullimplementation.com" + }, + "type": "create", + "specification": { + "id": "b9c6e9b5-c292-4ad1-9af8-650cac4ea587", + "slug": "create-service-exposer-v2" + }, + "service": { + "id": "babaa91d-89fb-4ee3-ba23-529620d1834f", + "slug": "test-javi", + "attributes": {}, + "type": "dependency", + "specification": { + "id": "70d51cc6-591d-4282-927c-4757b092604b", + "slug": "service-exposer-v2" + }, + "dimensions": { + "environment": "development" + } + }, + "link": null, + "tags": { + "namespace_id": "1340017944", + "namespace": "playground", + "organization_id": "1858797910", + "organization": "edenred", + "account_id": "758973013", + "account": "playground", + "application_id": "179976948", + "application": "floppy-bird-api" + }, + "entity_nrn": "organization=1858797910:account=758973013:namespace=1340017944:application=179976948" +} \ No newline at end of file diff --git a/endpoint-exposer/examples/delete-link.json b/endpoint-exposer/examples/delete-link.json new file mode 100644 index 0000000..b17be37 --- /dev/null +++ b/endpoint-exposer/examples/delete-link.json @@ -0,0 +1,71 @@ +{ + "id": "ba21e8e6-dcc1-4b9f-87a4-3610c61dfda4", + "source": "service", + "event": "service:action:delete", + "created_at": "2025-06-18T13:01:20.204Z", + "notification": { + "action": "service:action:delete", + "id": "5b426145-cc25-429e-88d2-83407a6f08b3", + "name": "delete-the-link-name-from-the-user", + "slug": "delete-the-link-name-from-the-user", + "status": "pending", + "created_at": "2025-06-18T13:01:20.207Z", + "updated_at": "2025-06-18T13:01:20.207Z", + "parameters": { + "path": "/users" + }, + "results": {}, + "type": "delete", + "specification": { + "id": "0dc917a7-180c-4155-ad84-767dc5be61a4", + "slug": "delete-publish" + }, + "service": { + "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", + "slug": "the-service-name-from-the-user", + "attributes": { + "domain": "some-domain.com", + "rules": [ + { + "path": "/users", + "scope": "public-argentina" + } + ] + }, + "specification": { + "id": "e820250b-62e3-4e20-8647-dd41b20d1bb9", + "slug": "publish" + }, + "dimensions": { + "environment": "development", + "country": "argentina" + } + }, + "link": { + "id": "64cc013c-9545-495e-a2ac-40df06af84d6", + "slug": "the-link-name-from-the-user", + "attributes": { + "path": "/users", + "scope": "dev-us" + }, + "specification": { + "id": "39d2d131-50b5-41b9-a739-1c3edde913ec", + "slug": "publish" + }, + "dimensions": {} + }, + "tags": { + "organization_id": "1255165411", + "organization": "kwik-e-mart", + "account_id": "95118862", + "account": "kwik-e-mart-main", + "namespace_id": "1068437903", + "namespace": "fede-m", + "application_id": "857154851", + "application": "scope-exposer", + "scope_id": "704207903", + "scope": "public-argentina" + }, + "entity_nrn": "organization=1255165411:account=95118862:namespace=1249051863:application=454567105:scope=704207903" + } +} \ No newline at end of file diff --git a/endpoint-exposer/examples/delete.json b/endpoint-exposer/examples/delete.json new file mode 100644 index 0000000..ac6a64d --- /dev/null +++ b/endpoint-exposer/examples/delete.json @@ -0,0 +1,50 @@ +{ + "id": "c266d1aa-23d7-4a23-8954-346824186c9a", + "source": "service", + "event": "service:action:delete", + "created_at": "2025-06-17T15:24:05.861Z", + "notification": { + "action": "service:action:delete", + "id": "b9a9971b-acce-421b-b7b7-4e940e73fac8", + "name": "create-the-service-name-from-the-user", + "slug": "create-the-service-name-from-the-user", + "status": "pending", + "created_at": "2025-06-17T15:24:05.861Z", + "updated_at": "2025-06-17T15:24:05.861Z", + "parameters": { + }, + "results": {}, + "type": "delete", + "specification": { + "id": "03bcdc47-212c-4b64-a2cc-ed0365e4228c", + "slug": "create-service-exposer" + }, + "service": { + "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", + "slug": "the-service-name-from-the-user", + "attributes": { + "rules": [], + "domain": "domain.com" + }, + "specification": { + "id": "f66f5e37-0303-48aa-bc9d-aeda1f909a10", + "slug": "service-exposer" + }, + "dimensions": { + "environment": "development" + } + }, + "link": null, + "tags": { + "organization_id": "1255165411", + "organization": "kwik-e-mart", + "account_id": "95118862", + "account": "kwik-e-mart-main", + "namespace_id": "1068437903", + "namespace": "fede-m", + "application_id": "857154851", + "application": "scope-exposer" + }, + "entity_nrn": "organization=1255165411:account=95118862:namespace=1068437903:application=857154851" + } +} \ No newline at end of file diff --git a/endpoint-exposer/examples/read.json b/endpoint-exposer/examples/read.json new file mode 100644 index 0000000..f165523 --- /dev/null +++ b/endpoint-exposer/examples/read.json @@ -0,0 +1,50 @@ +{ + "id": "c266d1aa-23d7-4a23-8954-346824186c9a", + "source": "service", + "event": "service:action:write", + "created_at": "2025-06-17T15:24:05.861Z", + "notification": { + "action": "service:action:write", + "id": "b9a9971b-acce-421b-b7b7-4e940e73fac8", + "name": "Read", + "slug": "read", + "status": "pending", + "created_at": "2025-06-17T15:24:05.861Z", + "updated_at": "2025-06-17T15:24:05.861Z", + "parameters": { + }, + "results": {}, + "type": "custom", + "specification": { + "id": "03bcdc47-212c-4b64-a2cc-ed0365e4228c", + "slug": "read" + }, + "service": { + "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", + "slug": "the-service-name-from-the-user", + "attributes": { + "rules": [], + "domain": "domain.com" + }, + "specification": { + "id": "f66f5e37-0303-48aa-bc9d-aeda1f909a10", + "slug": "service-exposer" + }, + "dimensions": { + "environment": "development" + } + }, + "link": null, + "tags": { + "organization_id": "1255165411", + "organization": "kwik-e-mart", + "account_id": "95118862", + "account": "kwik-e-mart-main", + "namespace_id": "1068437903", + "namespace": "fede-m", + "application_id": "857154851", + "application": "scope-exposer" + }, + "entity_nrn": "organization=1255165411:account=95118862:namespace=1068437903:application=857154851" + } +} \ No newline at end of file diff --git a/endpoint-exposer/examples/refresh.json b/endpoint-exposer/examples/refresh.json new file mode 100644 index 0000000..ae32f56 --- /dev/null +++ b/endpoint-exposer/examples/refresh.json @@ -0,0 +1,61 @@ +{ + "id": "ba21e8e6-dcc1-4b9f-87a4-3610c61dfda4", + "source": "service", + "event": "service:action:update", + "created_at": "2025-06-18T13:01:20.204Z", + "notification": { + "action": "service:action:update", + "id": "5b426145-cc25-429e-88d2-83407a6f08b3", + "name": "Refresh", + "slug": "refresh", + "status": "pending", + "created_at": "2025-06-18T13:01:20.207Z", + "updated_at": "2025-06-18T13:01:20.207Z", + "parameters": {}, + "results": {}, + "type": "custom", + "specification": { + "id": "0dc917a7-180c-4155-ad84-767dc5be61a4", + "slug": "delete-publish" + }, + "service": { + "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", + "slug": "the-service-name-from-the-user", + "attributes": {}, + "specification": { + "id": "e820250b-62e3-4e20-8647-dd41b20d1bb9", + "slug": "publish" + }, + "dimensions": { + "environment": "development", + "country": "argentina" + } + }, + "link": { + "id": "64cc013c-9545-495e-a2ac-40df06af84d6", + "slug": "the-link-name-from-the-user", + "attributes": { + "path": "/users", + "scope": "dev-us" + }, + "specification": { + "id": "39d2d131-50b5-41b9-a739-1c3edde913ec", + "slug": "publish" + }, + "dimensions": {} + }, + "tags": { + "organization_id": "1255165411", + "organization": "kwik-e-mart", + "account_id": "95118862", + "account": "kwik-e-mart-main", + "namespace_id": "1068437903", + "namespace": "fede-m", + "application_id": "857154851", + "application": "scope-exposer", + "scope_id": "704207903", + "scope": "public-argentina" + }, + "entity_nrn": "organization=1255165411:account=95118862:namespace=1249051863:application=454567105:scope=704207903" + } +} \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/apply b/endpoint-exposer/scripts/alb/apply new file mode 100644 index 0000000..2692c3c --- /dev/null +++ b/endpoint-exposer/scripts/alb/apply @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +echo "TEMPLATE DIR: $OUTPUT_DIR, ACTION: $ACTION, DRY_RUN: $DRY_RUN" + +# Find all .yaml files that were not yet applied / deleted +find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" \) -prune -o -type f -name "*.yaml" -print | while read -r TEMPLATE_FILE; do + echo "kubectl $ACTION $TEMPLATE_FILE" + + if [[ "$DRY_RUN" == "false" ]]; then + kubectl "$ACTION" -f "$TEMPLATE_FILE" + fi + + BASE_DIR="$(dirname "$TEMPLATE_FILE")" + FILENAME="$(basename "$TEMPLATE_FILE")" + DEST_DIR="${BASE_DIR}/$ACTION" + + mkdir -p "$DEST_DIR" + mv "$TEMPLATE_FILE" "$DEST_DIR/$FILENAME" +done + +if [[ "$DRY_RUN" == "true" ]]; then + exit 1 +fi \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_context b/endpoint-exposer/scripts/alb/build_context new file mode 100644 index 0000000..761fed4 --- /dev/null +++ b/endpoint-exposer/scripts/alb/build_context @@ -0,0 +1,36 @@ +#!/bin/bash + +set -euo pipefail + +SERVICE_ID=$(echo "$CONTEXT" | jq -r .service.id) +SERVICE_NAME=$(echo "$CONTEXT" | jq -r .service.slug) + +ACTION_ID=$(echo "$CONTEXT" | jq -r .id) +ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) + +LINK_ID=$(echo "$CONTEXT" | jq -r .link.id) +LINK_NAME=$(echo "$CONTEXT" | jq -r .link.slug) + +CONTEXT=$(echo "$CONTEXT" | jq \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg alb_name "$ALB_NAME" \ + '. + {k8s_namespace: $k8s_namespace, alb_name: $alb_name}') + +SCOPE_ID=$(echo "$CONTEXT" | jq -r .tags.scope_id) +SCOPE_SLUG=$(echo "$CONTEXT" | jq -r .tags.scope) + +RULE_PATH=$(echo "$CONTEXT" | jq -r '.link.attributes.path // .parameters.path') + +export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_NAME-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" + +mkdir -p "$OUTPUT_DIR" + +export SERVICE_ID +export SERVICE_NAME +export ACTION_ID +export ACTION_NAME +export LINK_ID +export LINK_NAME +export SCOPE_ID +export SCOPE_SLUG +export RULE_PATH \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_ingress b/endpoint-exposer/scripts/alb/build_ingress new file mode 100644 index 0000000..610c439 --- /dev/null +++ b/endpoint-exposer/scripts/alb/build_ingress @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r .parameters.domain) + +echo "Creating ingress for service $SERVICE_NAME with domain $SERVICE_DOMAIN" + +echo "Creating public ingress..." + +INGRESS_FILE="$OUTPUT_DIR/ingress-$SERVICE_ID-public.yaml" +CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" + +echo "$CONTEXT" > "$CONTEXT_PATH" + +echo "Building Template: $TEMPLATE to $INGRESS_FILE" + +gomplate -c .="$CONTEXT_PATH" \ + --file "$TEMPLATE" \ + --out "$INGRESS_FILE" + +rm "$CONTEXT_PATH" \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_ingress_with_rule b/endpoint-exposer/scripts/alb/build_ingress_with_rule new file mode 100644 index 0000000..f3f4e5f --- /dev/null +++ b/endpoint-exposer/scripts/alb/build_ingress_with_rule @@ -0,0 +1,199 @@ +#!/bin/bash + +set -euo pipefail + +is_ingress_empty() { + local yaml_content="$1" + + local num_rules + local num_paths + local path + local service_name + local service_port + + num_rules=$(yq '.spec.rules | length' <<< "$yaml_content") + num_paths=$(yq '.spec.rules[0].http.paths | length' <<< "$yaml_content") + path=$(yq '.spec.rules[0].http.paths[0].path' <<< "$yaml_content") + service_name=$(yq '.spec.rules[0].http.paths[0].backend.service.name' <<< "$yaml_content") + service_port=$(yq '.spec.rules[0].http.paths[0].backend.service.port.name' <<< "$yaml_content") + + if [[ "$num_rules" -eq 1 && \ + "$num_paths" -eq 1 && \ + "$path" == "/" && \ + "$service_name" == "response-404" && \ + "$service_port" == "use-annotation" ]]; then + echo "true" + else + echo "false" + fi +} + +create_path_rule() { + local rule_path="$1" + local service_json="$2" + + jq -n \ + --arg path "$rule_path" \ + --argjson service "$service_json" \ + '{ + path: $path, + pathType: "Prefix", + backend: { + service: $service + } + }' +} + +add_annotation_if_needed() { + local ingress_yaml="$1" + local annotation="$2" + local scope_id="$3" + + if [[ "$annotation" != "null" && -n "$annotation" ]]; then + local annotation_key="alb.ingress.kubernetes.io/actions.bg-deployment-$scope_id" + + # Escape the value so it’s properly quoted in YAML + local escaped_annotation + escaped_annotation=$(printf '%s\n' "$annotation" | jq -Rs .) # wrap & escape as JSON string + + echo "$ingress_yaml" | yq eval ".metadata.annotations.\"$annotation_key\" = $escaped_annotation" + else + echo "$ingress_yaml" + fi +} + +update_ingress_rule() { + local ingress_yaml="$1" + local rule_path="$2" + local service_json="$3" + local annotation="$4" + local scope_id="$5" + + local updated_ingress + + # Replace the current rule with the new rule data + updated_ingress=$(echo "$ingress_yaml" | yq eval '.spec.rules[0].http.paths[0].path = "'$rule_path'"') + updated_ingress=$(echo "$updated_ingress" | yq eval '.spec.rules[0].http.paths[0].pathType = "Prefix"') + updated_ingress=$(echo "$updated_ingress" | yq eval '.spec.rules[0].http.paths[0].backend.service = '"$service_json") + + # Add annotation if needed + updated_ingress=$(add_annotation_if_needed "$updated_ingress" "$annotation" "$scope_id") + + echo "$updated_ingress" +} + +find_path_index() { + local ingress_yaml="$1" + local target_path="$2" + + local num_paths + local i + local current_path + + num_paths=$(yq '.spec.rules[0].http.paths | length' <<< "$ingress_yaml") + + for ((i=0; i "$INGRESS_FILE" +else + PATH_INDEX=$(find_path_index "$INGRESS" "$RULE_PATH") + + # if there is a rule for the path we replace it + if [[ "$PATH_INDEX" != "-1" ]]; then + UPDATED_INGRESS=$(replace_existing_rule "$INGRESS" "$RULE_PATH" "$SERVICE" "$ANNOTATION" "$SCOPE_ID" "$PATH_INDEX") + echo "$UPDATED_INGRESS" | yq "." > "$INGRESS_FILE" + else + # if there is no rule for the path we add a new one + UPDATED_INGRESS=$(add_new_rule "$INGRESS" "$RULE_PATH" "$SERVICE" "$ANNOTATION" "$SCOPE_ID") + echo "$UPDATED_INGRESS" | yq "." > "$INGRESS_FILE" + fi +fi + +echo "Ingress configuration saved to: $INGRESS_FILE" \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_ingress_without_rule b/endpoint-exposer/scripts/alb/build_ingress_without_rule new file mode 100644 index 0000000..282fff7 --- /dev/null +++ b/endpoint-exposer/scripts/alb/build_ingress_without_rule @@ -0,0 +1,47 @@ +#!/bin/bash + +set -euo pipefail + +find_path_index() { + local ingress_yaml="$1" + local target_path="$2" + + local num_paths + local i + local current_path + + num_paths=$(yq '.spec.rules[0].http.paths | length' <<< "$ingress_yaml") + + for ((i=0; i "$INGRESS_FILE" + + diff --git a/endpoint-exposer/scripts/alb/build_rule b/endpoint-exposer/scripts/alb/build_rule new file mode 100644 index 0000000..1de728a --- /dev/null +++ b/endpoint-exposer/scripts/alb/build_rule @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +INGRESS_NAME=$(kubectl get ingress -n "$K8S_NAMESPACE" -l "scope_id=$SCOPE_ID" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null || echo "") + +# Check if ingress exists +if [ -z "$INGRESS_NAME" ]; then + echo "There is no ingress for scope $SCOPE_ID. Publishing the rule with an empty backend" + + SCOPE_RULE='{"blue_green_annotation": null, "service": {"name": "response-404", "port": { "name": "use-annotation"} }}' +else + echo "Found ingress for scope $SCOPE_ID. Publishing the backend service configuration" + INGRESS=$(kubectl get ingress -n "$K8S_NAMESPACE" "$INGRESS_NAME" -o json) + + SCOPE_RULE=$(echo "$INGRESS" | jq '{ + blue_green_annotation: .metadata.annotations["alb.ingress.kubernetes.io/actions.bg-deployment"], + service: .spec.rules[0].http.paths[0].backend.service + }') +fi + +export SCOPE_RULE \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/fetch_provider_data b/endpoint-exposer/scripts/alb/fetch_provider_data new file mode 100755 index 0000000..51603cf --- /dev/null +++ b/endpoint-exposer/scripts/alb/fetch_provider_data @@ -0,0 +1,22 @@ +#!/bin/bash + +NRN=$(echo "$CONTEXT" | jq -r .entity_nrn) + +DIMENSIONS=$(echo "$CONTEXT" | jq .service.dimensions) + +DIMENSION_FILTER=$(echo "$DIMENSIONS" | jq -r 'to_entries | map("\(.key):\(.value)") | join(",")') + +if [ -z "$DIMENSION_FILTER" ] || [ "$DIMENSION_FILTER" = "" ]; then + PROVIDER_DATA=$(np provider list --categories container-orchestration --nrn "$NRN" --format json | jq -r ".results[0]") +else + PROVIDER_DATA=$(np provider list --categories container-orchestration --nrn "$NRN" --dimensions "$DIMENSION_FILTER" --format json | jq -r ".results[0]") +fi + +K8S_NAMESPACE=$(echo "$PROVIDER_DATA" | jq -r .attributes.cluster.namespace) + +ALB_NAME=$(echo "$PROVIDER_DATA" | jq -r --arg default "k8s-nullplatform-internet-facing" ' + .attributes.balancer.public_name // $default +') + +export K8S_NAMESPACE +export ALB_NAME \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/read_ingress b/endpoint-exposer/scripts/alb/read_ingress new file mode 100644 index 0000000..bde7c9f --- /dev/null +++ b/endpoint-exposer/scripts/alb/read_ingress @@ -0,0 +1,5 @@ +#!/bin/bash + +INGRESS_NAME=$(kubectl get ingress -n "$K8S_NAMESPACE" -l "service_id=$SERVICE_ID" -o jsonpath="{.items[0].metadata.name}") + +kubectl get ingress -n "$K8S_NAMESPACE" "$INGRESS_NAME" -o yaml diff --git a/endpoint-exposer/scripts/alb/update_service b/endpoint-exposer/scripts/alb/update_service new file mode 100644 index 0000000..5f743d3 --- /dev/null +++ b/endpoint-exposer/scripts/alb/update_service @@ -0,0 +1,21 @@ +#!/bin/bash + +if [[ $ACTION == "add" ]]; then + UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" --arg scope "$SCOPE_SLUG" ' + .service.attributes |= (.rules //= [] | .rules += [{path: $path, scope: $scope}]) | + { + attributes: .service.attributes, + entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) + } + ') +elif [[ $ACTION == "remove" ]]; then + UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' + .service.attributes |= (.rules //= [] | .rules |= map(select(.path != $path))) | + { + attributes: .service.attributes, + entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) + } + ') +fi + +np service patch --id "$SERVICE_ID" --body "$UPDATED_ATTRIBUTES" diff --git a/endpoint-exposer/scripts/alb/validate_unique_rule b/endpoint-exposer/scripts/alb/validate_unique_rule new file mode 100644 index 0000000..49a56b1 --- /dev/null +++ b/endpoint-exposer/scripts/alb/validate_unique_rule @@ -0,0 +1,12 @@ +#!/bin/bash + +EXISTS=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' + (.service.attributes.rules // []) + | map(select(.path == $path)) + | length +') + +if [[ "$EXISTS" -gt 0 ]]; then + echo "Error: A rule with path '$RULE_PATH' already exists." + exit 1 +fi \ No newline at end of file diff --git a/endpoint-exposer/scripts/common/echo b/endpoint-exposer/scripts/common/echo new file mode 100755 index 0000000..ace31ee --- /dev/null +++ b/endpoint-exposer/scripts/common/echo @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "$MESSAGE" \ No newline at end of file diff --git a/endpoint-exposer/scripts/dns/build_dns b/endpoint-exposer/scripts/dns/build_dns new file mode 100755 index 0000000..6a6641b --- /dev/null +++ b/endpoint-exposer/scripts/dns/build_dns @@ -0,0 +1,15 @@ +#!/bin/bash + +# Build DNS context based on DNS_TYPE +echo "=== DEBUG: Starting build_dns ===" +echo "DNS_TYPE=$DNS_TYPE" + +case "$DNS_TYPE" in + external_dns) + "$SERVICE_PATH/scripts/dns/build_dns_endpoint" + ;; + *) + echo "Error: Unsupported DNS type '$DNS_TYPE'" + exit 1 + ;; +esac \ No newline at end of file diff --git a/endpoint-exposer/scripts/dns/build_dns_endpoint b/endpoint-exposer/scripts/dns/build_dns_endpoint new file mode 100755 index 0000000..c43327f --- /dev/null +++ b/endpoint-exposer/scripts/dns/build_dns_endpoint @@ -0,0 +1,42 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== DEBUG: Starting build_dns_endpoint ===" + +SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain') +echo "DEBUG: SERVICE_DOMAIN=$SERVICE_DOMAIN" + + +PUBLIC_GATEWAY_NAME=${PUBLIC_GATEWAY_NAME:-"gateway-public"} +GATEWAY_NAMESPACE=${GATEWAY_NAMESPACE:-"gateways"} + +echo "DEBUG: Fetching gateway IP from $PUBLIC_GATEWAY_NAME..." +GATEWAY_IP=$(kubectl get gateway -n "$GATEWAY_NAMESPACE" "$PUBLIC_GATEWAY_NAME" -o jsonpath='{.status.addresses[0].value}') +echo "DEBUG: GATEWAY_IP=$GATEWAY_IP" + +if [[ -z "$GATEWAY_IP" || "$GATEWAY_IP" == "null" ]]; then + echo "ERROR: Could not retrieve gateway IP from $PUBLIC_GATEWAY_NAME" + exit 1 +fi + +echo "Creating DNSEndpoint for service $SERVICE_NAME with domain $SERVICE_DOMAIN" + +DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" +CONTEXT_PATH="$OUTPUT_DIR/context-dns-$SERVICE_ID.json" + +# Add gateway_ip to context +CONTEXT_WITH_IP=$(echo "$CONTEXT" | jq --arg gateway_ip "$GATEWAY_IP" '. + {gateway_ip: $gateway_ip}') + +echo "$CONTEXT_WITH_IP" > "$CONTEXT_PATH" + +echo "Building DNSEndpoint Template to $DNS_FILE" + +gomplate -c .="$CONTEXT_PATH" \ + --file "$SERVICE_PATH/templates/istio/dns-endpoint.yaml.tpl" \ + --out "$DNS_FILE" + +rm "$CONTEXT_PATH" + +echo "DEBUG: DNSEndpoint created at $DNS_FILE" +echo "=== DEBUG: build_dns_endpoint completed ===" diff --git a/endpoint-exposer/scripts/istio/apply b/endpoint-exposer/scripts/istio/apply new file mode 100644 index 0000000..2692c3c --- /dev/null +++ b/endpoint-exposer/scripts/istio/apply @@ -0,0 +1,25 @@ +#!/bin/bash + +set -euo pipefail + +echo "TEMPLATE DIR: $OUTPUT_DIR, ACTION: $ACTION, DRY_RUN: $DRY_RUN" + +# Find all .yaml files that were not yet applied / deleted +find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" \) -prune -o -type f -name "*.yaml" -print | while read -r TEMPLATE_FILE; do + echo "kubectl $ACTION $TEMPLATE_FILE" + + if [[ "$DRY_RUN" == "false" ]]; then + kubectl "$ACTION" -f "$TEMPLATE_FILE" + fi + + BASE_DIR="$(dirname "$TEMPLATE_FILE")" + FILENAME="$(basename "$TEMPLATE_FILE")" + DEST_DIR="${BASE_DIR}/$ACTION" + + mkdir -p "$DEST_DIR" + mv "$TEMPLATE_FILE" "$DEST_DIR/$FILENAME" +done + +if [[ "$DRY_RUN" == "true" ]]; then + exit 1 +fi \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context new file mode 100644 index 0000000..5303734 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_context @@ -0,0 +1,45 @@ +#!/bin/bash + +set -euo pipefail + +SERVICE_ID=$(echo "$CONTEXT" | jq -r .service.id) +SERVICE_NAME=$(echo "$CONTEXT" | jq -r .service.slug) + +ACTION_ID=$(echo "$CONTEXT" | jq -r .id) +ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) + +# Extract domains from parameters +PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // ""') +PRIVATE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain // ""') + +# Extract routes array from parameters +ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') + +# For backward compatibility with link-based workflow +LINK_ID=$(echo "$CONTEXT" | jq -r '.link.id // ""') +LINK_NAME=$(echo "$CONTEXT" | jq -r '.link.slug // ""') +SCOPE_ID=$(echo "$CONTEXT" | jq -r '.tags.scope_id // ""') +SCOPE_SLUG=$(echo "$CONTEXT" | jq -r '.tags.scope // ""') +RULE_PATH=$(echo "$CONTEXT" | jq -r '.link.attributes.path // .parameters.path // ""') + +CONTEXT=$(echo "$CONTEXT" | jq \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg alb_name "$ALB_NAME" \ + '. + {k8s_namespace: $k8s_namespace, alb_name: $alb_name}') + +export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_NAME-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" + +mkdir -p "$OUTPUT_DIR" + +export SERVICE_ID +export SERVICE_NAME +export ACTION_ID +export ACTION_NAME +export PUBLIC_DOMAIN +export PRIVATE_DOMAIN +export ROUTES_JSON +export LINK_ID +export LINK_NAME +export SCOPE_ID +export SCOPE_SLUG +export RULE_PATH \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_ingress b/endpoint-exposer/scripts/istio/build_ingress new file mode 100644 index 0000000..bbea5c3 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') + +echo "Creating HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" + +echo "Creating public HTTPRoute..." + +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" + +echo "$CONTEXT" > "$CONTEXT_PATH" + +echo "Building Template: $TEMPLATE to $HTTPROUTE_FILE" + +gomplate -c .="$CONTEXT_PATH" \ + --file "$TEMPLATE" \ + --out "$HTTPROUTE_FILE" + +rm "$CONTEXT_PATH" \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_ingress_with_rule b/endpoint-exposer/scripts/istio/build_ingress_with_rule new file mode 100755 index 0000000..eb5bae1 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_ingress_with_rule @@ -0,0 +1,361 @@ +#!/bin/bash + +set -euo pipefail + +# Detect path type and convert path value accordingly +# Returns: "type:value" format +detect_path_type() { + local path="$1" + + # Check for wildcard (*) - use PathPrefix + if [[ "$path" == *"*"* ]]; then + # Remove trailing /* or * + local prefix_path="${path%/*}" + if [[ -z "$prefix_path" ]]; then + prefix_path="/" + fi + echo "PathPrefix:$prefix_path" + return + fi + + # Check for path parameters (:param) - use RegularExpression + if [[ "$path" == *:* ]]; then + # Replace :param with [^/]+ + local regex_path="${path//:+([^\/])/[^/]+}" + # For bash pattern replacement, we need to handle it differently + regex_path=$(echo "$path" | sed 's/:[^/]*/[^\/]+/g') + echo "RegularExpression:$regex_path" + return + fi + + # Default: Exact match + echo "Exact:$path" +} + +# Get priority for path type (lower number = higher priority) +get_path_priority() { + local path="$1" + + if [[ "$path" != *":"* && "$path" != *"*"* ]]; then + echo "1" # Exact - highest priority + elif [[ "$path" == *":"* ]]; then + echo "2" # RegularExpression - medium priority + else + echo "3" # PathPrefix - lowest priority + fi +} + +is_httproute_empty() { + local yaml_content="$1" + + local num_rules + local backend_name + local backend_weight + + num_rules=$(yq '.spec.rules | length' <<< "$yaml_content") + backend_name=$(yq '.spec.rules[0].backendRefs[0].name' <<< "$yaml_content") + backend_weight=$(yq '.spec.rules[0].backendRefs[0].weight' <<< "$yaml_content") + + # An HTTPRoute is "empty" if it only has one rule with response-404 backend and weight 0 + if [[ "$num_rules" -eq 1 && \ + "$backend_name" == "response-404" && \ + "$backend_weight" == "0" ]]; then + echo "true" + else + echo "false" + fi +} + +create_http_rule() { + local rule_path="$1" + local service_json="$2" + local blue_green_config="$3" + local method="${4:-}" + local visibility="${5:-public}" + + local service_name + local service_port + + service_name=$(echo "$service_json" | jq -r '.name') + service_port=$(echo "$service_json" | jq -r '.port.number // .port.name // 80') + + # Detect path type and get the converted path value + local path_type_value + path_type_value=$(detect_path_type "$rule_path") + local path_type="${path_type_value%%:*}" + local path_value="${path_type_value#*:}" + + echo "DEBUG: Original path='$rule_path', Detected type='$path_type', Converted value='$path_value'" >&2 + + # Build matches array with path and optional method + local matches_json + + # Add method if specified + if [[ -n "$method" && "$method" != "null" ]]; then + matches_json=$(jq -n \ + --arg path "$path_value" \ + --arg path_type "$path_type" \ + --arg method "$method" \ + '[{ + path: { + type: $path_type, + value: $path + }, + method: $method + }]') + else + matches_json=$(jq -n \ + --arg path "$path_value" \ + --arg path_type "$path_type" \ + '[{ + path: { + type: $path_type, + value: $path + } + }]') + fi + + # Check if there's blue/green configuration + if [[ "$blue_green_config" != "null" && -n "$blue_green_config" ]]; then + # Parse blue/green destinations and weights from the annotation + local blue_weight green_weight blue_service green_service + + blue_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].weight // 100') + green_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].weight // 0') + blue_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].serviceName') + green_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].serviceName') + + # Create rule with weighted backends (no URL rewrite) + jq -n \ + --argjson matches "$matches_json" \ + --arg blue_service "$blue_service" \ + --arg green_service "$green_service" \ + --arg service_port "$service_port" \ + --argjson blue_weight "$blue_weight" \ + --argjson green_weight "$green_weight" \ + '{ + matches: $matches, + backendRefs: [ + { + name: $blue_service, + port: ($service_port | tonumber), + weight: $blue_weight + }, + { + name: $green_service, + port: ($service_port | tonumber), + weight: $green_weight + } + ] + }' + else + # Single destination without blue/green (no URL rewrite) + jq -n \ + --argjson matches "$matches_json" \ + --arg service_name "$service_name" \ + --arg service_port "$service_port" \ + '{ + matches: $matches, + backendRefs: [ + { + name: $service_name, + port: ($service_port | tonumber) + } + ] + }' + fi +} + +update_httproute_rule() { + local hr_yaml="$1" + local rule_path="$2" + local service_json="$3" + local blue_green_config="$4" + + local service_name + local service_port + local updated_hr + + service_name=$(echo "$service_json" | jq -r '.name') + service_port=$(echo "$service_json" | jq -r '.port.number // .port.name // 80') + + # Update the first rule's path with Exact type (no URL rewrite) + updated_hr=$(echo "$hr_yaml" | yq eval ".spec.rules[0].matches[0].path.type = \"Exact\"") + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].matches[0].path.value = \"$rule_path\"") + + # Remove filters (no URL rewrite needed) + updated_hr=$(echo "$updated_hr" | yq eval "del(.spec.rules[0].filters)") + + # Check if there's blue/green configuration + if [[ "$blue_green_config" != "null" && -n "$blue_green_config" ]]; then + # Parse blue/green destinations and weights + local blue_weight green_weight blue_service green_service + + blue_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].weight // 100') + green_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].weight // 0') + blue_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].serviceName') + green_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].serviceName') + + # Set blue backend + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].backendRefs[0].name = \"${blue_service}\"") + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].backendRefs[0].port = $service_port") + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].backendRefs[0].weight = $blue_weight") + + # Add green backend + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].backendRefs += [{\"name\": \"${green_service}\", \"port\": $service_port, \"weight\": $green_weight}]") + else + # Single destination + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].backendRefs[0].name = \"${service_name}\"") + updated_hr=$(echo "$updated_hr" | yq eval ".spec.rules[0].backendRefs[0].port = $service_port") + updated_hr=$(echo "$updated_hr" | yq eval "del(.spec.rules[0].backendRefs[0].weight)") + fi + + echo "$updated_hr" +} + +find_rule_index() { + local hr_yaml="$1" + local target_path="$2" + + local num_rules + local i + local current_path + + num_rules=$(yq '.spec.rules | length' <<< "$hr_yaml") + + for ((i=0; i "$HTTPROUTE_FILE" +else + RULE_INDEX=$(find_rule_index "$HTTPROUTE" "$RULE_PATH") + echo "Found rule index for path '$RULE_PATH': $RULE_INDEX" + + # if there is a rule for the path we replace it + if [[ "$RULE_INDEX" != "-1" ]]; then + echo "Case 2: Replacing existing rule at index $RULE_INDEX" + UPDATED_HR=$(replace_existing_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "$RULE_INDEX" "${METHOD:-}" "${VISIBILITY:-public}") + echo "$UPDATED_HR" | yq "." > "$HTTPROUTE_FILE" + else + # if there is no rule for the path we add a new one + echo "Case 3: Adding new rule" + UPDATED_HR=$(add_new_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "${METHOD:-}" "${VISIBILITY:-public}") + + # Debug: Check if hostnames and parentRefs are present before saving + echo "DEBUG: Checking HTTPRoute before saving..." + HOSTNAMES=$(echo "$UPDATED_HR" | yq eval '.spec.hostnames | length' -) + PARENTREFS=$(echo "$UPDATED_HR" | yq eval '.spec.parentRefs | length' -) + echo "DEBUG: Number of hostnames: $HOSTNAMES" + echo "DEBUG: Number of parentRefs: $PARENTREFS" + + echo "$UPDATED_HR" | yq "." > "$HTTPROUTE_FILE" + fi +fi + +echo "" +echo "=== HTTPRoute configuration saved to: $HTTPROUTE_FILE ===" diff --git a/endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup b/endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup new file mode 100644 index 0000000..3367222 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup @@ -0,0 +1,278 @@ +#!/bin/bash + +set -euo pipefail + +is_virtualservice_empty() { + local yaml_content="$1" + + local num_http_routes + local has_fault + local service_host + + num_http_routes=$(yq '.spec.http | length' <<< "$yaml_content") + has_fault=$(yq '.spec.http[0].fault.abort' <<< "$yaml_content") + service_host=$(yq '.spec.http[0].route[0].destination.host' <<< "$yaml_content") + + # A VirtualService is "empty" if it only has one route with a 404 fault injection to response-404 + if [[ "$num_http_routes" -eq 1 && \ + "$has_fault" != "null" && \ + "$service_host" =~ response-404 ]]; then + echo "true" + else + echo "false" + fi +} + +create_http_route() { + local rule_path="$1" + local service_json="$2" + local blue_green_config="$3" + + local service_name + local service_port + + service_name=$(echo "$service_json" | jq -r '.name') + service_port=$(echo "$service_json" | jq -r '.port.number // .port.name // 80') + + # Check if there's blue/green configuration + if [[ "$blue_green_config" != "null" && -n "$blue_green_config" ]]; then + # Parse blue/green destinations and weights from the annotation + local blue_weight green_weight blue_service green_service + + blue_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].weight // 100') + green_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].weight // 0') + blue_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].serviceName') + green_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].serviceName') + + # Create route with weighted destinations + jq -n \ + --arg path "$rule_path" \ + --arg blue_service "$blue_service" \ + --arg green_service "$green_service" \ + --arg service_port "$service_port" \ + --arg namespace "$K8S_NAMESPACE" \ + --argjson blue_weight "$blue_weight" \ + --argjson green_weight "$green_weight" \ + '{ + match: [ + { + uri: { + prefix: $path + } + } + ], + route: [ + { + destination: { + host: ($blue_service + "." + $namespace + ".svc.cluster.local"), + port: { + number: ($service_port | tonumber) + } + }, + weight: $blue_weight + }, + { + destination: { + host: ($green_service + "." + $namespace + ".svc.cluster.local"), + port: { + number: ($service_port | tonumber) + } + }, + weight: $green_weight + } + ] + }' + else + # Single destination without blue/green + jq -n \ + --arg path "$rule_path" \ + --arg service_name "$service_name" \ + --arg service_port "$service_port" \ + --arg namespace "$K8S_NAMESPACE" \ + '{ + match: [ + { + uri: { + prefix: $path + } + } + ], + route: [ + { + destination: { + host: ($service_name + "." + $namespace + ".svc.cluster.local"), + port: { + number: ($service_port | tonumber) + } + } + } + ] + }' + fi +} + +update_virtualservice_route() { + local vs_yaml="$1" + local rule_path="$2" + local service_json="$3" + local blue_green_config="$4" + + local service_name + local service_port + local updated_vs + + service_name=$(echo "$service_json" | jq -r '.name') + service_port=$(echo "$service_json" | jq -r '.port.number // .port.name // 80') + + # Remove fault injection + updated_vs=$(echo "$vs_yaml" | yq eval 'del(.spec.http[0].fault)') + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].match[0].uri.prefix = \"$rule_path\"") + + # Check if there's blue/green configuration + if [[ "$blue_green_config" != "null" && -n "$blue_green_config" ]]; then + # Parse blue/green destinations and weights + local blue_weight green_weight blue_service green_service + + blue_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].weight // 100') + green_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].weight // 0') + blue_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].serviceName') + green_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].serviceName') + + # Set blue destination + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.host = \"${blue_service}.${K8S_NAMESPACE}.svc.cluster.local\"") + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.port.number = $service_port") + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].weight = $blue_weight") + + # Add green destination + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route += [{\"destination\": {\"host\": \"${green_service}.${K8S_NAMESPACE}.svc.cluster.local\", \"port\": {\"number\": $service_port}}, \"weight\": $green_weight}]") + else + # Single destination + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.host = \"${service_name}.${K8S_NAMESPACE}.svc.cluster.local\"") + updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.port.number = $service_port") + fi + + echo "$updated_vs" +} + +find_route_index() { + local vs_yaml="$1" + local target_path="$2" + + local num_routes + local i + local current_path + + num_routes=$(yq '.spec.http | length' <<< "$vs_yaml") + + for ((i=0; i "$VS_FILE" +else + ROUTE_INDEX=$(find_route_index "$VIRTUALSERVICE" "$RULE_PATH") + echo "Found route index for path '$RULE_PATH': $ROUTE_INDEX" + + # if there is a route for the path we replace it + if [[ "$ROUTE_INDEX" != "-1" ]]; then + echo "Case 2: Replacing existing route at index $ROUTE_INDEX" + UPDATED_VS=$(replace_existing_route "$VIRTUALSERVICE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "$ROUTE_INDEX") + echo "$UPDATED_VS" | yq "." > "$VS_FILE" + else + # if there is no route for the path we add a new one + echo "Case 3: Adding new route" + UPDATED_VS=$(add_new_route "$VIRTUALSERVICE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG") + echo "$UPDATED_VS" | yq "." > "$VS_FILE" + fi +fi + +echo "" +echo "=== VirtualService configuration saved to: $VS_FILE ===" \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_ingress_without_rule b/endpoint-exposer/scripts/istio/build_ingress_without_rule new file mode 100644 index 0000000..2fef8a3 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_ingress_without_rule @@ -0,0 +1,62 @@ +#!/bin/bash + +set -euo pipefail + +find_rule_index() { + local hr_yaml="$1" + local target_path="$2" + + local num_rules + local i + local current_path + + num_rules=$(yq '.spec.rules | length' <<< "$hr_yaml") + + for ((i=0; i "$HTTPROUTE_FILE" + +echo "=== HTTPRoute configuration saved to: $HTTPROUTE_FILE ===" diff --git a/endpoint-exposer/scripts/istio/build_rule b/endpoint-exposer/scripts/istio/build_rule new file mode 100644 index 0000000..ef5675a --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_rule @@ -0,0 +1,179 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== DEBUG: Starting build_rule script ===" +echo "DEBUG: K8S_NAMESPACE=$K8S_NAMESPACE" +echo "DEBUG: SCOPE_ID=$SCOPE_ID" + +# Check for in-progress deployment +echo "DEBUG: Checking for in-progress deployment..." +SCOPE_JSON=$(np scope read --id "$SCOPE_ID" --format json) +echo "DEBUG: Scope JSON retrieved" + +IN_PROGRESS_DEPLOYMENT=$(echo "$SCOPE_JSON" | jq -r '.in_progress_deployment // "null"') +echo "DEBUG: IN_PROGRESS_DEPLOYMENT=$IN_PROGRESS_DEPLOYMENT" + +DEPLOYMENT_STATUS="" +SWITCHED_TRAFFIC=0 + +if [[ "$IN_PROGRESS_DEPLOYMENT" != "null" ]]; then + echo "DEBUG: Found in-progress deployment, fetching details..." + DEPLOYMENT_JSON=$(np deployment read --id "$IN_PROGRESS_DEPLOYMENT" --format json) + DEPLOYMENT_STATUS=$(echo "$DEPLOYMENT_JSON" | jq -r '.status') + SWITCHED_TRAFFIC=$(echo "$DEPLOYMENT_JSON" | jq -r '.strategy_data.desired_switched_traffic // 0') + echo "DEBUG: DEPLOYMENT_STATUS=$DEPLOYMENT_STATUS" + echo "DEBUG: SWITCHED_TRAFFIC=$SWITCHED_TRAFFIC" +fi + +# Get all services and filter by scope_id in selector +# Note: kubectl -l only filters by labels, not by selectors. scope_id is in the selector field. +echo "DEBUG: Fetching all services from namespace..." +ALL_SERVICES=$(kubectl get services -n "$K8S_NAMESPACE" -o json 2>&1) + +# Try to sanitize the JSON by removing any control characters or ANSI escape codes +echo "DEBUG: Sanitizing JSON output..." +ALL_SERVICES_CLEAN=$(echo "$ALL_SERVICES" | sed $'s/\x1b\\[[0-9;]*m//g' | tr -d '\000-\011\013-\037') +echo "DEBUG: Cleaned JSON length: ${#ALL_SERVICES_CLEAN} characters" + +# Check if we have valid JSON +if echo "$ALL_SERVICES_CLEAN" | jq empty 2>/dev/null; then + echo "DEBUG: JSON is valid after cleaning" + ALL_SERVICES="$ALL_SERVICES_CLEAN" +else + echo "DEBUG: WARNING - JSON may still have issues, attempting to parse anyway" + ALL_SERVICES="$ALL_SERVICES_CLEAN" +fi + +# Filter services by scope_id in selector (not label) +echo "DEBUG: Filtering services with scope_id=$SCOPE_ID in selector..." +SERVICES_JSON=$(echo "$ALL_SERVICES" | jq --arg scope_id "$SCOPE_ID" '{ + apiVersion: .apiVersion, + kind: .kind, + metadata: .metadata, + items: [.items[] | select(.spec.selector.scope_id == $scope_id)] +}') +echo "DEBUG: Filtered services JSON" + +NUM_SERVICES=$(echo "$SERVICES_JSON" | jq '.items | length') +echo "DEBUG: NUM_SERVICES=$NUM_SERVICES" + +if [[ "$NUM_SERVICES" -eq 0 ]]; then + echo "There is no service for scope_id=$SCOPE_ID. Publishing the rule with an empty backend" + + SCOPE_RULE='{"service": {"name": "response-404", "port": { "number": 80} }}' + echo "DEBUG: SCOPE_RULE (no services)=$SCOPE_RULE" +elif [[ "$NUM_SERVICES" -eq 1 ]]; then + echo "Found single service for scope_id=$SCOPE_ID" + + echo "DEBUG: Extracting service name and port..." + SERVICE_NAME=$(echo "$SERVICES_JSON" | jq -r '.items[0].metadata.name') + SERVICE_PORT=$(echo "$SERVICES_JSON" | jq -r '.items[0].spec.ports[0].port') + + echo "Service: $SERVICE_NAME, Port: $SERVICE_PORT" + echo "DEBUG: SERVICE_NAME=$SERVICE_NAME, SERVICE_PORT=$SERVICE_PORT" + + echo "DEBUG: Building SCOPE_RULE for single service..." + SCOPE_RULE=$(jq -n \ + --arg name "$SERVICE_NAME" \ + --argjson port "$SERVICE_PORT" \ + '{ + service: { + name: $name, + port: { + number: $port + } + } + }') + echo "DEBUG: SCOPE_RULE (single service)=$SCOPE_RULE" +else + echo "Detected blue/green deployment with $NUM_SERVICES services for scope_id=$SCOPE_ID" + + # Check if deployment is finalized - if so, only use the latest service + if [[ "$DEPLOYMENT_STATUS" == "finalized" ]]; then + echo "DEBUG: Deployment is finalized, using only the latest service" + + # Use only the first service (latest deployment) + BLUE_SERVICE=$(echo "$SERVICES_JSON" | jq -r '.items[0].metadata.name') + BLUE_PORT=$(echo "$SERVICES_JSON" | jq -r '.items[0].spec.ports[0].port') + + echo "Deployment finalized. Using service: $BLUE_SERVICE" + echo "DEBUG: SERVICE_NAME=$BLUE_SERVICE, SERVICE_PORT=$BLUE_PORT" + + SCOPE_RULE=$(jq -n \ + --arg name "$BLUE_SERVICE" \ + --argjson port "$BLUE_PORT" \ + '{ + service: { + name: $name, + port: { + number: $port + } + } + }') + echo "DEBUG: SCOPE_RULE (finalized deployment)=$SCOPE_RULE" + else + # Extract blue and green services + echo "DEBUG: Extracting blue service details..." + BLUE_SERVICE=$(echo "$SERVICES_JSON" | jq -r '.items[0].metadata.name') + BLUE_PORT=$(echo "$SERVICES_JSON" | jq -r '.items[0].spec.ports[0].port') + echo "DEBUG: BLUE_SERVICE=$BLUE_SERVICE, BLUE_PORT=$BLUE_PORT" + + echo "DEBUG: Extracting green service details..." + GREEN_SERVICE=$(echo "$SERVICES_JSON" | jq -r '.items[1].metadata.name') + GREEN_PORT=$(echo "$SERVICES_JSON" | jq -r '.items[1].spec.ports[0].port') + echo "DEBUG: GREEN_SERVICE=$GREEN_SERVICE, GREEN_PORT=$GREEN_PORT" + + # Determine weights based on deployment status + if [[ "$DEPLOYMENT_STATUS" == "running" ]]; then + echo "DEBUG: Deployment is running, using switched_traffic for weights" + # New service (green) gets the switched traffic percentage + GREEN_WEIGHT=$SWITCHED_TRAFFIC + # Old service (blue) gets the remaining traffic + BLUE_WEIGHT=$((100 - SWITCHED_TRAFFIC)) + echo "DEBUG: Using deployment weights - BLUE_WEIGHT=$BLUE_WEIGHT, GREEN_WEIGHT=$GREEN_WEIGHT" + else + # Fallback to annotation-based weights + echo "DEBUG: No running deployment, using annotation-based weights" + BLUE_WEIGHT=$(echo "$SERVICES_JSON" | jq -r '.items[0].metadata.annotations["weight"] // "100"' | sed 's/"//g') + GREEN_WEIGHT=$(echo "$SERVICES_JSON" | jq -r '.items[1].metadata.annotations["weight"] // "0"' | sed 's/"//g') + echo "DEBUG: BLUE_WEIGHT=$BLUE_WEIGHT (from annotation), GREEN_WEIGHT=$GREEN_WEIGHT (from annotation)" + fi + + echo "Blue: $BLUE_SERVICE (weight: $BLUE_WEIGHT), Green: $GREEN_SERVICE (weight: $GREEN_WEIGHT)" + + # Build blue/green annotation similar to ALB format + echo "DEBUG: Building SCOPE_RULE for blue/green deployment..." + SCOPE_RULE=$(jq -n \ + --arg blue_service "$BLUE_SERVICE" \ + --argjson blue_weight "$BLUE_WEIGHT" \ + --arg green_service "$GREEN_SERVICE" \ + --argjson green_weight "$GREEN_WEIGHT" \ + --argjson port "$BLUE_PORT" \ + '{ + blue_green_annotation: { + forward: { + targetGroups: [ + { + serviceName: $blue_service, + weight: $blue_weight + }, + { + serviceName: $green_service, + weight: $green_weight + } + ] + } + }, + service: { + name: $blue_service, + port: { + number: $port + } + } + }') + echo "DEBUG: SCOPE_RULE (blue/green)=$SCOPE_RULE" + fi +fi + +export SCOPE_RULE \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/clean_httproute_rules b/endpoint-exposer/scripts/istio/clean_httproute_rules new file mode 100755 index 0000000..85179f6 --- /dev/null +++ b/endpoint-exposer/scripts/istio/clean_httproute_rules @@ -0,0 +1,30 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== Cleaning HTTPRoute rules ===" +echo "HTTPROUTE_FILE: $HTTPROUTE_FILE" + +# Read the HTTPRoute +HTTPROUTE=$(cat "$HTTPROUTE_FILE") +echo "$HTTPROUTE" > "$HTTPROUTE_FILE.before" + +# Keep only the first rule (the fallback rule with response-404) +# Reset to empty state with just the fallback rule +CLEANED_HR=$(echo "$HTTPROUTE" | yq eval 'del(.spec.rules[1:])' -) + +# Ensure the first rule is the fallback rule +FIRST_RULE_BACKEND=$(echo "$CLEANED_HR" | yq eval '.spec.rules[0].backendRefs[0].name' -) + +if [[ "$FIRST_RULE_BACKEND" != "response-404" ]]; then + echo "WARNING: First rule is not the fallback rule. Resetting to fallback rule..." + + # Create a clean HTTPRoute with only the fallback rule + CLEANED_HR=$(echo "$CLEANED_HR" | yq eval '.spec.rules = [{"matches": [{"path": {"type": "PathPrefix", "value": "/"}}], "backendRefs": [{"name": "response-404", "port": 80, "weight": 0}]}]' -) +fi + +# Save the cleaned HTTPRoute +echo "$CLEANED_HR" > "$HTTPROUTE_FILE" +echo "$CLEANED_HR" > "$HTTPROUTE_FILE.cleaned" + +echo "HTTPRoute rules cleaned. Only fallback rule remains." diff --git a/endpoint-exposer/scripts/istio/fetch_httproute b/endpoint-exposer/scripts/istio/fetch_httproute new file mode 100755 index 0000000..080f4d3 --- /dev/null +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -0,0 +1,45 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== Fetching existing HTTPRoute from Kubernetes ===" +echo "SERVICE_ID: $SERVICE_ID" +echo "SERVICE_NAME: $SERVICE_NAME" +echo "K8S_NAMESPACE: $K8S_NAMESPACE" + +# Build the HTTPRoute name using the same pattern as the template +HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" +echo "HTTPRoute name: $HTTPROUTE_NAME" + +# Define output file +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + +# Create output directory if it doesn't exist +mkdir -p "$OUTPUT_DIR" + +# Fetch the existing HTTPRoute from Kubernetes +echo "Fetching HTTPRoute '$HTTPROUTE_NAME' from namespace '$K8S_NAMESPACE'..." +if kubectl get httproute "$HTTPROUTE_NAME" -n "$K8S_NAMESPACE" -o yaml > "$HTTPROUTE_FILE" 2>/dev/null; then + echo "HTTPRoute fetched successfully" + + # Remove only managed fields and status, but keep important metadata + yq eval 'del(.metadata.managedFields, .status)' -i "$HTTPROUTE_FILE" + + echo "HTTPRoute cleaned and saved to: $HTTPROUTE_FILE" +else + echo "ERROR: HTTPRoute '$HTTPROUTE_NAME' not found in namespace '$K8S_NAMESPACE'" + echo "Creating new HTTPRoute from template instead..." + + # If HTTPRoute doesn't exist, create from template as fallback + CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" + echo "$CONTEXT" > "$CONTEXT_PATH" + + gomplate -c .="$CONTEXT_PATH" \ + --file "$SERVICE_PATH/templates/istio/empty.yaml.tpl" \ + --out "$HTTPROUTE_FILE" + + rm "$CONTEXT_PATH" + echo "New HTTPRoute created from template" +fi + +echo "HTTPRoute file ready at: $HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/fetch_provider_data b/endpoint-exposer/scripts/istio/fetch_provider_data new file mode 100755 index 0000000..282c9a1 --- /dev/null +++ b/endpoint-exposer/scripts/istio/fetch_provider_data @@ -0,0 +1,22 @@ +#!/bin/bash + +NRN=$(echo "$CONTEXT" | jq -r .entity_nrn) + +DIMENSIONS=$(echo "$CONTEXT" | jq .service.dimensions) + +DIMENSION_FILTER=$(echo "$DIMENSIONS" | jq -r 'to_entries | map("\(.key):\(.value)") | join(",")') + +if [ -z "$DIMENSION_FILTER" ] || [ "$DIMENSION_FILTER" = "" ]; then + PROVIDER_DATA=$(np provider list --categories container-orchestration --nrn "$NRN" --format json | jq -r ".results[0]") +else + PROVIDER_DATA=$(np provider list --categories container-orchestration --nrn "$NRN" --dimensions "$DIMENSION_FILTER" --format json | jq -r ".results[0]") +fi + +# K8S_NAMESPACE=$(echo "$PROVIDER_DATA" | jq -r .attributes.cluster.namespace) + +ALB_NAME=$(echo "$PROVIDER_DATA" | jq -r --arg default "k8s-nullplatform-internet-facing" ' + .attributes.balancer.public_name // $default +') + +export K8S_NAMESPACE +export ALB_NAME \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes new file mode 100755 index 0000000..383330b --- /dev/null +++ b/endpoint-exposer/scripts/istio/process_routes @@ -0,0 +1,118 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== Starting process_routes script ===" +echo "SERVICE_ID: $SERVICE_ID" +echo "SERVICE_NAME: $SERVICE_NAME" +echo "K8S_NAMESPACE: $K8S_NAMESPACE" +echo "ROUTES_JSON: $ROUTES_JSON" + +# Check if we have any routes to process +NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') +echo "Number of routes to process: $NUM_ROUTES" + +if [[ "$NUM_ROUTES" -eq 0 ]]; then + echo "No routes to process" + exit 0 +fi + +# Build the HTTPRoute name using the same pattern as the template +HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" +echo "HTTPRoute name: $HTTPROUTE_NAME" + +# Define output file +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +export HTTPROUTE_FILE + +# Read the HTTPRoute from the file created in the previous step +if [[ ! -f "$HTTPROUTE_FILE" ]]; then + echo "ERROR: HTTPRoute file not found at $HTTPROUTE_FILE" + exit 1 +fi + +HTTPROUTE=$(cat "$HTTPROUTE_FILE") + +# Save initial HTTPRoute to file +echo "$HTTPROUTE" > "$HTTPROUTE_FILE" + +# Get application ID once +APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // empty') +if [[ -n "$APPLICATION_ID" ]]; then + echo "Application ID: $APPLICATION_ID" +else + echo "No Application ID found in context" + exit 1 +fi + +# Fetch all scopes once +echo "Fetching scopes for application $APPLICATION_ID..." +SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq -rs ".[].results") +echo "Scopes fetched successfully" + +# Sort routes by path specificity (Exact > RegularExpression > PathPrefix) +# Priority: 1=Exact, 2=RegularExpression, 3=PathPrefix +echo "" +echo "=== Sorting routes by specificity ===" +SORTED_ROUTES=$(echo "$ROUTES_JSON" | jq 'sort_by( + if (.path | contains("*")) then 3 + elif (.path | contains(":")) then 2 + else 1 + end +)') +ROUTES_JSON="$SORTED_ROUTES" +echo "Routes sorted by specificity (Exact > RegularExpression > PathPrefix)" + +# Process each route +for ((i=0; i "$HTTPROUTE_FILE" + +echo "HTTPRoute hostname updated to: $PUBLIC_DOMAIN" +echo "HTTPRoute parentRefs set to gateway-public" + +# Debug: Verify the file was saved correctly +echo "DEBUG: Verifying saved file..." +SAVED_HOSTNAMES=$(cat "$HTTPROUTE_FILE" | yq eval '.spec.hostnames | length' -) +SAVED_PARENTREFS=$(cat "$HTTPROUTE_FILE" | yq eval '.spec.parentRefs | length' -) +echo "DEBUG: Saved file has $SAVED_HOSTNAMES hostnames" +echo "DEBUG: Saved file has $SAVED_PARENTREFS parentRefs" diff --git a/endpoint-exposer/scripts/istio/update_service b/endpoint-exposer/scripts/istio/update_service new file mode 100644 index 0000000..5f743d3 --- /dev/null +++ b/endpoint-exposer/scripts/istio/update_service @@ -0,0 +1,21 @@ +#!/bin/bash + +if [[ $ACTION == "add" ]]; then + UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" --arg scope "$SCOPE_SLUG" ' + .service.attributes |= (.rules //= [] | .rules += [{path: $path, scope: $scope}]) | + { + attributes: .service.attributes, + entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) + } + ') +elif [[ $ACTION == "remove" ]]; then + UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' + .service.attributes |= (.rules //= [] | .rules |= map(select(.path != $path))) | + { + attributes: .service.attributes, + entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) + } + ') +fi + +np service patch --id "$SERVICE_ID" --body "$UPDATED_ATTRIBUTES" diff --git a/endpoint-exposer/scripts/istio/validate_unique_rule b/endpoint-exposer/scripts/istio/validate_unique_rule new file mode 100644 index 0000000..49a56b1 --- /dev/null +++ b/endpoint-exposer/scripts/istio/validate_unique_rule @@ -0,0 +1,12 @@ +#!/bin/bash + +EXISTS=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' + (.service.attributes.rules // []) + | map(select(.path == $path)) + | length +') + +if [[ "$EXISTS" -gt 0 ]]; then + echo "Error: A rule with path '$RULE_PATH' already exists." + exit 1 +fi \ No newline at end of file diff --git a/endpoint-exposer/specs/actions/read.json.tpl b/endpoint-exposer/specs/actions/read.json.tpl new file mode 100644 index 0000000..f6a4c52 --- /dev/null +++ b/endpoint-exposer/specs/actions/read.json.tpl @@ -0,0 +1,25 @@ +{ + "name": "Read", + "slug": "read", + "type": "custom", + "annotations": {}, + "enabled_when": "", + "retryable": false, + "service_specification_id": "{{ env.Getenv "SERVICE_SPECIFICATION_ID" }}", + "parameters": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "values": {} + }, + "results": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "values": {} + } +} \ No newline at end of file diff --git a/endpoint-exposer/specs/link-spec.json.tpl b/endpoint-exposer/specs/link-spec.json.tpl new file mode 100644 index 0000000..3f45d10 --- /dev/null +++ b/endpoint-exposer/specs/link-spec.json.tpl @@ -0,0 +1,28 @@ +{ + "assignable_to": "scope", + "attributes": { + "schema": { + "properties": { + }, + "required": [ + ], + "type": "object" + }, + "values": {} + }, + "dimensions": {}, + "name": "Publish", + "selectors": { + "category": "any", + "imported": false, + "provider": "any", + "sub_category": "any" + }, + "slug": "publish", + "specification_id": "{{ env.Getenv "SERVICE_SPECIFICATION_ID" }}", + "unique": false, + "use_default_actions": true, + "visible_to": [ + "{{ env.Getenv "NRN" }}" + ] +} diff --git a/endpoint-exposer/specs/links/refresh.json.tpl b/endpoint-exposer/specs/links/refresh.json.tpl new file mode 100644 index 0000000..bd56670 --- /dev/null +++ b/endpoint-exposer/specs/links/refresh.json.tpl @@ -0,0 +1,25 @@ +{ + "name": "Refresh", + "slug": "refresh", + "type": "custom", + "annotations": {}, + "enabled_when": "", + "retryable": false, + "link_specification_id": "{{ env.Getenv "LINK_SPECIFICATION_ID" }}", + "parameters": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "values": {} + }, + "results": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "values": {} + } +} \ No newline at end of file diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl new file mode 100644 index 0000000..741adad --- /dev/null +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -0,0 +1,138 @@ +{ + "assignable_to": "dimension", + "attributes": { + "schema": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "uiSchema": { + "type": "VerticalLayout", + "elements": [ + { + "type": "Group", + "label": "Domains", + "elements": [ + { + "type": "Control", + "scope": "#/properties/publicDomain" + }, + { + "type": "Control", + "scope": "#/properties/privateDomain" + } + ] + }, + { + "type": "Group", + "label": "Routes", + "elements": [ + { + "type": "Control", + "scope": "#/properties/routes", + "options": { + "detail": { + "type": "GridLayout", + "columns": 4, + "elements": [ + { + "type": "Control", + "label": "Verb", + "scope": "#/items/properties/method" + }, + { + "type": "Control", + "label": "Path", + "scope": "#/items/properties/path" + }, + { + "type": "Control", + "label": "Visibility", + "scope": "#/items/properties/visibility" + }, + { + "type": "Control", + "label": "Scope", + "scope": "#/items/properties/scope" + } + ] + }, + "showSortButtons": true + } + } + ] + } + ] + }, + "properties": { + "publicDomain": { + "type": "string", + "editableOn": ["create", "update"] + }, + "privateDomain": { + "type": "string", + "editableOn": ["create", "update"] + }, + "routes": { + "items": { + "properties": { + "method": { + "type": "string", + "title": "Verb", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] + }, + "path": { + "type": "string", + "title": "Path" + }, + "visibility": { + "type": "string", + "title": "Visibility", + "enum": ["internal", "public"] + }, + "scope": { + "type": "string", + "title": "Scope", + "description": "The scope slug", + "additionalKeywords": { + "enum": "[.scopes[]?.slug]" + } + } + }, + "required": [ + "method", + "path", + "visibility", + "scope" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "publicDomain", + "privateDomain" + ], + "type": "object" + }, + "values": {} + }, + "dimensions": {}, + "name": "Service exposer V2", + "selectors": { + "category": "any", + "imported": false, + "provider": "any", + "sub_category": "any" + }, + "slug": "service-exposer", + "type": "dependency", + "use_default_actions": true, + "available_actions": [ + "read" + ], + "available_links": [ + ], + "visible_to": [ + "{{ env.Getenv "NRN" }}" + ] +} diff --git a/endpoint-exposer/templates/alb/empty.yaml.tpl b/endpoint-exposer/templates/alb/empty.yaml.tpl new file mode 100644 index 0000000..35ef800 --- /dev/null +++ b/endpoint-exposer/templates/alb/empty.yaml.tpl @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: k-8-s-{{ .service.slug }}-{{ .service.id }}-public + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service.slug }} + service_id: {{ .service.id }} + annotations: + kubernetes.io/ingress.class: alb + alb.ingress.kubernetes.io/group.name: {{ .alb_name }} + alb.ingress.kubernetes.io/load-balancer-name: {{ .alb_name }} + alb.ingress.kubernetes.io/scheme: internet-facing + alb.ingress.kubernetes.io/target-type: ip + alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]' + alb.ingress.kubernetes.io/ssl-redirect: "443" + alb.ingress.kubernetes.io/actions.response-404: >- + {"type":"fixed-response","fixedResponseConfig":{"contentType":"text/plain","statusCode":"404","messageBody":"no scopes exposed through this service."}} +spec: + ingressClassName: alb + rules: + - host: {{ .parameters.domain }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: response-404 + port: + name: use-annotation \ No newline at end of file diff --git a/endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl b/endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl new file mode 100644 index 0000000..e8983cf --- /dev/null +++ b/endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl @@ -0,0 +1,16 @@ +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: {{ .service.slug }}-{{ .service.id }}-dns + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service.slug }} + service_id: {{ .service.id }} +spec: + endpoints: + - dnsName: {{ if has . "parameters" }}{{ if has .parameters "public_domain" }}{{ .parameters.public_domain }}{{ else if has .parameters "domain" }}{{ .parameters.domain }}{{ else }}{{ .service.attributes.domain }}{{ end }}{{ else }}{{ .service.attributes.domain }}{{ end }} + recordTTL: 60 + recordType: A + targets: + - "{{ .gateway_ip }}" diff --git a/endpoint-exposer/templates/istio/empty.yaml.tpl b/endpoint-exposer/templates/istio/empty.yaml.tpl new file mode 100644 index 0000000..d0dd6de --- /dev/null +++ b/endpoint-exposer/templates/istio/empty.yaml.tpl @@ -0,0 +1,24 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .service.slug }}-{{ .service.id }}-route + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service.slug }} + service_id: {{ .service.id }} +spec: + parentRefs: + - name: gateway-public + namespace: gateways + hostnames: + - {{ if has . "parameters" }}{{ if has .parameters "public_domain" }}{{ .parameters.public_domain }}{{ else if has .parameters "domain" }}{{ .parameters.domain }}{{ else }}{{ .service.attributes.domain }}{{ end }}{{ else }}{{ .service.attributes.domain }}{{ end }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: response-404 + port: 80 + weight: 0 \ No newline at end of file diff --git a/endpoint-exposer/values.yaml b/endpoint-exposer/values.yaml new file mode 100644 index 0000000..6831afc --- /dev/null +++ b/endpoint-exposer/values.yaml @@ -0,0 +1,2 @@ +configuration: + K8S_NAMESPACE: nullplatform \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/create.yaml b/endpoint-exposer/workflows/alb/create.yaml new file mode 100644 index 0000000..7c8c3a2 --- /dev/null +++ b/endpoint-exposer/workflows/alb/create.yaml @@ -0,0 +1,44 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/alb/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: LINK_ID + type: environment + - name: LINK_NAME + type: environment + - name: SCOPE_ID + type: environment + - name: RULE_PATH + type: environment + - name: "build template" + type: script + file: "$SERVICE_PATH/scripts/alb/build_ingress" + configuration: + TEMPLATE: "$SERVICE_PATH/templates/alb/empty.yaml.tpl" + output: + - name: INGRESS_FILE + type: file + file: "$OUTPUT_DIR/ingress-$SERVICE_ID-$SCOPE_ID-public.yaml" + - name: apply + type: script + file: "$SERVICE_PATH/scripts/alb/apply" + configuration: + ACTION: apply + DRY_RUN: false \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/delete.yaml b/endpoint-exposer/workflows/alb/delete.yaml new file mode 100644 index 0000000..3c7309e --- /dev/null +++ b/endpoint-exposer/workflows/alb/delete.yaml @@ -0,0 +1,6 @@ +include: + - "$SERVICE_PATH/workflows/alb/create.yaml" +steps: + - name: apply + configuration: + ACTION: delete \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/link.yaml b/endpoint-exposer/workflows/alb/link.yaml new file mode 100644 index 0000000..0145798 --- /dev/null +++ b/endpoint-exposer/workflows/alb/link.yaml @@ -0,0 +1,56 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/alb/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: LINK_ID + type: environment + - name: LINK_NAME + type: environment + - name: SCOPE_ID + type: environment + - name: RULE_PATH + type: environment + - name: "validate_unique_rule" + type: script + file: "$SERVICE_PATH/scripts/alb/validate_unique_rule" + - name: "build_rule" + type: script + file : "$SERVICE_PATH/scripts/alb/build_rule" + output: + - name: SCOPE_RULE + type: environment + - name: "build_ingress" + type: script + file: "$SERVICE_PATH/scripts/alb/build_ingress_with_rule" + output: + - name: INGRESS_FILE + type: file + file: "$OUTPUT_DIR/ingress-$SERVICE_ID-$SCOPE_ID-public.yaml" + - name: apply + type: script + file: "$SERVICE_PATH/scripts/alb/apply" + configuration: + ACTION: apply + DRY_RUN: false + - name: "add_rule_to_service" + type: script + file: "$SERVICE_PATH/scripts/alb/update_service" + configuration: + ACTION: add \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/read.yaml b/endpoint-exposer/workflows/alb/read.yaml new file mode 100644 index 0000000..8dea452 --- /dev/null +++ b/endpoint-exposer/workflows/alb/read.yaml @@ -0,0 +1,32 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/alb/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: LINK_ID + type: environment + - name: LINK_NAME + type: environment + - name: SCOPE_ID + type: environment + - name: RULE_PATH + type: environment + - name: read + type: script + file: "$SERVICE_PATH/scripts/alb/read_ingress" \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/refresh.yaml b/endpoint-exposer/workflows/alb/refresh.yaml new file mode 100644 index 0000000..6edef06 --- /dev/null +++ b/endpoint-exposer/workflows/alb/refresh.yaml @@ -0,0 +1,7 @@ +include: + - "$SERVICE_PATH/workflows/alb/link.yaml" +steps: + - name: "validate_unique_rule" + action: skip + - name: "add_rule_to_service" + action: skip \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/unlink.yaml b/endpoint-exposer/workflows/alb/unlink.yaml new file mode 100644 index 0000000..690374a --- /dev/null +++ b/endpoint-exposer/workflows/alb/unlink.yaml @@ -0,0 +1,47 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/alb/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: LINK_ID + type: environment + - name: LINK_NAME + type: environment + - name: SCOPE_ID + type: environment + - name: RULE_PATH + type: environment + - name: "build_template" + type: script + file: "$SERVICE_PATH/scripts/alb/build_ingress_without_rule" + output: + - name: INGRESS_FILE + type: file + file: "$OUTPUT_DIR/ingress-$SERVICE_ID-$SCOPE_ID-public.yaml" + - name: apply + type: script + file: "$SERVICE_PATH/scripts/alb/apply" + configuration: + ACTION: apply + DRY_RUN: false + - name: "add_rule_to_service" + type: script + file: "$SERVICE_PATH/scripts/alb/update_service" + configuration: + ACTION: remove \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml new file mode 100644 index 0000000..2d28e27 --- /dev/null +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -0,0 +1,56 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: PUBLIC_DOMAIN + type: environment + - name: PRIVATE_DOMAIN + type: environment + - name: ROUTES_JSON + type: environment + - name: "build httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/build_ingress" + configuration: + TEMPLATE: "$SERVICE_PATH/templates/istio/empty.yaml.tpl" + output: + - name: HTTPROUTE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "process routes" + type: script + file: "$SERVICE_PATH/scripts/istio/process_routes" + output: + - name: HTTPROUTE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "build dns endpoint" + type: script + file: "$SERVICE_PATH/scripts/dns/build_dns" + output: + - name: DNS_FILE + type: file + file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" + - name: apply + type: script + file: "$SERVICE_PATH/scripts/istio/apply" + configuration: + ACTION: apply + DRY_RUN: false \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/delete.yaml b/endpoint-exposer/workflows/istio/delete.yaml new file mode 100644 index 0000000..1f4d320 --- /dev/null +++ b/endpoint-exposer/workflows/istio/delete.yaml @@ -0,0 +1,6 @@ +include: + - "$SERVICE_PATH/workflows/istio/create.yaml" +steps: + - name: apply + configuration: + ACTION: delete \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/link.yaml b/endpoint-exposer/workflows/istio/link.yaml new file mode 100644 index 0000000..8c757ea --- /dev/null +++ b/endpoint-exposer/workflows/istio/link.yaml @@ -0,0 +1,6 @@ +steps: + - name: "link created" + type: script + file: "$SERVICE_PATH/scripts/common/echo" + configuration: + MESSAGE: "link-created" diff --git a/endpoint-exposer/workflows/istio/read.yaml b/endpoint-exposer/workflows/istio/read.yaml new file mode 100644 index 0000000..5251e42 --- /dev/null +++ b/endpoint-exposer/workflows/istio/read.yaml @@ -0,0 +1,32 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: LINK_ID + type: environment + - name: LINK_NAME + type: environment + - name: SCOPE_ID + type: environment + - name: RULE_PATH + type: environment + - name: read + type: script + file: "$SERVICE_PATH/scripts/istio/read_ingress" \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/refresh.yaml b/endpoint-exposer/workflows/istio/refresh.yaml new file mode 100644 index 0000000..a920564 --- /dev/null +++ b/endpoint-exposer/workflows/istio/refresh.yaml @@ -0,0 +1,7 @@ +include: + - "$SERVICE_PATH/workflows/istio/link.yaml" +steps: + - name: "validate_unique_rule" + action: skip + - name: "add_rule_to_service" + action: skip \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/unlink.yaml b/endpoint-exposer/workflows/istio/unlink.yaml new file mode 100644 index 0000000..5ac94f9 --- /dev/null +++ b/endpoint-exposer/workflows/istio/unlink.yaml @@ -0,0 +1,6 @@ +steps: + - name: "link removed" + type: script + file: "$SERVICE_PATH/scripts/common/echo" + configuration: + MESSAGE: "link-removed" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml new file mode 100644 index 0000000..e1e66a6 --- /dev/null +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -0,0 +1,60 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: ALB_NAME + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_NAME + type: environment + - name: ACTION_ID + type: environment + - name: ACTION_NAME + type: environment + - name: PUBLIC_DOMAIN + type: environment + - name: PRIVATE_DOMAIN + type: environment + - name: ROUTES_JSON + type: environment + - name: "fetch existing httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_httproute" + output: + - name: HTTPROUTE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "update httproute hostname" + type: script + file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" + - name: "clean httproute rules" + type: script + file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" + - name: "process routes" + type: script + file: "$SERVICE_PATH/scripts/istio/process_routes" + output: + - name: HTTPROUTE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "update dns endpoint" + type: script + file: "$SERVICE_PATH/scripts/dns/build_dns" + output: + - name: DNS_FILE + type: file + file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" + - name: apply + type: script + file: "$SERVICE_PATH/scripts/istio/apply" + configuration: + ACTION: apply + DRY_RUN: false From 303f6ce169b4c46ab269a1c1fd55f77dde09fd2c Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 15:36:15 -0300 Subject: [PATCH 002/101] feat: http routes to handle private and public traffic --- endpoint-exposer/scripts/istio/build_ingress | 23 ++- .../scripts/istio/fetch_httproute | 20 ++- endpoint-exposer/scripts/istio/process_routes | 151 +++++++++++------- .../scripts/istio/update_httproute_hostname | 19 ++- .../templates/istio/empty-private.yaml.tpl | 25 +++ endpoint-exposer/workflows/istio/create.yaml | 19 ++- endpoint-exposer/workflows/istio/update.yaml | 43 ++++- 7 files changed, 218 insertions(+), 82 deletions(-) create mode 100644 endpoint-exposer/templates/istio/empty-private.yaml.tpl diff --git a/endpoint-exposer/scripts/istio/build_ingress b/endpoint-exposer/scripts/istio/build_ingress index bbea5c3..72f849a 100644 --- a/endpoint-exposer/scripts/istio/build_ingress +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -2,13 +2,24 @@ set -euo pipefail -SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') +# Determine output file - use OUTPUT_FILE if set, otherwise default to public +if [[ -n "${OUTPUT_FILE:-}" ]]; then + HTTPROUTE_FILE="$OUTPUT_FILE" + echo "Using specified output file: $HTTPROUTE_FILE" +else + HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + echo "Using default output file: $HTTPROUTE_FILE" +fi + +# Determine which domain to use based on template +if [[ "$TEMPLATE" == *"private"* ]]; then + SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain // .service.attributes.private_domain') + echo "Creating private HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" +else + SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') + echo "Creating public HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" +fi -echo "Creating HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" - -echo "Creating public HTTPRoute..." - -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" echo "$CONTEXT" > "$CONTEXT_PATH" diff --git a/endpoint-exposer/scripts/istio/fetch_httproute b/endpoint-exposer/scripts/istio/fetch_httproute index 080f4d3..a5ce47a 100755 --- a/endpoint-exposer/scripts/istio/fetch_httproute +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -7,12 +7,26 @@ echo "SERVICE_ID: $SERVICE_ID" echo "SERVICE_NAME: $SERVICE_NAME" echo "K8S_NAMESPACE: $K8S_NAMESPACE" +# Determine visibility (default to public if not set) +VISIBILITY="${VISIBILITY:-public}" +echo "Visibility: $VISIBILITY" + # Build the HTTPRoute name using the same pattern as the template -HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" +if [[ "$VISIBILITY" == "private" ]]; then + HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route-private" + TEMPLATE="$SERVICE_PATH/templates/istio/empty-private.yaml.tpl" +else + HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" + TEMPLATE="$SERVICE_PATH/templates/istio/empty.yaml.tpl" +fi echo "HTTPRoute name: $HTTPROUTE_NAME" -# Define output file -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +# Define output file - use OUTPUT_FILE if set, otherwise default based on visibility +if [[ -n "${OUTPUT_FILE:-}" ]]; then + HTTPROUTE_FILE="$OUTPUT_FILE" +else + HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-$VISIBILITY.yaml" +fi # Create output directory if it doesn't exist mkdir -p "$OUTPUT_DIR" diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes index 383330b..a1e60f5 100755 --- a/endpoint-exposer/scripts/istio/process_routes +++ b/endpoint-exposer/scripts/istio/process_routes @@ -17,25 +17,6 @@ if [[ "$NUM_ROUTES" -eq 0 ]]; then exit 0 fi -# Build the HTTPRoute name using the same pattern as the template -HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" -echo "HTTPRoute name: $HTTPROUTE_NAME" - -# Define output file -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" -export HTTPROUTE_FILE - -# Read the HTTPRoute from the file created in the previous step -if [[ ! -f "$HTTPROUTE_FILE" ]]; then - echo "ERROR: HTTPRoute file not found at $HTTPROUTE_FILE" - exit 1 -fi - -HTTPROUTE=$(cat "$HTTPROUTE_FILE") - -# Save initial HTTPRoute to file -echo "$HTTPROUTE" > "$HTTPROUTE_FILE" - # Get application ID once APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // empty') if [[ -n "$APPLICATION_ID" ]]; then @@ -63,56 +44,110 @@ SORTED_ROUTES=$(echo "$ROUTES_JSON" | jq 'sort_by( ROUTES_JSON="$SORTED_ROUTES" echo "Routes sorted by specificity (Exact > RegularExpression > PathPrefix)" -# Process each route -for ((i=0; i "$HTTPROUTE_FILE" -echo "HTTPRoute hostname updated to: $PUBLIC_DOMAIN" -echo "HTTPRoute parentRefs set to gateway-public" +echo "HTTPRoute hostname updated to: $DOMAIN" +echo "HTTPRoute parentRefs set to: $GATEWAY" # Debug: Verify the file was saved correctly echo "DEBUG: Verifying saved file..." diff --git a/endpoint-exposer/templates/istio/empty-private.yaml.tpl b/endpoint-exposer/templates/istio/empty-private.yaml.tpl new file mode 100644 index 0000000..ae74cf3 --- /dev/null +++ b/endpoint-exposer/templates/istio/empty-private.yaml.tpl @@ -0,0 +1,25 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .service.slug }}-{{ .service.id }}-route-private + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service.slug }} + service_id: {{ .service.id }} + visibility: private +spec: + parentRefs: + - name: gateway-public + namespace: gateways + hostnames: + - {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: response-404 + port: 80 + weight: 0 diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 2d28e27..ae9efb2 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -25,22 +25,35 @@ steps: type: environment - name: ROUTES_JSON type: environment - - name: "build httproute" + - name: "build public httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_ingress" configuration: TEMPLATE: "$SERVICE_PATH/templates/istio/empty.yaml.tpl" output: - - name: HTTPROUTE_FILE + - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "build private httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/build_ingress" + configuration: + TEMPLATE: "$SERVICE_PATH/templates/istio/empty-private.yaml.tpl" + OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + output: + - name: HTTPROUTE_FILE_PRIVATE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - name: "process routes" type: script file: "$SERVICE_PATH/scripts/istio/process_routes" output: - - name: HTTPROUTE_FILE + - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: HTTPROUTE_FILE_PRIVATE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - name: "build dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index e1e66a6..c6e5b6e 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -25,26 +25,59 @@ steps: type: environment - name: ROUTES_JSON type: environment - - name: "fetch existing httproute" + - name: "fetch existing public httproute" type: script file: "$SERVICE_PATH/scripts/istio/fetch_httproute" + configuration: + VISIBILITY: "public" output: - - name: HTTPROUTE_FILE + - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: "update httproute hostname" + - name: "fetch existing private httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_httproute" + configuration: + VISIBILITY: "private" + OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + output: + - name: HTTPROUTE_FILE_PRIVATE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + - name: "update public httproute hostname" type: script file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" - - name: "clean httproute rules" + configuration: + HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + DOMAIN: "$PUBLIC_DOMAIN" + GATEWAY: "gateway-public" + - name: "update private httproute hostname" + type: script + file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" + configuration: + HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + DOMAIN: "$PRIVATE_DOMAIN" + GATEWAY: "gateway-private" + - name: "clean public httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" + configuration: + HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "clean private httproute rules" + type: script + file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" + configuration: + HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - name: "process routes" type: script file: "$SERVICE_PATH/scripts/istio/process_routes" output: - - name: HTTPROUTE_FILE + - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: HTTPROUTE_FILE_PRIVATE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - name: "update dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" From c95b552ea74876bece0af2dda40fb35bf02a9879 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 15:47:44 -0300 Subject: [PATCH 003/101] feat: http routes to handle private and public traffic --- .../scripts/dns/build_dns_endpoint | 34 ++++++++++++++----- .../istio/dns-endpoint-private.yaml.tpl | 17 ++++++++++ .../templates/istio/empty-private.yaml.tpl | 2 +- endpoint-exposer/workflows/istio/create.yaml | 15 ++++++-- endpoint-exposer/workflows/istio/update.yaml | 15 ++++++-- 5 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl diff --git a/endpoint-exposer/scripts/dns/build_dns_endpoint b/endpoint-exposer/scripts/dns/build_dns_endpoint index c43327f..1340a11 100755 --- a/endpoint-exposer/scripts/dns/build_dns_endpoint +++ b/endpoint-exposer/scripts/dns/build_dns_endpoint @@ -4,26 +4,42 @@ set -euo pipefail echo "=== DEBUG: Starting build_dns_endpoint ===" -SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain') -echo "DEBUG: SERVICE_DOMAIN=$SERVICE_DOMAIN" +# Determine visibility (default to public if not set) +VISIBILITY="${VISIBILITY:-public}" +echo "DEBUG: VISIBILITY=$VISIBILITY" + +# Select domain and gateway based on visibility +if [[ "$VISIBILITY" == "private" ]]; then + SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain') + GATEWAY_NAME=${PRIVATE_GATEWAY_NAME:-"gateway-private"} + DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint-private.yaml.tpl" + DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-private.yaml" + echo "DEBUG: Building PRIVATE DNS endpoint" +else + SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain') + GATEWAY_NAME=${PUBLIC_GATEWAY_NAME:-"gateway-public"} + DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint.yaml.tpl" + DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" + echo "DEBUG: Building PUBLIC DNS endpoint" +fi +echo "DEBUG: SERVICE_DOMAIN=$SERVICE_DOMAIN" +echo "DEBUG: GATEWAY_NAME=$GATEWAY_NAME" -PUBLIC_GATEWAY_NAME=${PUBLIC_GATEWAY_NAME:-"gateway-public"} GATEWAY_NAMESPACE=${GATEWAY_NAMESPACE:-"gateways"} -echo "DEBUG: Fetching gateway IP from $PUBLIC_GATEWAY_NAME..." -GATEWAY_IP=$(kubectl get gateway -n "$GATEWAY_NAMESPACE" "$PUBLIC_GATEWAY_NAME" -o jsonpath='{.status.addresses[0].value}') +echo "DEBUG: Fetching gateway IP from $GATEWAY_NAME..." +GATEWAY_IP=$(kubectl get gateway -n "$GATEWAY_NAMESPACE" "$GATEWAY_NAME" -o jsonpath='{.status.addresses[0].value}') echo "DEBUG: GATEWAY_IP=$GATEWAY_IP" if [[ -z "$GATEWAY_IP" || "$GATEWAY_IP" == "null" ]]; then - echo "ERROR: Could not retrieve gateway IP from $PUBLIC_GATEWAY_NAME" + echo "ERROR: Could not retrieve gateway IP from $GATEWAY_NAME" exit 1 fi echo "Creating DNSEndpoint for service $SERVICE_NAME with domain $SERVICE_DOMAIN" -DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" -CONTEXT_PATH="$OUTPUT_DIR/context-dns-$SERVICE_ID.json" +CONTEXT_PATH="$OUTPUT_DIR/context-dns-$SERVICE_ID-$VISIBILITY.json" # Add gateway_ip to context CONTEXT_WITH_IP=$(echo "$CONTEXT" | jq --arg gateway_ip "$GATEWAY_IP" '. + {gateway_ip: $gateway_ip}') @@ -33,7 +49,7 @@ echo "$CONTEXT_WITH_IP" > "$CONTEXT_PATH" echo "Building DNSEndpoint Template to $DNS_FILE" gomplate -c .="$CONTEXT_PATH" \ - --file "$SERVICE_PATH/templates/istio/dns-endpoint.yaml.tpl" \ + --file "$DNS_TEMPLATE" \ --out "$DNS_FILE" rm "$CONTEXT_PATH" diff --git a/endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl b/endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl new file mode 100644 index 0000000..5bcdc5c --- /dev/null +++ b/endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl @@ -0,0 +1,17 @@ +apiVersion: externaldns.k8s.io/v1alpha1 +kind: DNSEndpoint +metadata: + name: {{ .service.slug }}-{{ .service.id }}-dns-private + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service.slug }} + service_id: {{ .service.id }} + visibility: private +spec: + endpoints: + - dnsName: {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} + recordTTL: 60 + recordType: A + targets: + - "{{ .gateway_ip }}" diff --git a/endpoint-exposer/templates/istio/empty-private.yaml.tpl b/endpoint-exposer/templates/istio/empty-private.yaml.tpl index ae74cf3..65698f2 100644 --- a/endpoint-exposer/templates/istio/empty-private.yaml.tpl +++ b/endpoint-exposer/templates/istio/empty-private.yaml.tpl @@ -10,7 +10,7 @@ metadata: visibility: private spec: parentRefs: - - name: gateway-public + - name: gateway-private namespace: gateways hostnames: - {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index ae9efb2..538d8f7 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -54,13 +54,24 @@ steps: - name: HTTPROUTE_FILE_PRIVATE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "build dns endpoint" + - name: "build public dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" + configuration: + VISIBILITY: "public" output: - - name: DNS_FILE + - name: DNS_FILE_PUBLIC type: file file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" + - name: "build private dns endpoint" + type: script + file: "$SERVICE_PATH/scripts/dns/build_dns" + configuration: + VISIBILITY: "private" + output: + - name: DNS_FILE_PRIVATE + type: file + file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/istio/apply" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index c6e5b6e..41cbd4b 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -78,13 +78,24 @@ steps: - name: HTTPROUTE_FILE_PRIVATE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "update dns endpoint" + - name: "update public dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" + configuration: + VISIBILITY: "public" output: - - name: DNS_FILE + - name: DNS_FILE_PUBLIC type: file file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" + - name: "update private dns endpoint" + type: script + file: "$SERVICE_PATH/scripts/dns/build_dns" + configuration: + VISIBILITY: "private" + output: + - name: DNS_FILE_PRIVATE + type: file + file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/istio/apply" From d63abe289ed3ea5415a7221f68ccb21e3f15cbca Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 16:08:47 -0300 Subject: [PATCH 004/101] feat: http routes to handle private and public traffic --- .../scripts/dns/build_dns_endpoint | 10 +++--- endpoint-exposer/scripts/istio/build_ingress | 4 +-- .../scripts/istio/fetch_httproute | 6 ++-- endpoint-exposer/scripts/istio/process_routes | 14 ++++---- ...aml.tpl => dns-endpoint-internal.yaml.tpl} | 4 +-- ...ivate.yaml.tpl => empty-internal.yaml.tpl} | 6 ++-- endpoint-exposer/workflows/istio/create.yaml | 24 +++++++------- endpoint-exposer/workflows/istio/update.yaml | 32 +++++++++---------- 8 files changed, 50 insertions(+), 50 deletions(-) rename endpoint-exposer/templates/istio/{dns-endpoint-private.yaml.tpl => dns-endpoint-internal.yaml.tpl} (86%) rename endpoint-exposer/templates/istio/{empty-private.yaml.tpl => empty-internal.yaml.tpl} (85%) diff --git a/endpoint-exposer/scripts/dns/build_dns_endpoint b/endpoint-exposer/scripts/dns/build_dns_endpoint index 1340a11..a5bc6ef 100755 --- a/endpoint-exposer/scripts/dns/build_dns_endpoint +++ b/endpoint-exposer/scripts/dns/build_dns_endpoint @@ -9,12 +9,12 @@ VISIBILITY="${VISIBILITY:-public}" echo "DEBUG: VISIBILITY=$VISIBILITY" # Select domain and gateway based on visibility -if [[ "$VISIBILITY" == "private" ]]; then +if [[ "$VISIBILITY" == "internal" ]]; then SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain') - GATEWAY_NAME=${PRIVATE_GATEWAY_NAME:-"gateway-private"} - DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint-private.yaml.tpl" - DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-private.yaml" - echo "DEBUG: Building PRIVATE DNS endpoint" + GATEWAY_NAME=${INTERNAL_GATEWAY_NAME:-"gateway-internal"} + DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint-internal.yaml.tpl" + DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" + echo "DEBUG: Building INTERNAL DNS endpoint" else SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain') GATEWAY_NAME=${PUBLIC_GATEWAY_NAME:-"gateway-public"} diff --git a/endpoint-exposer/scripts/istio/build_ingress b/endpoint-exposer/scripts/istio/build_ingress index 72f849a..34fff61 100644 --- a/endpoint-exposer/scripts/istio/build_ingress +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -12,9 +12,9 @@ else fi # Determine which domain to use based on template -if [[ "$TEMPLATE" == *"private"* ]]; then +if [[ "$TEMPLATE" == *"internal"* ]]; then SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain // .service.attributes.private_domain') - echo "Creating private HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" + echo "Creating internal HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" else SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') echo "Creating public HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" diff --git a/endpoint-exposer/scripts/istio/fetch_httproute b/endpoint-exposer/scripts/istio/fetch_httproute index a5ce47a..a9835b4 100755 --- a/endpoint-exposer/scripts/istio/fetch_httproute +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -12,9 +12,9 @@ VISIBILITY="${VISIBILITY:-public}" echo "Visibility: $VISIBILITY" # Build the HTTPRoute name using the same pattern as the template -if [[ "$VISIBILITY" == "private" ]]; then - HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route-private" - TEMPLATE="$SERVICE_PATH/templates/istio/empty-private.yaml.tpl" +if [[ "$VISIBILITY" == "internal" ]]; then + HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route-internal" + TEMPLATE="$SERVICE_PATH/templates/istio/empty-internal.yaml.tpl" else HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" TEMPLATE="$SERVICE_PATH/templates/istio/empty.yaml.tpl" diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes index a1e60f5..44bb233 100755 --- a/endpoint-exposer/scripts/istio/process_routes +++ b/endpoint-exposer/scripts/istio/process_routes @@ -48,13 +48,13 @@ echo "Routes sorted by specificity (Exact > RegularExpression > PathPrefix)" echo "" echo "=== Separating routes by visibility ===" PUBLIC_ROUTES=$(echo "$ROUTES_JSON" | jq '[.[] | select(.visibility == "public")]') -PRIVATE_ROUTES=$(echo "$ROUTES_JSON" | jq '[.[] | select(.visibility == "private")]') +INTERNAL_ROUTES=$(echo "$ROUTES_JSON" | jq '[.[] | select(.visibility == "internal")]') NUM_PUBLIC=$(echo "$PUBLIC_ROUTES" | jq 'length') -NUM_PRIVATE=$(echo "$PRIVATE_ROUTES" | jq 'length') +NUM_INTERNAL=$(echo "$INTERNAL_ROUTES" | jq 'length') echo "Public routes: $NUM_PUBLIC" -echo "Private routes: $NUM_PRIVATE" +echo "Internal routes: $NUM_INTERNAL" # Function to process routes for a specific visibility process_routes_for_visibility() { @@ -75,8 +75,8 @@ process_routes_for_visibility() { HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" else - HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route-private" + HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" + HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route-internal" fi export HTTPROUTE_FILE @@ -146,8 +146,8 @@ process_routes_for_visibility() { # Process public routes process_routes_for_visibility "public" "$PUBLIC_ROUTES" "$NUM_PUBLIC" -# Process private routes -process_routes_for_visibility "private" "$PRIVATE_ROUTES" "$NUM_PRIVATE" +# Process internal routes +process_routes_for_visibility "internal" "$INTERNAL_ROUTES" "$NUM_INTERNAL" echo "" echo "=== All routes processed successfully ===" diff --git a/endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl b/endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl similarity index 86% rename from endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl rename to endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl index 5bcdc5c..8cb3198 100644 --- a/endpoint-exposer/templates/istio/dns-endpoint-private.yaml.tpl +++ b/endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl @@ -1,13 +1,13 @@ apiVersion: externaldns.k8s.io/v1alpha1 kind: DNSEndpoint metadata: - name: {{ .service.slug }}-{{ .service.id }}-dns-private + name: {{ .service.slug }}-{{ .service.id }}-dns-internal namespace: {{ .k8s_namespace }} labels: nullplatform: "true" service: {{ .service.slug }} service_id: {{ .service.id }} - visibility: private + visibility: internal spec: endpoints: - dnsName: {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} diff --git a/endpoint-exposer/templates/istio/empty-private.yaml.tpl b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl similarity index 85% rename from endpoint-exposer/templates/istio/empty-private.yaml.tpl rename to endpoint-exposer/templates/istio/empty-internal.yaml.tpl index 65698f2..58df21c 100644 --- a/endpoint-exposer/templates/istio/empty-private.yaml.tpl +++ b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl @@ -1,16 +1,16 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: {{ .service.slug }}-{{ .service.id }}-route-private + name: {{ .service.slug }}-{{ .service.id }}-route-internal namespace: {{ .k8s_namespace }} labels: nullplatform: "true" service: {{ .service.slug }} service_id: {{ .service.id }} - visibility: private + visibility: internal spec: parentRefs: - - name: gateway-private + - name: gateway-internal namespace: gateways hostnames: - {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 538d8f7..9247872 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -21,7 +21,7 @@ steps: type: environment - name: PUBLIC_DOMAIN type: environment - - name: PRIVATE_DOMAIN + - name: INTERNAL_DOMAIN type: environment - name: ROUTES_JSON type: environment @@ -34,16 +34,16 @@ steps: - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: "build private httproute" + - name: "build internal httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_ingress" configuration: - TEMPLATE: "$SERVICE_PATH/templates/istio/empty-private.yaml.tpl" - OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + TEMPLATE: "$SERVICE_PATH/templates/istio/empty-internal.yaml.tpl" + OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" output: - - name: HTTPROUTE_FILE_PRIVATE + - name: HTTPROUTE_FILE_INTERNAL type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - name: "process routes" type: script file: "$SERVICE_PATH/scripts/istio/process_routes" @@ -51,9 +51,9 @@ steps: - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: HTTPROUTE_FILE_PRIVATE + - name: HTTPROUTE_FILE_INTERNAL type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - name: "build public dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" @@ -63,15 +63,15 @@ steps: - name: DNS_FILE_PUBLIC type: file file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" - - name: "build private dns endpoint" + - name: "build internal dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" configuration: - VISIBILITY: "private" + VISIBILITY: "internal" output: - - name: DNS_FILE_PRIVATE + - name: DNS_FILE_INTERNAL type: file - file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-private.yaml" + file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/istio/apply" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 41cbd4b..f5c0819 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -34,16 +34,16 @@ steps: - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: "fetch existing private httproute" + - name: "fetch existing internal httproute" type: script file: "$SERVICE_PATH/scripts/istio/fetch_httproute" configuration: - VISIBILITY: "private" - OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + VISIBILITY: "internal" + OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" output: - - name: HTTPROUTE_FILE_PRIVATE + - name: HTTPROUTE_FILE_INTERNAL type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - name: "update public httproute hostname" type: script file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" @@ -51,23 +51,23 @@ steps: HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" DOMAIN: "$PUBLIC_DOMAIN" GATEWAY: "gateway-public" - - name: "update private httproute hostname" + - name: "update internal httproute hostname" type: script file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" configuration: - HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" DOMAIN: "$PRIVATE_DOMAIN" - GATEWAY: "gateway-private" + GATEWAY: "gateway-internal" - name: "clean public httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" configuration: HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: "clean private httproute rules" + - name: "clean internal httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" configuration: - HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - name: "process routes" type: script file: "$SERVICE_PATH/scripts/istio/process_routes" @@ -75,9 +75,9 @@ steps: - name: HTTPROUTE_FILE_PUBLIC type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: HTTPROUTE_FILE_PRIVATE + - name: HTTPROUTE_FILE_INTERNAL type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - name: "update public dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" @@ -87,15 +87,15 @@ steps: - name: DNS_FILE_PUBLIC type: file file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" - - name: "update private dns endpoint" + - name: "update internal dns endpoint" type: script file: "$SERVICE_PATH/scripts/dns/build_dns" configuration: - VISIBILITY: "private" + VISIBILITY: "internal" output: - - name: DNS_FILE_PRIVATE + - name: DNS_FILE_INTERNAL type: file - file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-private.yaml" + file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/istio/apply" From 470b16d045a65de109c4f230852f6d4dd91a9558 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 16:28:02 -0300 Subject: [PATCH 005/101] feat: http routes to handle private and public traffic --- endpoint-exposer/scripts/istio/build_ingress | 23 ++++++++++--------- .../scripts/istio/clean_httproute_rules | 5 ++++ .../scripts/istio/fetch_httproute | 2 ++ .../scripts/istio/update_httproute_hostname | 5 ++++ endpoint-exposer/workflows/istio/create.yaml | 4 +--- endpoint-exposer/workflows/istio/update.yaml | 12 ++++------ 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_ingress b/endpoint-exposer/scripts/istio/build_ingress index 34fff61..ad54d62 100644 --- a/endpoint-exposer/scripts/istio/build_ingress +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -2,22 +2,19 @@ set -euo pipefail -# Determine output file - use OUTPUT_FILE if set, otherwise default to public -if [[ -n "${OUTPUT_FILE:-}" ]]; then - HTTPROUTE_FILE="$OUTPUT_FILE" - echo "Using specified output file: $HTTPROUTE_FILE" -else - HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - echo "Using default output file: $HTTPROUTE_FILE" -fi - -# Determine which domain to use based on template +# Determine which domain to use and output file based on template if [[ "$TEMPLATE" == *"internal"* ]]; then + # Internal HTTPRoute SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain // .service.attributes.private_domain') + HTTPROUTE_FILE="${OUTPUT_FILE:-$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml}" echo "Creating internal HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" + echo "Output file: $HTTPROUTE_FILE" else + # Public HTTPRoute SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') + HTTPROUTE_FILE="${OUTPUT_FILE:-$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml}" echo "Creating public HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" + echo "Output file: $HTTPROUTE_FILE" fi CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" @@ -30,4 +27,8 @@ gomplate -c .="$CONTEXT_PATH" \ --file "$TEMPLATE" \ --out "$HTTPROUTE_FILE" -rm "$CONTEXT_PATH" \ No newline at end of file +rm "$CONTEXT_PATH" + +# Export the file path for the workflow +export HTTPROUTE_FILE +echo "HTTPRoute created at: $HTTPROUTE_FILE" \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/clean_httproute_rules b/endpoint-exposer/scripts/istio/clean_httproute_rules index 85179f6..55c7dda 100755 --- a/endpoint-exposer/scripts/istio/clean_httproute_rules +++ b/endpoint-exposer/scripts/istio/clean_httproute_rules @@ -2,6 +2,11 @@ set -euo pipefail +# If HTTPROUTE_FILE_VAR is provided, use it to get the file path from that variable +if [[ -n "${HTTPROUTE_FILE_VAR:-}" ]]; then + HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" +fi + echo "=== Cleaning HTTPRoute rules ===" echo "HTTPROUTE_FILE: $HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/fetch_httproute b/endpoint-exposer/scripts/istio/fetch_httproute index a9835b4..a30d3c7 100755 --- a/endpoint-exposer/scripts/istio/fetch_httproute +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -56,4 +56,6 @@ else echo "New HTTPRoute created from template" fi +# Export the file path for the workflow +export HTTPROUTE_FILE echo "HTTPRoute file ready at: $HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/update_httproute_hostname b/endpoint-exposer/scripts/istio/update_httproute_hostname index 7b22353..7f7b044 100755 --- a/endpoint-exposer/scripts/istio/update_httproute_hostname +++ b/endpoint-exposer/scripts/istio/update_httproute_hostname @@ -2,6 +2,11 @@ set -euo pipefail +# If HTTPROUTE_FILE_VAR is provided, use it to get the file path from that variable +if [[ -n "${HTTPROUTE_FILE_VAR:-}" ]]; then + HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" +fi + # Use DOMAIN and GATEWAY if provided via configuration, otherwise use PUBLIC_DOMAIN and gateway-public DOMAIN="${DOMAIN:-${PUBLIC_DOMAIN}}" GATEWAY="${GATEWAY:-gateway-public}" diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 9247872..3290a23 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -39,11 +39,9 @@ steps: file: "$SERVICE_PATH/scripts/istio/build_ingress" configuration: TEMPLATE: "$SERVICE_PATH/templates/istio/empty-internal.yaml.tpl" - OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" output: - name: HTTPROUTE_FILE_INTERNAL - type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" + type: environment - name: "process routes" type: script file: "$SERVICE_PATH/scripts/istio/process_routes" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index f5c0819..6bbd32d 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -39,35 +39,33 @@ steps: file: "$SERVICE_PATH/scripts/istio/fetch_httproute" configuration: VISIBILITY: "internal" - OUTPUT_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" output: - name: HTTPROUTE_FILE_INTERNAL - type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" + type: environment - name: "update public httproute hostname" type: script file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" configuration: - HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + HTTPROUTE_FILE_VAR: "HTTPROUTE_FILE_PUBLIC" DOMAIN: "$PUBLIC_DOMAIN" GATEWAY: "gateway-public" - name: "update internal httproute hostname" type: script file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" configuration: - HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" + HTTPROUTE_FILE_VAR: "HTTPROUTE_FILE_INTERNAL" DOMAIN: "$PRIVATE_DOMAIN" GATEWAY: "gateway-internal" - name: "clean public httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" configuration: - HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + HTTPROUTE_FILE_VAR: "HTTPROUTE_FILE_PUBLIC" - name: "clean internal httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" configuration: - HTTPROUTE_FILE: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" + HTTPROUTE_FILE_VAR: "HTTPROUTE_FILE_INTERNAL" - name: "process routes" type: script file: "$SERVICE_PATH/scripts/istio/process_routes" From 9518ebd9b280299a1263c972d1f603a51d095f74 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 16:40:22 -0300 Subject: [PATCH 006/101] chore: link to public gateway --- endpoint-exposer/templates/istio/empty-internal.yaml.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/templates/istio/empty-internal.yaml.tpl b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl index 58df21c..4878ed3 100644 --- a/endpoint-exposer/templates/istio/empty-internal.yaml.tpl +++ b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl @@ -10,7 +10,7 @@ metadata: visibility: internal spec: parentRefs: - - name: gateway-internal + - name: gateway-public namespace: gateways hostnames: - {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} From 467abb806b45ab99a17b01635db3895ff0892861 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 16:48:24 -0300 Subject: [PATCH 007/101] chore: replace gateway --- endpoint-exposer/examples/create-link.json | 65 ----------------- endpoint-exposer/examples/create.json | 64 ----------------- endpoint-exposer/examples/delete-link.json | 71 ------------------- endpoint-exposer/examples/delete.json | 50 ------------- endpoint-exposer/examples/read.json | 50 ------------- endpoint-exposer/examples/refresh.json | 61 ---------------- .../scripts/dns/build_dns_endpoint | 2 +- .../templates/istio/empty-internal.yaml.tpl | 2 +- endpoint-exposer/workflows/istio/update.yaml | 2 +- 9 files changed, 3 insertions(+), 364 deletions(-) delete mode 100644 endpoint-exposer/examples/create-link.json delete mode 100644 endpoint-exposer/examples/create.json delete mode 100644 endpoint-exposer/examples/delete-link.json delete mode 100644 endpoint-exposer/examples/delete.json delete mode 100644 endpoint-exposer/examples/read.json delete mode 100644 endpoint-exposer/examples/refresh.json diff --git a/endpoint-exposer/examples/create-link.json b/endpoint-exposer/examples/create-link.json deleted file mode 100644 index 2f86a2e..0000000 --- a/endpoint-exposer/examples/create-link.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "id": "ba21e8e6-dcc1-4b9f-87a4-3610c61dfda4", - "source": "service", - "event": "service:action:create", - "created_at": "2025-06-18T13:01:20.204Z", - "notification": { - "action": "service:action:create", - "id": "5b426145-cc25-429e-88d2-83407a6f08b3", - "name": "create-the-link-name-from-the-user", - "slug": "create-the-link-name-from-the-user", - "status": "pending", - "created_at": "2025-06-18T13:01:20.207Z", - "updated_at": "2025-06-18T13:01:20.207Z", - "parameters": { - "path": "/users" - }, - "results": {}, - "type": "create", - "specification": { - "id": "0dc917a7-180c-4155-ad84-767dc5be61a4", - "slug": "create-publish" - }, - "service": { - "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", - "slug": "the-service-name-from-the-user", - "attributes": { - "domain": "some-domain.com" - }, - "specification": { - "id": "e820250b-62e3-4e20-8647-dd41b20d1bb9", - "slug": "publish" - }, - "dimensions": { - "environment": "development", - "country": "argentina" - } - }, - "link": { - "id": "64cc013c-9545-495e-a2ac-40df06af84d6", - "slug": "the-link-name-from-the-user", - "attributes": { - "path": "/users", - "scope": "dev-us" - }, - "specification": { - "id": "39d2d131-50b5-41b9-a739-1c3edde913ec", - "slug": "publish" - }, - "dimensions": {} - }, - "tags": { - "organization_id": "1255165411", - "organization": "kwik-e-mart", - "account_id": "95118862", - "account": "kwik-e-mart-main", - "namespace_id": "1068437903", - "namespace": "fede-m", - "application_id": "857154851", - "application": "scope-exposer", - "scope_id": "704207903", - "scope": "public-argentina" - }, - "entity_nrn": "organization=1255165411:account=95118862:namespace=1249051863:application=454567105:scope=704207903" - } -} \ No newline at end of file diff --git a/endpoint-exposer/examples/create.json b/endpoint-exposer/examples/create.json deleted file mode 100644 index f6912ce..0000000 --- a/endpoint-exposer/examples/create.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "action": "service:action:create", - "id": "d7f7698d-ce17-4dd5-9cb6-02dc39e7a726", - "name": "create-test-javi", - "slug": "create-test-javi", - "status": "pending", - "created_by": "1621446846", - "created_at": "2025-12-30T16:34:40.773Z", - "updated_at": "2025-12-30T16:34:40.773Z", - "parameters": { - "routes": [ - { - "method": "GET", - "path": "/health", - "scope": "DevelopmentScope", - "visibility": "public" - } - ], - "public_domain": "javi.edenred.nullimplementation.com", - "private_domain": "javi.edenred.nullimplementation.com" - }, - "results": { - "routes": [ - { - "method": "GET", - "path": "/health", - "scope": "DevelopmentScope", - "visibility": "public" - } - ], - "public_domain": "javi.edenred.nullimplementation.com", - "private_domain": "javi.edenred.nullimplementation.com" - }, - "type": "create", - "specification": { - "id": "b9c6e9b5-c292-4ad1-9af8-650cac4ea587", - "slug": "create-service-exposer-v2" - }, - "service": { - "id": "babaa91d-89fb-4ee3-ba23-529620d1834f", - "slug": "test-javi", - "attributes": {}, - "type": "dependency", - "specification": { - "id": "70d51cc6-591d-4282-927c-4757b092604b", - "slug": "service-exposer-v2" - }, - "dimensions": { - "environment": "development" - } - }, - "link": null, - "tags": { - "namespace_id": "1340017944", - "namespace": "playground", - "organization_id": "1858797910", - "organization": "edenred", - "account_id": "758973013", - "account": "playground", - "application_id": "179976948", - "application": "floppy-bird-api" - }, - "entity_nrn": "organization=1858797910:account=758973013:namespace=1340017944:application=179976948" -} \ No newline at end of file diff --git a/endpoint-exposer/examples/delete-link.json b/endpoint-exposer/examples/delete-link.json deleted file mode 100644 index b17be37..0000000 --- a/endpoint-exposer/examples/delete-link.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "id": "ba21e8e6-dcc1-4b9f-87a4-3610c61dfda4", - "source": "service", - "event": "service:action:delete", - "created_at": "2025-06-18T13:01:20.204Z", - "notification": { - "action": "service:action:delete", - "id": "5b426145-cc25-429e-88d2-83407a6f08b3", - "name": "delete-the-link-name-from-the-user", - "slug": "delete-the-link-name-from-the-user", - "status": "pending", - "created_at": "2025-06-18T13:01:20.207Z", - "updated_at": "2025-06-18T13:01:20.207Z", - "parameters": { - "path": "/users" - }, - "results": {}, - "type": "delete", - "specification": { - "id": "0dc917a7-180c-4155-ad84-767dc5be61a4", - "slug": "delete-publish" - }, - "service": { - "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", - "slug": "the-service-name-from-the-user", - "attributes": { - "domain": "some-domain.com", - "rules": [ - { - "path": "/users", - "scope": "public-argentina" - } - ] - }, - "specification": { - "id": "e820250b-62e3-4e20-8647-dd41b20d1bb9", - "slug": "publish" - }, - "dimensions": { - "environment": "development", - "country": "argentina" - } - }, - "link": { - "id": "64cc013c-9545-495e-a2ac-40df06af84d6", - "slug": "the-link-name-from-the-user", - "attributes": { - "path": "/users", - "scope": "dev-us" - }, - "specification": { - "id": "39d2d131-50b5-41b9-a739-1c3edde913ec", - "slug": "publish" - }, - "dimensions": {} - }, - "tags": { - "organization_id": "1255165411", - "organization": "kwik-e-mart", - "account_id": "95118862", - "account": "kwik-e-mart-main", - "namespace_id": "1068437903", - "namespace": "fede-m", - "application_id": "857154851", - "application": "scope-exposer", - "scope_id": "704207903", - "scope": "public-argentina" - }, - "entity_nrn": "organization=1255165411:account=95118862:namespace=1249051863:application=454567105:scope=704207903" - } -} \ No newline at end of file diff --git a/endpoint-exposer/examples/delete.json b/endpoint-exposer/examples/delete.json deleted file mode 100644 index ac6a64d..0000000 --- a/endpoint-exposer/examples/delete.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": "c266d1aa-23d7-4a23-8954-346824186c9a", - "source": "service", - "event": "service:action:delete", - "created_at": "2025-06-17T15:24:05.861Z", - "notification": { - "action": "service:action:delete", - "id": "b9a9971b-acce-421b-b7b7-4e940e73fac8", - "name": "create-the-service-name-from-the-user", - "slug": "create-the-service-name-from-the-user", - "status": "pending", - "created_at": "2025-06-17T15:24:05.861Z", - "updated_at": "2025-06-17T15:24:05.861Z", - "parameters": { - }, - "results": {}, - "type": "delete", - "specification": { - "id": "03bcdc47-212c-4b64-a2cc-ed0365e4228c", - "slug": "create-service-exposer" - }, - "service": { - "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", - "slug": "the-service-name-from-the-user", - "attributes": { - "rules": [], - "domain": "domain.com" - }, - "specification": { - "id": "f66f5e37-0303-48aa-bc9d-aeda1f909a10", - "slug": "service-exposer" - }, - "dimensions": { - "environment": "development" - } - }, - "link": null, - "tags": { - "organization_id": "1255165411", - "organization": "kwik-e-mart", - "account_id": "95118862", - "account": "kwik-e-mart-main", - "namespace_id": "1068437903", - "namespace": "fede-m", - "application_id": "857154851", - "application": "scope-exposer" - }, - "entity_nrn": "organization=1255165411:account=95118862:namespace=1068437903:application=857154851" - } -} \ No newline at end of file diff --git a/endpoint-exposer/examples/read.json b/endpoint-exposer/examples/read.json deleted file mode 100644 index f165523..0000000 --- a/endpoint-exposer/examples/read.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "id": "c266d1aa-23d7-4a23-8954-346824186c9a", - "source": "service", - "event": "service:action:write", - "created_at": "2025-06-17T15:24:05.861Z", - "notification": { - "action": "service:action:write", - "id": "b9a9971b-acce-421b-b7b7-4e940e73fac8", - "name": "Read", - "slug": "read", - "status": "pending", - "created_at": "2025-06-17T15:24:05.861Z", - "updated_at": "2025-06-17T15:24:05.861Z", - "parameters": { - }, - "results": {}, - "type": "custom", - "specification": { - "id": "03bcdc47-212c-4b64-a2cc-ed0365e4228c", - "slug": "read" - }, - "service": { - "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", - "slug": "the-service-name-from-the-user", - "attributes": { - "rules": [], - "domain": "domain.com" - }, - "specification": { - "id": "f66f5e37-0303-48aa-bc9d-aeda1f909a10", - "slug": "service-exposer" - }, - "dimensions": { - "environment": "development" - } - }, - "link": null, - "tags": { - "organization_id": "1255165411", - "organization": "kwik-e-mart", - "account_id": "95118862", - "account": "kwik-e-mart-main", - "namespace_id": "1068437903", - "namespace": "fede-m", - "application_id": "857154851", - "application": "scope-exposer" - }, - "entity_nrn": "organization=1255165411:account=95118862:namespace=1068437903:application=857154851" - } -} \ No newline at end of file diff --git a/endpoint-exposer/examples/refresh.json b/endpoint-exposer/examples/refresh.json deleted file mode 100644 index ae32f56..0000000 --- a/endpoint-exposer/examples/refresh.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "id": "ba21e8e6-dcc1-4b9f-87a4-3610c61dfda4", - "source": "service", - "event": "service:action:update", - "created_at": "2025-06-18T13:01:20.204Z", - "notification": { - "action": "service:action:update", - "id": "5b426145-cc25-429e-88d2-83407a6f08b3", - "name": "Refresh", - "slug": "refresh", - "status": "pending", - "created_at": "2025-06-18T13:01:20.207Z", - "updated_at": "2025-06-18T13:01:20.207Z", - "parameters": {}, - "results": {}, - "type": "custom", - "specification": { - "id": "0dc917a7-180c-4155-ad84-767dc5be61a4", - "slug": "delete-publish" - }, - "service": { - "id": "4f3fc1da-10bf-47e7-8dd8-b9febc3ddea1", - "slug": "the-service-name-from-the-user", - "attributes": {}, - "specification": { - "id": "e820250b-62e3-4e20-8647-dd41b20d1bb9", - "slug": "publish" - }, - "dimensions": { - "environment": "development", - "country": "argentina" - } - }, - "link": { - "id": "64cc013c-9545-495e-a2ac-40df06af84d6", - "slug": "the-link-name-from-the-user", - "attributes": { - "path": "/users", - "scope": "dev-us" - }, - "specification": { - "id": "39d2d131-50b5-41b9-a739-1c3edde913ec", - "slug": "publish" - }, - "dimensions": {} - }, - "tags": { - "organization_id": "1255165411", - "organization": "kwik-e-mart", - "account_id": "95118862", - "account": "kwik-e-mart-main", - "namespace_id": "1068437903", - "namespace": "fede-m", - "application_id": "857154851", - "application": "scope-exposer", - "scope_id": "704207903", - "scope": "public-argentina" - }, - "entity_nrn": "organization=1255165411:account=95118862:namespace=1249051863:application=454567105:scope=704207903" - } -} \ No newline at end of file diff --git a/endpoint-exposer/scripts/dns/build_dns_endpoint b/endpoint-exposer/scripts/dns/build_dns_endpoint index a5bc6ef..7ec49a9 100755 --- a/endpoint-exposer/scripts/dns/build_dns_endpoint +++ b/endpoint-exposer/scripts/dns/build_dns_endpoint @@ -11,7 +11,7 @@ echo "DEBUG: VISIBILITY=$VISIBILITY" # Select domain and gateway based on visibility if [[ "$VISIBILITY" == "internal" ]]; then SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain') - GATEWAY_NAME=${INTERNAL_GATEWAY_NAME:-"gateway-internal"} + GATEWAY_NAME=${INTERNAL_GATEWAY_NAME:-"gateway-public"} DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint-internal.yaml.tpl" DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" echo "DEBUG: Building INTERNAL DNS endpoint" diff --git a/endpoint-exposer/templates/istio/empty-internal.yaml.tpl b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl index 4878ed3..a03c394 100644 --- a/endpoint-exposer/templates/istio/empty-internal.yaml.tpl +++ b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl @@ -20,6 +20,6 @@ spec: type: PathPrefix value: / backendRefs: - - name: response-404 + - name: response-404F port: 80 weight: 0 diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 6bbd32d..cd7bc77 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -55,7 +55,7 @@ steps: configuration: HTTPROUTE_FILE_VAR: "HTTPROUTE_FILE_INTERNAL" DOMAIN: "$PRIVATE_DOMAIN" - GATEWAY: "gateway-internal" + GATEWAY: "gateway-public" - name: "clean public httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" From 3dfdf917ae3149fa57da9f8aab4464aa9aa9fd4a Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 18:50:49 -0300 Subject: [PATCH 008/101] chore: link to private gateway --- endpoint-exposer/scripts/dns/build_dns_endpoint | 2 +- endpoint-exposer/templates/istio/empty-internal.yaml.tpl | 4 ++-- endpoint-exposer/workflows/istio/update.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/endpoint-exposer/scripts/dns/build_dns_endpoint b/endpoint-exposer/scripts/dns/build_dns_endpoint index 7ec49a9..e55e181 100755 --- a/endpoint-exposer/scripts/dns/build_dns_endpoint +++ b/endpoint-exposer/scripts/dns/build_dns_endpoint @@ -11,7 +11,7 @@ echo "DEBUG: VISIBILITY=$VISIBILITY" # Select domain and gateway based on visibility if [[ "$VISIBILITY" == "internal" ]]; then SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain') - GATEWAY_NAME=${INTERNAL_GATEWAY_NAME:-"gateway-public"} + GATEWAY_NAME=${INTERNAL_GATEWAY_NAME:-"gateway-private"} DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint-internal.yaml.tpl" DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" echo "DEBUG: Building INTERNAL DNS endpoint" diff --git a/endpoint-exposer/templates/istio/empty-internal.yaml.tpl b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl index a03c394..8beaccd 100644 --- a/endpoint-exposer/templates/istio/empty-internal.yaml.tpl +++ b/endpoint-exposer/templates/istio/empty-internal.yaml.tpl @@ -10,7 +10,7 @@ metadata: visibility: internal spec: parentRefs: - - name: gateway-public + - name: gateway-private namespace: gateways hostnames: - {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} @@ -20,6 +20,6 @@ spec: type: PathPrefix value: / backendRefs: - - name: response-404F + - name: response-404 port: 80 weight: 0 diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index cd7bc77..d8b3d04 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -55,7 +55,7 @@ steps: configuration: HTTPROUTE_FILE_VAR: "HTTPROUTE_FILE_INTERNAL" DOMAIN: "$PRIVATE_DOMAIN" - GATEWAY: "gateway-public" + GATEWAY: "gateway-private" - name: "clean public httproute rules" type: script file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" From 9cc69db558932f3bc6c6ab3572f3d96c399f2a60 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 18:51:59 -0300 Subject: [PATCH 009/101] chore: remove alb scripts --- endpoint-exposer/scripts/alb/apply | 25 -- endpoint-exposer/scripts/alb/build_context | 36 --- endpoint-exposer/scripts/alb/build_ingress | 22 -- .../scripts/alb/build_ingress_with_rule | 199 ------------- .../scripts/alb/build_ingress_without_rule | 47 --- endpoint-exposer/scripts/alb/build_rule | 22 -- .../scripts/alb/fetch_provider_data | 22 -- endpoint-exposer/scripts/alb/read_ingress | 5 - endpoint-exposer/scripts/alb/update_service | 21 -- .../scripts/alb/validate_unique_rule | 12 - ...ld_ingress_with_rule_virtualservice_backup | 278 ------------------ 11 files changed, 689 deletions(-) delete mode 100644 endpoint-exposer/scripts/alb/apply delete mode 100644 endpoint-exposer/scripts/alb/build_context delete mode 100644 endpoint-exposer/scripts/alb/build_ingress delete mode 100644 endpoint-exposer/scripts/alb/build_ingress_with_rule delete mode 100644 endpoint-exposer/scripts/alb/build_ingress_without_rule delete mode 100644 endpoint-exposer/scripts/alb/build_rule delete mode 100755 endpoint-exposer/scripts/alb/fetch_provider_data delete mode 100644 endpoint-exposer/scripts/alb/read_ingress delete mode 100644 endpoint-exposer/scripts/alb/update_service delete mode 100644 endpoint-exposer/scripts/alb/validate_unique_rule delete mode 100644 endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup diff --git a/endpoint-exposer/scripts/alb/apply b/endpoint-exposer/scripts/alb/apply deleted file mode 100644 index 2692c3c..0000000 --- a/endpoint-exposer/scripts/alb/apply +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "TEMPLATE DIR: $OUTPUT_DIR, ACTION: $ACTION, DRY_RUN: $DRY_RUN" - -# Find all .yaml files that were not yet applied / deleted -find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" \) -prune -o -type f -name "*.yaml" -print | while read -r TEMPLATE_FILE; do - echo "kubectl $ACTION $TEMPLATE_FILE" - - if [[ "$DRY_RUN" == "false" ]]; then - kubectl "$ACTION" -f "$TEMPLATE_FILE" - fi - - BASE_DIR="$(dirname "$TEMPLATE_FILE")" - FILENAME="$(basename "$TEMPLATE_FILE")" - DEST_DIR="${BASE_DIR}/$ACTION" - - mkdir -p "$DEST_DIR" - mv "$TEMPLATE_FILE" "$DEST_DIR/$FILENAME" -done - -if [[ "$DRY_RUN" == "true" ]]; then - exit 1 -fi \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_context b/endpoint-exposer/scripts/alb/build_context deleted file mode 100644 index 761fed4..0000000 --- a/endpoint-exposer/scripts/alb/build_context +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SERVICE_ID=$(echo "$CONTEXT" | jq -r .service.id) -SERVICE_NAME=$(echo "$CONTEXT" | jq -r .service.slug) - -ACTION_ID=$(echo "$CONTEXT" | jq -r .id) -ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) - -LINK_ID=$(echo "$CONTEXT" | jq -r .link.id) -LINK_NAME=$(echo "$CONTEXT" | jq -r .link.slug) - -CONTEXT=$(echo "$CONTEXT" | jq \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg alb_name "$ALB_NAME" \ - '. + {k8s_namespace: $k8s_namespace, alb_name: $alb_name}') - -SCOPE_ID=$(echo "$CONTEXT" | jq -r .tags.scope_id) -SCOPE_SLUG=$(echo "$CONTEXT" | jq -r .tags.scope) - -RULE_PATH=$(echo "$CONTEXT" | jq -r '.link.attributes.path // .parameters.path') - -export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_NAME-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" - -mkdir -p "$OUTPUT_DIR" - -export SERVICE_ID -export SERVICE_NAME -export ACTION_ID -export ACTION_NAME -export LINK_ID -export LINK_NAME -export SCOPE_ID -export SCOPE_SLUG -export RULE_PATH \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_ingress b/endpoint-exposer/scripts/alb/build_ingress deleted file mode 100644 index 610c439..0000000 --- a/endpoint-exposer/scripts/alb/build_ingress +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r .parameters.domain) - -echo "Creating ingress for service $SERVICE_NAME with domain $SERVICE_DOMAIN" - -echo "Creating public ingress..." - -INGRESS_FILE="$OUTPUT_DIR/ingress-$SERVICE_ID-public.yaml" -CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" - -echo "$CONTEXT" > "$CONTEXT_PATH" - -echo "Building Template: $TEMPLATE to $INGRESS_FILE" - -gomplate -c .="$CONTEXT_PATH" \ - --file "$TEMPLATE" \ - --out "$INGRESS_FILE" - -rm "$CONTEXT_PATH" \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_ingress_with_rule b/endpoint-exposer/scripts/alb/build_ingress_with_rule deleted file mode 100644 index f3f4e5f..0000000 --- a/endpoint-exposer/scripts/alb/build_ingress_with_rule +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -is_ingress_empty() { - local yaml_content="$1" - - local num_rules - local num_paths - local path - local service_name - local service_port - - num_rules=$(yq '.spec.rules | length' <<< "$yaml_content") - num_paths=$(yq '.spec.rules[0].http.paths | length' <<< "$yaml_content") - path=$(yq '.spec.rules[0].http.paths[0].path' <<< "$yaml_content") - service_name=$(yq '.spec.rules[0].http.paths[0].backend.service.name' <<< "$yaml_content") - service_port=$(yq '.spec.rules[0].http.paths[0].backend.service.port.name' <<< "$yaml_content") - - if [[ "$num_rules" -eq 1 && \ - "$num_paths" -eq 1 && \ - "$path" == "/" && \ - "$service_name" == "response-404" && \ - "$service_port" == "use-annotation" ]]; then - echo "true" - else - echo "false" - fi -} - -create_path_rule() { - local rule_path="$1" - local service_json="$2" - - jq -n \ - --arg path "$rule_path" \ - --argjson service "$service_json" \ - '{ - path: $path, - pathType: "Prefix", - backend: { - service: $service - } - }' -} - -add_annotation_if_needed() { - local ingress_yaml="$1" - local annotation="$2" - local scope_id="$3" - - if [[ "$annotation" != "null" && -n "$annotation" ]]; then - local annotation_key="alb.ingress.kubernetes.io/actions.bg-deployment-$scope_id" - - # Escape the value so it’s properly quoted in YAML - local escaped_annotation - escaped_annotation=$(printf '%s\n' "$annotation" | jq -Rs .) # wrap & escape as JSON string - - echo "$ingress_yaml" | yq eval ".metadata.annotations.\"$annotation_key\" = $escaped_annotation" - else - echo "$ingress_yaml" - fi -} - -update_ingress_rule() { - local ingress_yaml="$1" - local rule_path="$2" - local service_json="$3" - local annotation="$4" - local scope_id="$5" - - local updated_ingress - - # Replace the current rule with the new rule data - updated_ingress=$(echo "$ingress_yaml" | yq eval '.spec.rules[0].http.paths[0].path = "'$rule_path'"') - updated_ingress=$(echo "$updated_ingress" | yq eval '.spec.rules[0].http.paths[0].pathType = "Prefix"') - updated_ingress=$(echo "$updated_ingress" | yq eval '.spec.rules[0].http.paths[0].backend.service = '"$service_json") - - # Add annotation if needed - updated_ingress=$(add_annotation_if_needed "$updated_ingress" "$annotation" "$scope_id") - - echo "$updated_ingress" -} - -find_path_index() { - local ingress_yaml="$1" - local target_path="$2" - - local num_paths - local i - local current_path - - num_paths=$(yq '.spec.rules[0].http.paths | length' <<< "$ingress_yaml") - - for ((i=0; i "$INGRESS_FILE" -else - PATH_INDEX=$(find_path_index "$INGRESS" "$RULE_PATH") - - # if there is a rule for the path we replace it - if [[ "$PATH_INDEX" != "-1" ]]; then - UPDATED_INGRESS=$(replace_existing_rule "$INGRESS" "$RULE_PATH" "$SERVICE" "$ANNOTATION" "$SCOPE_ID" "$PATH_INDEX") - echo "$UPDATED_INGRESS" | yq "." > "$INGRESS_FILE" - else - # if there is no rule for the path we add a new one - UPDATED_INGRESS=$(add_new_rule "$INGRESS" "$RULE_PATH" "$SERVICE" "$ANNOTATION" "$SCOPE_ID") - echo "$UPDATED_INGRESS" | yq "." > "$INGRESS_FILE" - fi -fi - -echo "Ingress configuration saved to: $INGRESS_FILE" \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/build_ingress_without_rule b/endpoint-exposer/scripts/alb/build_ingress_without_rule deleted file mode 100644 index 282fff7..0000000 --- a/endpoint-exposer/scripts/alb/build_ingress_without_rule +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -find_path_index() { - local ingress_yaml="$1" - local target_path="$2" - - local num_paths - local i - local current_path - - num_paths=$(yq '.spec.rules[0].http.paths | length' <<< "$ingress_yaml") - - for ((i=0; i "$INGRESS_FILE" - - diff --git a/endpoint-exposer/scripts/alb/build_rule b/endpoint-exposer/scripts/alb/build_rule deleted file mode 100644 index 1de728a..0000000 --- a/endpoint-exposer/scripts/alb/build_rule +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -INGRESS_NAME=$(kubectl get ingress -n "$K8S_NAMESPACE" -l "scope_id=$SCOPE_ID" -o jsonpath="{.items[0].metadata.name}" 2>/dev/null || echo "") - -# Check if ingress exists -if [ -z "$INGRESS_NAME" ]; then - echo "There is no ingress for scope $SCOPE_ID. Publishing the rule with an empty backend" - - SCOPE_RULE='{"blue_green_annotation": null, "service": {"name": "response-404", "port": { "name": "use-annotation"} }}' -else - echo "Found ingress for scope $SCOPE_ID. Publishing the backend service configuration" - INGRESS=$(kubectl get ingress -n "$K8S_NAMESPACE" "$INGRESS_NAME" -o json) - - SCOPE_RULE=$(echo "$INGRESS" | jq '{ - blue_green_annotation: .metadata.annotations["alb.ingress.kubernetes.io/actions.bg-deployment"], - service: .spec.rules[0].http.paths[0].backend.service - }') -fi - -export SCOPE_RULE \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/fetch_provider_data b/endpoint-exposer/scripts/alb/fetch_provider_data deleted file mode 100755 index 51603cf..0000000 --- a/endpoint-exposer/scripts/alb/fetch_provider_data +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -NRN=$(echo "$CONTEXT" | jq -r .entity_nrn) - -DIMENSIONS=$(echo "$CONTEXT" | jq .service.dimensions) - -DIMENSION_FILTER=$(echo "$DIMENSIONS" | jq -r 'to_entries | map("\(.key):\(.value)") | join(",")') - -if [ -z "$DIMENSION_FILTER" ] || [ "$DIMENSION_FILTER" = "" ]; then - PROVIDER_DATA=$(np provider list --categories container-orchestration --nrn "$NRN" --format json | jq -r ".results[0]") -else - PROVIDER_DATA=$(np provider list --categories container-orchestration --nrn "$NRN" --dimensions "$DIMENSION_FILTER" --format json | jq -r ".results[0]") -fi - -K8S_NAMESPACE=$(echo "$PROVIDER_DATA" | jq -r .attributes.cluster.namespace) - -ALB_NAME=$(echo "$PROVIDER_DATA" | jq -r --arg default "k8s-nullplatform-internet-facing" ' - .attributes.balancer.public_name // $default -') - -export K8S_NAMESPACE -export ALB_NAME \ No newline at end of file diff --git a/endpoint-exposer/scripts/alb/read_ingress b/endpoint-exposer/scripts/alb/read_ingress deleted file mode 100644 index bde7c9f..0000000 --- a/endpoint-exposer/scripts/alb/read_ingress +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -INGRESS_NAME=$(kubectl get ingress -n "$K8S_NAMESPACE" -l "service_id=$SERVICE_ID" -o jsonpath="{.items[0].metadata.name}") - -kubectl get ingress -n "$K8S_NAMESPACE" "$INGRESS_NAME" -o yaml diff --git a/endpoint-exposer/scripts/alb/update_service b/endpoint-exposer/scripts/alb/update_service deleted file mode 100644 index 5f743d3..0000000 --- a/endpoint-exposer/scripts/alb/update_service +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -if [[ $ACTION == "add" ]]; then - UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" --arg scope "$SCOPE_SLUG" ' - .service.attributes |= (.rules //= [] | .rules += [{path: $path, scope: $scope}]) | - { - attributes: .service.attributes, - entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) - } - ') -elif [[ $ACTION == "remove" ]]; then - UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' - .service.attributes |= (.rules //= [] | .rules |= map(select(.path != $path))) | - { - attributes: .service.attributes, - entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) - } - ') -fi - -np service patch --id "$SERVICE_ID" --body "$UPDATED_ATTRIBUTES" diff --git a/endpoint-exposer/scripts/alb/validate_unique_rule b/endpoint-exposer/scripts/alb/validate_unique_rule deleted file mode 100644 index 49a56b1..0000000 --- a/endpoint-exposer/scripts/alb/validate_unique_rule +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -EXISTS=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' - (.service.attributes.rules // []) - | map(select(.path == $path)) - | length -') - -if [[ "$EXISTS" -gt 0 ]]; then - echo "Error: A rule with path '$RULE_PATH' already exists." - exit 1 -fi \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup b/endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup deleted file mode 100644 index 3367222..0000000 --- a/endpoint-exposer/scripts/istio/build_ingress_with_rule_virtualservice_backup +++ /dev/null @@ -1,278 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -is_virtualservice_empty() { - local yaml_content="$1" - - local num_http_routes - local has_fault - local service_host - - num_http_routes=$(yq '.spec.http | length' <<< "$yaml_content") - has_fault=$(yq '.spec.http[0].fault.abort' <<< "$yaml_content") - service_host=$(yq '.spec.http[0].route[0].destination.host' <<< "$yaml_content") - - # A VirtualService is "empty" if it only has one route with a 404 fault injection to response-404 - if [[ "$num_http_routes" -eq 1 && \ - "$has_fault" != "null" && \ - "$service_host" =~ response-404 ]]; then - echo "true" - else - echo "false" - fi -} - -create_http_route() { - local rule_path="$1" - local service_json="$2" - local blue_green_config="$3" - - local service_name - local service_port - - service_name=$(echo "$service_json" | jq -r '.name') - service_port=$(echo "$service_json" | jq -r '.port.number // .port.name // 80') - - # Check if there's blue/green configuration - if [[ "$blue_green_config" != "null" && -n "$blue_green_config" ]]; then - # Parse blue/green destinations and weights from the annotation - local blue_weight green_weight blue_service green_service - - blue_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].weight // 100') - green_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].weight // 0') - blue_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].serviceName') - green_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].serviceName') - - # Create route with weighted destinations - jq -n \ - --arg path "$rule_path" \ - --arg blue_service "$blue_service" \ - --arg green_service "$green_service" \ - --arg service_port "$service_port" \ - --arg namespace "$K8S_NAMESPACE" \ - --argjson blue_weight "$blue_weight" \ - --argjson green_weight "$green_weight" \ - '{ - match: [ - { - uri: { - prefix: $path - } - } - ], - route: [ - { - destination: { - host: ($blue_service + "." + $namespace + ".svc.cluster.local"), - port: { - number: ($service_port | tonumber) - } - }, - weight: $blue_weight - }, - { - destination: { - host: ($green_service + "." + $namespace + ".svc.cluster.local"), - port: { - number: ($service_port | tonumber) - } - }, - weight: $green_weight - } - ] - }' - else - # Single destination without blue/green - jq -n \ - --arg path "$rule_path" \ - --arg service_name "$service_name" \ - --arg service_port "$service_port" \ - --arg namespace "$K8S_NAMESPACE" \ - '{ - match: [ - { - uri: { - prefix: $path - } - } - ], - route: [ - { - destination: { - host: ($service_name + "." + $namespace + ".svc.cluster.local"), - port: { - number: ($service_port | tonumber) - } - } - } - ] - }' - fi -} - -update_virtualservice_route() { - local vs_yaml="$1" - local rule_path="$2" - local service_json="$3" - local blue_green_config="$4" - - local service_name - local service_port - local updated_vs - - service_name=$(echo "$service_json" | jq -r '.name') - service_port=$(echo "$service_json" | jq -r '.port.number // .port.name // 80') - - # Remove fault injection - updated_vs=$(echo "$vs_yaml" | yq eval 'del(.spec.http[0].fault)') - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].match[0].uri.prefix = \"$rule_path\"") - - # Check if there's blue/green configuration - if [[ "$blue_green_config" != "null" && -n "$blue_green_config" ]]; then - # Parse blue/green destinations and weights - local blue_weight green_weight blue_service green_service - - blue_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].weight // 100') - green_weight=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].weight // 0') - blue_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[0].serviceName') - green_service=$(echo "$blue_green_config" | jq -r '.forward.targetGroups[1].serviceName') - - # Set blue destination - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.host = \"${blue_service}.${K8S_NAMESPACE}.svc.cluster.local\"") - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.port.number = $service_port") - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].weight = $blue_weight") - - # Add green destination - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route += [{\"destination\": {\"host\": \"${green_service}.${K8S_NAMESPACE}.svc.cluster.local\", \"port\": {\"number\": $service_port}}, \"weight\": $green_weight}]") - else - # Single destination - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.host = \"${service_name}.${K8S_NAMESPACE}.svc.cluster.local\"") - updated_vs=$(echo "$updated_vs" | yq eval ".spec.http[0].route[0].destination.port.number = $service_port") - fi - - echo "$updated_vs" -} - -find_route_index() { - local vs_yaml="$1" - local target_path="$2" - - local num_routes - local i - local current_path - - num_routes=$(yq '.spec.http | length' <<< "$vs_yaml") - - for ((i=0; i "$VS_FILE" -else - ROUTE_INDEX=$(find_route_index "$VIRTUALSERVICE" "$RULE_PATH") - echo "Found route index for path '$RULE_PATH': $ROUTE_INDEX" - - # if there is a route for the path we replace it - if [[ "$ROUTE_INDEX" != "-1" ]]; then - echo "Case 2: Replacing existing route at index $ROUTE_INDEX" - UPDATED_VS=$(replace_existing_route "$VIRTUALSERVICE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "$ROUTE_INDEX") - echo "$UPDATED_VS" | yq "." > "$VS_FILE" - else - # if there is no route for the path we add a new one - echo "Case 3: Adding new route" - UPDATED_VS=$(add_new_route "$VIRTUALSERVICE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG") - echo "$UPDATED_VS" | yq "." > "$VS_FILE" - fi -fi - -echo "" -echo "=== VirtualService configuration saved to: $VS_FILE ===" \ No newline at end of file From 82dcf6b0b28e1761400f2b7fac9c48f5d999f4dc Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 18:52:36 -0300 Subject: [PATCH 010/101] chore: remove alb workflows --- endpoint-exposer/templates/alb/empty.yaml.tpl | 32 ----------- endpoint-exposer/workflows/alb/create.yaml | 44 --------------- endpoint-exposer/workflows/alb/delete.yaml | 6 -- endpoint-exposer/workflows/alb/link.yaml | 56 ------------------- endpoint-exposer/workflows/alb/read.yaml | 32 ----------- endpoint-exposer/workflows/alb/refresh.yaml | 7 --- endpoint-exposer/workflows/alb/unlink.yaml | 47 ---------------- 7 files changed, 224 deletions(-) delete mode 100644 endpoint-exposer/templates/alb/empty.yaml.tpl delete mode 100644 endpoint-exposer/workflows/alb/create.yaml delete mode 100644 endpoint-exposer/workflows/alb/delete.yaml delete mode 100644 endpoint-exposer/workflows/alb/link.yaml delete mode 100644 endpoint-exposer/workflows/alb/read.yaml delete mode 100644 endpoint-exposer/workflows/alb/refresh.yaml delete mode 100644 endpoint-exposer/workflows/alb/unlink.yaml diff --git a/endpoint-exposer/templates/alb/empty.yaml.tpl b/endpoint-exposer/templates/alb/empty.yaml.tpl deleted file mode 100644 index 35ef800..0000000 --- a/endpoint-exposer/templates/alb/empty.yaml.tpl +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: k-8-s-{{ .service.slug }}-{{ .service.id }}-public - namespace: {{ .k8s_namespace }} - labels: - nullplatform: "true" - service: {{ .service.slug }} - service_id: {{ .service.id }} - annotations: - kubernetes.io/ingress.class: alb - alb.ingress.kubernetes.io/group.name: {{ .alb_name }} - alb.ingress.kubernetes.io/load-balancer-name: {{ .alb_name }} - alb.ingress.kubernetes.io/scheme: internet-facing - alb.ingress.kubernetes.io/target-type: ip - alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80},{"HTTPS":443}]' - alb.ingress.kubernetes.io/ssl-redirect: "443" - alb.ingress.kubernetes.io/actions.response-404: >- - {"type":"fixed-response","fixedResponseConfig":{"contentType":"text/plain","statusCode":"404","messageBody":"no scopes exposed through this service."}} -spec: - ingressClassName: alb - rules: - - host: {{ .parameters.domain }} - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: response-404 - port: - name: use-annotation \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/create.yaml b/endpoint-exposer/workflows/alb/create.yaml deleted file mode 100644 index 7c8c3a2..0000000 --- a/endpoint-exposer/workflows/alb/create.yaml +++ /dev/null @@ -1,44 +0,0 @@ -steps: - - name: "find k8s namespace" - type: script - file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" - output: - - name: K8S_NAMESPACE - type: environment - - name: ALB_NAME - type: environment - - name: "build context" - type: script - file: "$SERVICE_PATH/scripts/alb/build_context" - output: - - name: SERVICE_ID - type: environment - - name: SERVICE_NAME - type: environment - - name: ACTION_ID - type: environment - - name: ACTION_NAME - type: environment - - name: LINK_ID - type: environment - - name: LINK_NAME - type: environment - - name: SCOPE_ID - type: environment - - name: RULE_PATH - type: environment - - name: "build template" - type: script - file: "$SERVICE_PATH/scripts/alb/build_ingress" - configuration: - TEMPLATE: "$SERVICE_PATH/templates/alb/empty.yaml.tpl" - output: - - name: INGRESS_FILE - type: file - file: "$OUTPUT_DIR/ingress-$SERVICE_ID-$SCOPE_ID-public.yaml" - - name: apply - type: script - file: "$SERVICE_PATH/scripts/alb/apply" - configuration: - ACTION: apply - DRY_RUN: false \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/delete.yaml b/endpoint-exposer/workflows/alb/delete.yaml deleted file mode 100644 index 3c7309e..0000000 --- a/endpoint-exposer/workflows/alb/delete.yaml +++ /dev/null @@ -1,6 +0,0 @@ -include: - - "$SERVICE_PATH/workflows/alb/create.yaml" -steps: - - name: apply - configuration: - ACTION: delete \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/link.yaml b/endpoint-exposer/workflows/alb/link.yaml deleted file mode 100644 index 0145798..0000000 --- a/endpoint-exposer/workflows/alb/link.yaml +++ /dev/null @@ -1,56 +0,0 @@ -steps: - - name: "find k8s namespace" - type: script - file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" - output: - - name: K8S_NAMESPACE - type: environment - - name: ALB_NAME - type: environment - - name: "build context" - type: script - file: "$SERVICE_PATH/scripts/alb/build_context" - output: - - name: SERVICE_ID - type: environment - - name: SERVICE_NAME - type: environment - - name: ACTION_ID - type: environment - - name: ACTION_NAME - type: environment - - name: LINK_ID - type: environment - - name: LINK_NAME - type: environment - - name: SCOPE_ID - type: environment - - name: RULE_PATH - type: environment - - name: "validate_unique_rule" - type: script - file: "$SERVICE_PATH/scripts/alb/validate_unique_rule" - - name: "build_rule" - type: script - file : "$SERVICE_PATH/scripts/alb/build_rule" - output: - - name: SCOPE_RULE - type: environment - - name: "build_ingress" - type: script - file: "$SERVICE_PATH/scripts/alb/build_ingress_with_rule" - output: - - name: INGRESS_FILE - type: file - file: "$OUTPUT_DIR/ingress-$SERVICE_ID-$SCOPE_ID-public.yaml" - - name: apply - type: script - file: "$SERVICE_PATH/scripts/alb/apply" - configuration: - ACTION: apply - DRY_RUN: false - - name: "add_rule_to_service" - type: script - file: "$SERVICE_PATH/scripts/alb/update_service" - configuration: - ACTION: add \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/read.yaml b/endpoint-exposer/workflows/alb/read.yaml deleted file mode 100644 index 8dea452..0000000 --- a/endpoint-exposer/workflows/alb/read.yaml +++ /dev/null @@ -1,32 +0,0 @@ -steps: - - name: "find k8s namespace" - type: script - file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" - output: - - name: K8S_NAMESPACE - type: environment - - name: ALB_NAME - type: environment - - name: "build context" - type: script - file: "$SERVICE_PATH/scripts/alb/build_context" - output: - - name: SERVICE_ID - type: environment - - name: SERVICE_NAME - type: environment - - name: ACTION_ID - type: environment - - name: ACTION_NAME - type: environment - - name: LINK_ID - type: environment - - name: LINK_NAME - type: environment - - name: SCOPE_ID - type: environment - - name: RULE_PATH - type: environment - - name: read - type: script - file: "$SERVICE_PATH/scripts/alb/read_ingress" \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/refresh.yaml b/endpoint-exposer/workflows/alb/refresh.yaml deleted file mode 100644 index 6edef06..0000000 --- a/endpoint-exposer/workflows/alb/refresh.yaml +++ /dev/null @@ -1,7 +0,0 @@ -include: - - "$SERVICE_PATH/workflows/alb/link.yaml" -steps: - - name: "validate_unique_rule" - action: skip - - name: "add_rule_to_service" - action: skip \ No newline at end of file diff --git a/endpoint-exposer/workflows/alb/unlink.yaml b/endpoint-exposer/workflows/alb/unlink.yaml deleted file mode 100644 index 690374a..0000000 --- a/endpoint-exposer/workflows/alb/unlink.yaml +++ /dev/null @@ -1,47 +0,0 @@ -steps: - - name: "find k8s namespace" - type: script - file: "$SERVICE_PATH/scripts/alb/fetch_provider_data" - output: - - name: K8S_NAMESPACE - type: environment - - name: ALB_NAME - type: environment - - name: "build context" - type: script - file: "$SERVICE_PATH/scripts/alb/build_context" - output: - - name: SERVICE_ID - type: environment - - name: SERVICE_NAME - type: environment - - name: ACTION_ID - type: environment - - name: ACTION_NAME - type: environment - - name: LINK_ID - type: environment - - name: LINK_NAME - type: environment - - name: SCOPE_ID - type: environment - - name: RULE_PATH - type: environment - - name: "build_template" - type: script - file: "$SERVICE_PATH/scripts/alb/build_ingress_without_rule" - output: - - name: INGRESS_FILE - type: file - file: "$OUTPUT_DIR/ingress-$SERVICE_ID-$SCOPE_ID-public.yaml" - - name: apply - type: script - file: "$SERVICE_PATH/scripts/alb/apply" - configuration: - ACTION: apply - DRY_RUN: false - - name: "add_rule_to_service" - type: script - file: "$SERVICE_PATH/scripts/alb/update_service" - configuration: - ACTION: remove \ No newline at end of file From af379b14b61f88f3f523a2717fefe657090612c4 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 5 Jan 2026 19:19:43 -0300 Subject: [PATCH 011/101] chore: remove unused scripts and dns endpoints --- .../scripts/{istio => common}/apply | 0 endpoint-exposer/scripts/dns/build_dns | 15 ----- .../scripts/dns/build_dns_endpoint | 58 ----------------- .../scripts/istio/build_ingress_without_rule | 62 ------------------- endpoint-exposer/scripts/istio/update_service | 21 ------- .../scripts/istio/validate_unique_rule | 12 ---- .../istio/dns-endpoint-internal.yaml.tpl | 17 ----- .../templates/istio/dns-endpoint.yaml.tpl | 16 ----- endpoint-exposer/workflows/istio/create.yaml | 20 +----- endpoint-exposer/workflows/istio/update.yaml | 20 +----- 10 files changed, 2 insertions(+), 239 deletions(-) rename endpoint-exposer/scripts/{istio => common}/apply (100%) delete mode 100755 endpoint-exposer/scripts/dns/build_dns delete mode 100755 endpoint-exposer/scripts/dns/build_dns_endpoint delete mode 100644 endpoint-exposer/scripts/istio/build_ingress_without_rule delete mode 100644 endpoint-exposer/scripts/istio/update_service delete mode 100644 endpoint-exposer/scripts/istio/validate_unique_rule delete mode 100644 endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl delete mode 100644 endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl diff --git a/endpoint-exposer/scripts/istio/apply b/endpoint-exposer/scripts/common/apply similarity index 100% rename from endpoint-exposer/scripts/istio/apply rename to endpoint-exposer/scripts/common/apply diff --git a/endpoint-exposer/scripts/dns/build_dns b/endpoint-exposer/scripts/dns/build_dns deleted file mode 100755 index 6a6641b..0000000 --- a/endpoint-exposer/scripts/dns/build_dns +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -# Build DNS context based on DNS_TYPE -echo "=== DEBUG: Starting build_dns ===" -echo "DNS_TYPE=$DNS_TYPE" - -case "$DNS_TYPE" in - external_dns) - "$SERVICE_PATH/scripts/dns/build_dns_endpoint" - ;; - *) - echo "Error: Unsupported DNS type '$DNS_TYPE'" - exit 1 - ;; -esac \ No newline at end of file diff --git a/endpoint-exposer/scripts/dns/build_dns_endpoint b/endpoint-exposer/scripts/dns/build_dns_endpoint deleted file mode 100755 index e55e181..0000000 --- a/endpoint-exposer/scripts/dns/build_dns_endpoint +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "=== DEBUG: Starting build_dns_endpoint ===" - -# Determine visibility (default to public if not set) -VISIBILITY="${VISIBILITY:-public}" -echo "DEBUG: VISIBILITY=$VISIBILITY" - -# Select domain and gateway based on visibility -if [[ "$VISIBILITY" == "internal" ]]; then - SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain') - GATEWAY_NAME=${INTERNAL_GATEWAY_NAME:-"gateway-private"} - DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint-internal.yaml.tpl" - DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" - echo "DEBUG: Building INTERNAL DNS endpoint" -else - SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain') - GATEWAY_NAME=${PUBLIC_GATEWAY_NAME:-"gateway-public"} - DNS_TEMPLATE="$SERVICE_PATH/templates/istio/dns-endpoint.yaml.tpl" - DNS_FILE="$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" - echo "DEBUG: Building PUBLIC DNS endpoint" -fi - -echo "DEBUG: SERVICE_DOMAIN=$SERVICE_DOMAIN" -echo "DEBUG: GATEWAY_NAME=$GATEWAY_NAME" - -GATEWAY_NAMESPACE=${GATEWAY_NAMESPACE:-"gateways"} - -echo "DEBUG: Fetching gateway IP from $GATEWAY_NAME..." -GATEWAY_IP=$(kubectl get gateway -n "$GATEWAY_NAMESPACE" "$GATEWAY_NAME" -o jsonpath='{.status.addresses[0].value}') -echo "DEBUG: GATEWAY_IP=$GATEWAY_IP" - -if [[ -z "$GATEWAY_IP" || "$GATEWAY_IP" == "null" ]]; then - echo "ERROR: Could not retrieve gateway IP from $GATEWAY_NAME" - exit 1 -fi - -echo "Creating DNSEndpoint for service $SERVICE_NAME with domain $SERVICE_DOMAIN" - -CONTEXT_PATH="$OUTPUT_DIR/context-dns-$SERVICE_ID-$VISIBILITY.json" - -# Add gateway_ip to context -CONTEXT_WITH_IP=$(echo "$CONTEXT" | jq --arg gateway_ip "$GATEWAY_IP" '. + {gateway_ip: $gateway_ip}') - -echo "$CONTEXT_WITH_IP" > "$CONTEXT_PATH" - -echo "Building DNSEndpoint Template to $DNS_FILE" - -gomplate -c .="$CONTEXT_PATH" \ - --file "$DNS_TEMPLATE" \ - --out "$DNS_FILE" - -rm "$CONTEXT_PATH" - -echo "DEBUG: DNSEndpoint created at $DNS_FILE" -echo "=== DEBUG: build_dns_endpoint completed ===" diff --git a/endpoint-exposer/scripts/istio/build_ingress_without_rule b/endpoint-exposer/scripts/istio/build_ingress_without_rule deleted file mode 100644 index 2fef8a3..0000000 --- a/endpoint-exposer/scripts/istio/build_ingress_without_rule +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -find_rule_index() { - local hr_yaml="$1" - local target_path="$2" - - local num_rules - local i - local current_path - - num_rules=$(yq '.spec.rules | length' <<< "$hr_yaml") - - for ((i=0; i "$HTTPROUTE_FILE" - -echo "=== HTTPRoute configuration saved to: $HTTPROUTE_FILE ===" diff --git a/endpoint-exposer/scripts/istio/update_service b/endpoint-exposer/scripts/istio/update_service deleted file mode 100644 index 5f743d3..0000000 --- a/endpoint-exposer/scripts/istio/update_service +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -if [[ $ACTION == "add" ]]; then - UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" --arg scope "$SCOPE_SLUG" ' - .service.attributes |= (.rules //= [] | .rules += [{path: $path, scope: $scope}]) | - { - attributes: .service.attributes, - entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) - } - ') -elif [[ $ACTION == "remove" ]]; then - UPDATED_ATTRIBUTES=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' - .service.attributes |= (.rules //= [] | .rules |= map(select(.path != $path))) | - { - attributes: .service.attributes, - entity_nrn: (.entity_nrn | sub(":scope=[^:]+$"; "")) - } - ') -fi - -np service patch --id "$SERVICE_ID" --body "$UPDATED_ATTRIBUTES" diff --git a/endpoint-exposer/scripts/istio/validate_unique_rule b/endpoint-exposer/scripts/istio/validate_unique_rule deleted file mode 100644 index 49a56b1..0000000 --- a/endpoint-exposer/scripts/istio/validate_unique_rule +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -EXISTS=$(echo "$CONTEXT" | jq --arg path "$RULE_PATH" ' - (.service.attributes.rules // []) - | map(select(.path == $path)) - | length -') - -if [[ "$EXISTS" -gt 0 ]]; then - echo "Error: A rule with path '$RULE_PATH' already exists." - exit 1 -fi \ No newline at end of file diff --git a/endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl b/endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl deleted file mode 100644 index 8cb3198..0000000 --- a/endpoint-exposer/templates/istio/dns-endpoint-internal.yaml.tpl +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: externaldns.k8s.io/v1alpha1 -kind: DNSEndpoint -metadata: - name: {{ .service.slug }}-{{ .service.id }}-dns-internal - namespace: {{ .k8s_namespace }} - labels: - nullplatform: "true" - service: {{ .service.slug }} - service_id: {{ .service.id }} - visibility: internal -spec: - endpoints: - - dnsName: {{ if has . "parameters" }}{{ if has .parameters "private_domain" }}{{ .parameters.private_domain }}{{ else }}{{ .service.attributes.private_domain }}{{ end }}{{ else }}{{ .service.attributes.private_domain }}{{ end }} - recordTTL: 60 - recordType: A - targets: - - "{{ .gateway_ip }}" diff --git a/endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl b/endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl deleted file mode 100644 index e8983cf..0000000 --- a/endpoint-exposer/templates/istio/dns-endpoint.yaml.tpl +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: externaldns.k8s.io/v1alpha1 -kind: DNSEndpoint -metadata: - name: {{ .service.slug }}-{{ .service.id }}-dns - namespace: {{ .k8s_namespace }} - labels: - nullplatform: "true" - service: {{ .service.slug }} - service_id: {{ .service.id }} -spec: - endpoints: - - dnsName: {{ if has . "parameters" }}{{ if has .parameters "public_domain" }}{{ .parameters.public_domain }}{{ else if has .parameters "domain" }}{{ .parameters.domain }}{{ else }}{{ .service.attributes.domain }}{{ end }}{{ else }}{{ .service.attributes.domain }}{{ end }} - recordTTL: 60 - recordType: A - targets: - - "{{ .gateway_ip }}" diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 3290a23..90e9032 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -52,27 +52,9 @@ steps: - name: HTTPROUTE_FILE_INTERNAL type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - - name: "build public dns endpoint" - type: script - file: "$SERVICE_PATH/scripts/dns/build_dns" - configuration: - VISIBILITY: "public" - output: - - name: DNS_FILE_PUBLIC - type: file - file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" - - name: "build internal dns endpoint" - type: script - file: "$SERVICE_PATH/scripts/dns/build_dns" - configuration: - VISIBILITY: "internal" - output: - - name: DNS_FILE_INTERNAL - type: file - file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" - name: apply type: script - file: "$SERVICE_PATH/scripts/istio/apply" + file: "$SERVICE_PATH/scripts/common/apply" configuration: ACTION: apply DRY_RUN: false \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index d8b3d04..29ee6aa 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -76,27 +76,9 @@ steps: - name: HTTPROUTE_FILE_INTERNAL type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml" - - name: "update public dns endpoint" - type: script - file: "$SERVICE_PATH/scripts/dns/build_dns" - configuration: - VISIBILITY: "public" - output: - - name: DNS_FILE_PUBLIC - type: file - file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-public.yaml" - - name: "update internal dns endpoint" - type: script - file: "$SERVICE_PATH/scripts/dns/build_dns" - configuration: - VISIBILITY: "internal" - output: - - name: DNS_FILE_INTERNAL - type: file - file: "$OUTPUT_DIR/dnsendpoint-$SERVICE_ID-internal.yaml" - name: apply type: script - file: "$SERVICE_PATH/scripts/istio/apply" + file: "$SERVICE_PATH/scripts/common/apply" configuration: ACTION: apply DRY_RUN: false From 11e231578ddd5f1d37de805f6a15c222d604f52d Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 09:32:40 -0300 Subject: [PATCH 012/101] chore: remote DEBUG logs --- .../container-scope-override/deployment/sync_exposer | 4 ---- endpoint-exposer/container-scope-override/values.yaml | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer index 570373c..de96760 100755 --- a/endpoint-exposer/container-scope-override/deployment/sync_exposer +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -1,8 +1,6 @@ #!/bin/bash echo "=== DEBUG: Starting sync_exposer script ===" -echo "DEBUG: CONTEXT=$CONTEXT" -echo "DEBUG: SERVICE_SPECIFICATION_SLUG=$SERVICE_SPECIFICATION_SLUG" SCOPE_NRN=$(echo "$CONTEXT" | jq -r .scope.nrn) @@ -11,14 +9,12 @@ echo "SERVICE SPECIFICATION SLUG: $SERVICE_SPECIFICATION_SLUG, SCOPE_NRN: $SCOPE # Step 1: Get service specification by slug echo "DEBUG: Fetching service specifications..." SERVICE_SPECS=$(np service specification list --nrn "$SCOPE_NRN" --type dependency --format json) -echo "DEBUG: Service specifications fetched: $SERVICE_SPECS" SERVICE_SPEC=$(echo "$SERVICE_SPECS" | jq -c --arg slug "$SERVICE_SPECIFICATION_SLUG" ' .results | map(select(.slug == $slug)) | .[0] ') -echo "DEBUG: Filtered service spec: $SERVICE_SPEC" SERVICE_SPEC_ID=$(echo "$SERVICE_SPEC" | jq -r .id) diff --git a/endpoint-exposer/container-scope-override/values.yaml b/endpoint-exposer/container-scope-override/values.yaml index 784fe86..101aee8 100644 --- a/endpoint-exposer/container-scope-override/values.yaml +++ b/endpoint-exposer/container-scope-override/values.yaml @@ -1,3 +1,3 @@ configuration: - SERVICE_SPECIFICATION_SLUG: service-exposer-v2 + SERVICE_SPECIFICATION_SLUG: endpoint-exposer \ No newline at end of file From 2b89d8a3f96c11e7332a2a04424963d128879b57 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 09:39:50 -0300 Subject: [PATCH 013/101] chore: remote DEBUG logs --- endpoint-exposer/scripts/istio/clean_httproute_rules | 5 ++++- endpoint-exposer/scripts/istio/update_httproute_hostname | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/endpoint-exposer/scripts/istio/clean_httproute_rules b/endpoint-exposer/scripts/istio/clean_httproute_rules index 55c7dda..ae6087c 100755 --- a/endpoint-exposer/scripts/istio/clean_httproute_rules +++ b/endpoint-exposer/scripts/istio/clean_httproute_rules @@ -4,7 +4,10 @@ set -euo pipefail # If HTTPROUTE_FILE_VAR is provided, use it to get the file path from that variable if [[ -n "${HTTPROUTE_FILE_VAR:-}" ]]; then - HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" + # Use indirect variable expansion safely + if [[ -n "${!HTTPROUTE_FILE_VAR:-}" ]]; then + HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" + fi fi echo "=== Cleaning HTTPRoute rules ===" diff --git a/endpoint-exposer/scripts/istio/update_httproute_hostname b/endpoint-exposer/scripts/istio/update_httproute_hostname index 7f7b044..df047f7 100755 --- a/endpoint-exposer/scripts/istio/update_httproute_hostname +++ b/endpoint-exposer/scripts/istio/update_httproute_hostname @@ -4,7 +4,10 @@ set -euo pipefail # If HTTPROUTE_FILE_VAR is provided, use it to get the file path from that variable if [[ -n "${HTTPROUTE_FILE_VAR:-}" ]]; then - HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" + # Use indirect variable expansion safely + if [[ -n "${!HTTPROUTE_FILE_VAR:-}" ]]; then + HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" + fi fi # Use DOMAIN and GATEWAY if provided via configuration, otherwise use PUBLIC_DOMAIN and gateway-public From d0638a0f2f12fc5bc455c248f876569795a05c16 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 14:35:30 -0300 Subject: [PATCH 014/101] chore: DEBUG logs --- .../container-scope-override/deployment/sync_exposer | 1 + 1 file changed, 1 insertion(+) diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer index de96760..cd99fcc 100755 --- a/endpoint-exposer/container-scope-override/deployment/sync_exposer +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -2,6 +2,7 @@ echo "=== DEBUG: Starting sync_exposer script ===" +echo "DEBUG: $CONTEXT" SCOPE_NRN=$(echo "$CONTEXT" | jq -r .scope.nrn) echo "SERVICE SPECIFICATION SLUG: $SERVICE_SPECIFICATION_SLUG, SCOPE_NRN: $SCOPE_NRN" From 9f89c08e6dfed8c84d566f411a49915b72139596 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 16:08:30 -0300 Subject: [PATCH 015/101] feat: adapt endpoint exposer to work by service action --- .../deployment/sync_exposer | 144 ++++++++---------- endpoint-exposer/workflows/istio/refresh.yaml | 7 +- 2 files changed, 65 insertions(+), 86 deletions(-) diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer index cd99fcc..08f92d6 100755 --- a/endpoint-exposer/container-scope-override/deployment/sync_exposer +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -3,21 +3,20 @@ echo "=== DEBUG: Starting sync_exposer script ===" echo "DEBUG: $CONTEXT" -SCOPE_NRN=$(echo "$CONTEXT" | jq -r .scope.nrn) +APPLICATION_NRN=$(jq -r .application.nrn <<< "$CONTEXT") -echo "SERVICE SPECIFICATION SLUG: $SERVICE_SPECIFICATION_SLUG, SCOPE_NRN: $SCOPE_NRN" +echo "SERVICE SPECIFICATION SLUG: $SERVICE_SPECIFICATION_SLUG, APPLICATION_NRN: $APPLICATION_NRN" # Step 1: Get service specification by slug echo "DEBUG: Fetching service specifications..." -SERVICE_SPECS=$(np service specification list --nrn "$SCOPE_NRN" --type dependency --format json) - -SERVICE_SPEC=$(echo "$SERVICE_SPECS" | jq -c --arg slug "$SERVICE_SPECIFICATION_SLUG" ' +SERVICE_SPECS=$(np service specification list --nrn "$APPLICATION_NRN" --type dependency --format json) +SERVICE_SPEC=$(jq -c --arg slug "$SERVICE_SPECIFICATION_SLUG" ' .results | map(select(.slug == $slug)) | .[0] -') +' <<< "$SERVICE_SPECS") -SERVICE_SPEC_ID=$(echo "$SERVICE_SPEC" | jq -r .id) +SERVICE_SPEC_ID=$(jq -r .id <<< "$SERVICE_SPEC") if [[ -z "$SERVICE_SPEC_ID" || "$SERVICE_SPEC_ID" == "null" ]]; then echo "Error: Could not find service specification with slug '$SERVICE_SPECIFICATION_SLUG'" @@ -26,106 +25,91 @@ fi echo "DEBUG: SERVICE_SPEC_ID=$SERVICE_SPEC_ID" -# Step 2: Get link specification with slug "publish" -echo "DEBUG: Fetching link specifications for service spec..." -LINK_SPECS=$(np service specification link specification list --id "$SERVICE_SPEC_ID" --format json) -echo "DEBUG: Link specifications fetched: $LINK_SPECS" +# Step 2: Get service instance that matches the SERVICE_SPEC_ID +echo "DEBUG: Fetching services for application..." +SERVICES=$(np service list --nrn "$APPLICATION_NRN" --format json) -PUBLISH_LINK_SPEC=$(echo "$LINK_SPECS" | jq -c ' +SERVICE=$(jq -c --arg spec_id "$SERVICE_SPEC_ID" ' .results - | map(select(.slug == "publish")) + | map(select(.specification_id == $spec_id)) | .[0] -') -echo "DEBUG: Filtered publish link spec: $PUBLISH_LINK_SPEC" +' <<< "$SERVICES") -LINK_SPECIFICATION_ID=$(echo "$PUBLISH_LINK_SPEC" | jq -r .id) +SERVICE_ID=$(jq -r .id <<< "$SERVICE") -if [[ -z "$LINK_SPECIFICATION_ID" || "$LINK_SPECIFICATION_ID" == "null" ]]; then - echo "Error: Could not find link specification with slug 'publish' for service specification '$SERVICE_SPEC_ID'" +if [[ -z "$SERVICE_ID" || "$SERVICE_ID" == "null" ]]; then + echo "Error: Could not find service instance for specification '$SERVICE_SPEC_ID'" exit 1 fi -echo "DEBUG: LINK_SPECIFICATION_ID=$LINK_SPECIFICATION_ID" - -echo "DEBUG: Fetching links for scope..." -LINKS=$(np link list --nrn "$SCOPE_NRN" --format json --status active) -echo "DEBUG: Links fetched. Response: $LINKS" +echo "DEBUG: SERVICE_ID=$SERVICE_ID" -EXPOSER_LINKS=$(echo "$LINKS" | jq -c --arg spec "$LINK_SPECIFICATION_ID" ' - .results - | map(select(.specification_id == $spec)) -') -echo "DEBUG: EXPOSER_LINKS filtered: $EXPOSER_LINKS" +# Step 3: Get service attributes as parameters +echo "DEBUG: Reading service attributes..." +SERVICE_DATA=$(np service read --id "$SERVICE_ID" --format json) +export PARAMETERS=$(jq -c .attributes <<< "$SERVICE_DATA") -COUNT=$(echo "$EXPOSER_LINKS" | jq 'length') -echo "DEBUG: COUNT=$COUNT" - -if [[ "$COUNT" -eq 0 ]]; then - echo "This scope is not public, skipping public ingress synchronization" - return -fi +echo "DEBUG: PARAMETERS=$PARAMETERS" -echo "This is a public scope, starting public ingress synchronization" +# Step 4: Get action specification with slug "update-" +ACTION_SLUG="update-$SERVICE_SPECIFICATION_SLUG" +echo "DEBUG: Fetching action specifications (looking for slug: $ACTION_SLUG)..." +SERVICE_ACTIONS=$(np service specification action specification list --serviceSpecificationId "$SERVICE_SPEC_ID" --format json) -ACTIONS=$(np link specification action specification list --linkSpecificationId "$LINK_SPECIFICATION_ID" --format json) - -REFRESH_ACTION=$(echo "$ACTIONS" | jq -c ' +ACTION_SPEC=$(jq -c --arg slug "$ACTION_SLUG" ' .results - | map(select(.slug == "refresh")) + | map(select(.slug == $slug)) | .[0] -') +' <<< "$SERVICE_ACTIONS") -REFRESH_ACTION_ID=$(echo "$REFRESH_ACTION" | jq -r .id) -echo "DEBUG: REFRESH_ACTION_ID=$REFRESH_ACTION_ID" +ACTION_SPEC_ID=$(jq -r .id <<< "$ACTION_SPEC") -# Iterate over matches -echo "DEBUG: Starting iteration over exposer links..." -echo "$EXPOSER_LINKS" | jq -c '.[]' | while read -r link; do - echo "DEBUG: Processing link: $link" +if [[ -z "$ACTION_SPEC_ID" || "$ACTION_SPEC_ID" == "null" ]]; then + echo "Error: Could not find action specification with slug '$ACTION_SLUG' for service specification '$SERVICE_SPEC_ID'" + exit 1 +fi - # Extract useful values - LINK_ID=$(echo "$link" | jq -r '.id') - echo "DEBUG: LINK_ID=$LINK_ID" +echo "DEBUG: ACTION_SPEC_ID=$ACTION_SPEC_ID" - echo "DEBUG: Creating link action..." - ACTION=$(np link action create --linkId "$LINK_ID" --body "{\"name\": \"refresh\", \"parameters\":{}, \"specification_id\": \"$REFRESH_ACTION_ID\"}" --format json) - echo "DEBUG: Action created: $ACTION" +# Step 5: Create service action with parameters +echo "DEBUG: Creating service action..." +ACTION=$(np service action create --serviceId "$SERVICE_ID" --body "$(jq -n --argjson params "$PARAMETERS" --arg spec_id "$ACTION_SPEC_ID" '{name: "update", parameters: $params, specification_id: $spec_id}')" --format json) - ACTION_ID=$(echo "$ACTION" | jq -r .id) - echo "DEBUG: ACTION_ID=$ACTION_ID" +ACTION_ID=$(jq -r .id <<< "$ACTION") +echo "DEBUG: ACTION_ID=$ACTION_ID" - echo "Created public ingress synchronization action[id=$ACTION_ID], waiting for its completion" +echo "Created endpoint exposer update action[id=$ACTION_ID], waiting for its completion" - MAX_ITERATIONS=20 - iteration=0 +# Step 6: Wait for action to complete +MAX_ITERATIONS=20 +iteration=0 - echo "DEBUG: Starting polling loop for action status..." - while true; do - ((iteration++)) - echo "DEBUG: Iteration $iteration/$MAX_ITERATIONS" +echo "DEBUG: Starting polling loop for action status..." +while true; do + ((iteration++)) + echo "DEBUG: Iteration $iteration/$MAX_ITERATIONS" - if [ "$iteration" -gt $MAX_ITERATIONS ]; then - echo "Error: Maximum number of iterations (${MAX_ITERATIONS}) reached. Could not refresh the exposer." - exit 1 - fi + if [ "$iteration" -gt $MAX_ITERATIONS ]; then + echo "Error: Maximum number of iterations (${MAX_ITERATIONS}) reached. Could not update the endpoint exposer." + exit 1 + fi - echo "DEBUG: Reading action status..." - ACTION_STATUS=$(np link action read --linkId "$LINK_ID" --id "$ACTION_ID" --format json | jq -r .status) + echo "DEBUG: Reading action status..." + ACTION_RESPONSE=$(np service action read --id "$ACTION_ID" --format json) + ACTION_STATUS=$(jq -r .status <<< "$ACTION_RESPONSE") - echo "Checking public ingress synchronization action[id=$ACTION_ID, status=$ACTION_STATUS]" + echo "Checking endpoint exposer update action[id=$ACTION_ID, status=$ACTION_STATUS]" - if [[ "$ACTION_STATUS" == "success" ]]; then - echo "✅ Public ingress successfully synchronized" - break - elif [[ "$ACTION_STATUS" == "failed" ]]; then - echo "❌ Could not synchronize public ingress, deployment will be rollbacked" - exit 1 - fi + if [[ "$ACTION_STATUS" == "success" ]]; then + echo "✅ Endpoint exposer successfully updated" + break + elif [[ "$ACTION_STATUS" == "failed" ]]; then + echo "❌ Could not update endpoint exposer, deployment will be rollbacked" + exit 1 + fi - echo "DEBUG: Sleeping for 5 seconds..." - sleep 5 - done - echo "DEBUG: Finished processing link $LINK_ID" + echo "DEBUG: Sleeping for 5 seconds..." + sleep 5 done echo "=== DEBUG: sync_exposer script completed successfully ===" diff --git a/endpoint-exposer/workflows/istio/refresh.yaml b/endpoint-exposer/workflows/istio/refresh.yaml index a920564..f2f102d 100644 --- a/endpoint-exposer/workflows/istio/refresh.yaml +++ b/endpoint-exposer/workflows/istio/refresh.yaml @@ -1,7 +1,2 @@ include: - - "$SERVICE_PATH/workflows/istio/link.yaml" -steps: - - name: "validate_unique_rule" - action: skip - - name: "add_rule_to_service" - action: skip \ No newline at end of file + - "$SERVICE_PATH/workflows/istio/update.yaml" \ No newline at end of file From 4f3a2ecc619f1369fa75a82212c237a72dc67a2d Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 16:13:55 -0300 Subject: [PATCH 016/101] feat: adapt endpoint exposer to work by service action --- .../container-scope-override/deployment/sync_exposer | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer index 08f92d6..0999a86 100755 --- a/endpoint-exposer/container-scope-override/deployment/sync_exposer +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -95,7 +95,7 @@ while true; do fi echo "DEBUG: Reading action status..." - ACTION_RESPONSE=$(np service action read --id "$ACTION_ID" --format json) + ACTION_RESPONSE=$(np service action read --serviceId "$SERVICE_ID" --id "$ACTION_ID" --format json) ACTION_STATUS=$(jq -r .status <<< "$ACTION_RESPONSE") echo "Checking endpoint exposer update action[id=$ACTION_ID, status=$ACTION_STATUS]" From cc52ddef88027bd6566797893f08969160d24796 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 16:27:22 -0300 Subject: [PATCH 017/101] feat: adapt endpoint exposer to work by service action --- .../scripts/istio/build_ingress_with_rule | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_ingress_with_rule b/endpoint-exposer/scripts/istio/build_ingress_with_rule index eb5bae1..b3d5f1f 100755 --- a/endpoint-exposer/scripts/istio/build_ingress_with_rule +++ b/endpoint-exposer/scripts/istio/build_ingress_with_rule @@ -216,18 +216,41 @@ update_httproute_rule() { find_rule_index() { local hr_yaml="$1" local target_path="$2" + local target_method="${3:-}" local num_rules local i local current_path + local current_method num_rules=$(yq '.spec.rules | length' <<< "$hr_yaml") for ((i=0; i "$HTTPROUTE_FILE" else - RULE_INDEX=$(find_rule_index "$HTTPROUTE" "$RULE_PATH") - echo "Found rule index for path '$RULE_PATH': $RULE_INDEX" + # Detect the converted path value to match against existing rules + PATH_TYPE_VALUE=$(detect_path_type "$RULE_PATH") + CONVERTED_PATH="${PATH_TYPE_VALUE#*:}" + + RULE_INDEX=$(find_rule_index "$HTTPROUTE" "$CONVERTED_PATH" "${METHOD:-}") + echo "Found rule index for path '$CONVERTED_PATH' with method '${METHOD:-none}': $RULE_INDEX" # if there is a rule for the path we replace it if [[ "$RULE_INDEX" != "-1" ]]; then From 85b25e01b967d057ad287f6f6e7a1b21a68bd65b Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 6 Jan 2026 17:19:34 -0300 Subject: [PATCH 018/101] fix: retries on action creation --- .../deployment/sync_exposer | 69 +++++++++++++++++-- 1 file changed, 63 insertions(+), 6 deletions(-) diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer index 0999a86..da6e3a4 100755 --- a/endpoint-exposer/container-scope-override/deployment/sync_exposer +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -2,7 +2,6 @@ echo "=== DEBUG: Starting sync_exposer script ===" -echo "DEBUG: $CONTEXT" APPLICATION_NRN=$(jq -r .application.nrn <<< "$CONTEXT") echo "SERVICE SPECIFICATION SLUG: $SERVICE_SPECIFICATION_SLUG, APPLICATION_NRN: $APPLICATION_NRN" @@ -71,14 +70,72 @@ fi echo "DEBUG: ACTION_SPEC_ID=$ACTION_SPEC_ID" -# Step 5: Create service action with parameters +# Step 5: Create service action with parameters (with retry for concurrency) echo "DEBUG: Creating service action..." -ACTION=$(np service action create --serviceId "$SERVICE_ID" --body "$(jq -n --argjson params "$PARAMETERS" --arg spec_id "$ACTION_SPEC_ID" '{name: "update", parameters: $params, specification_id: $spec_id}')" --format json) -ACTION_ID=$(jq -r .id <<< "$ACTION") -echo "DEBUG: ACTION_ID=$ACTION_ID" +MAX_CREATE_RETRIES=10 +RETRY_DELAY=5 +create_attempt=0 +ACTION_ID="" -echo "Created endpoint exposer update action[id=$ACTION_ID], waiting for its completion" +while [[ -z "$ACTION_ID" || "$ACTION_ID" == "null" ]]; do + ((create_attempt++)) + echo "DEBUG: Create attempt $create_attempt/$MAX_CREATE_RETRIES" + + if [ "$create_attempt" -gt $MAX_CREATE_RETRIES ]; then + echo "Error: Maximum number of create attempts (${MAX_CREATE_RETRIES}) reached. Could not create action." + exit 1 + fi + + # Add delay before retry (except on first attempt) + if [ "$create_attempt" -gt 1 ]; then + echo "DEBUG: Waiting ${RETRY_DELAY} seconds before retry..." + sleep $RETRY_DELAY + fi + + # Try to create the action + ACTION_RESPONSE=$(np service action create --serviceId "$SERVICE_ID" --body "$(jq -n --argjson params "$PARAMETERS" --arg spec_id "$ACTION_SPEC_ID" '{name: "update", parameters: $params, specification_id: $spec_id}')" --format json 2>&1 || true) + + # Check if response contains an error about action already in progress + if echo "$ACTION_RESPONSE" | grep -q "already an action with status.*in_progress"; then + echo "DEBUG: Action already in progress detected" + + # Try to find the existing in_progress action + echo "DEBUG: Attempting to find existing in_progress action..." + EXISTING_ACTIONS=$(np service action list --serviceId "$SERVICE_ID" --format json) + EXISTING_ACTION=$(echo "$EXISTING_ACTIONS" | jq -c --arg spec_id "$ACTION_SPEC_ID" ' + .results + | map(select(.specification_id == $spec_id and .status == "in_progress")) + | .[0] + ') + + EXISTING_ACTION_ID=$(echo "$EXISTING_ACTION" | jq -r '.id // empty') + + if [[ -n "$EXISTING_ACTION_ID" && "$EXISTING_ACTION_ID" != "null" ]]; then + echo "DEBUG: Found existing in_progress action with ID: $EXISTING_ACTION_ID" + ACTION_ID="$EXISTING_ACTION_ID" + echo "Using existing action instead of creating new one" + break + fi + + echo "DEBUG: No existing action found, will retry..." + elif echo "$ACTION_RESPONSE" | grep -q '"error"'; then + echo "ERROR: Failed to create action: $ACTION_RESPONSE" + echo "DEBUG: Will retry after delay..." + else + # Success - extract action ID + ACTION_ID=$(echo "$ACTION_RESPONSE" | jq -r '.id // empty') + + if [[ -n "$ACTION_ID" && "$ACTION_ID" != "null" ]]; then + echo "DEBUG: ACTION_ID=$ACTION_ID" + echo "Created endpoint exposer update action[id=$ACTION_ID], waiting for its completion" + break + else + echo "DEBUG: Could not extract ACTION_ID from response: $ACTION_RESPONSE" + echo "DEBUG: Will retry after delay..." + fi + fi +done # Step 6: Wait for action to complete MAX_ITERATIONS=20 From 3a7991eb9f0cdd9d2a2c1e2612f05b5d2e82dfd2 Mon Sep 17 00:00:00 2001 From: Javi Date: Wed, 7 Jan 2026 13:59:13 -0300 Subject: [PATCH 019/101] feat: if no service instance created allow continue without errors --- .../container-scope-override/deployment/sync_exposer | 4 ++-- endpoint-exposer/workflows/istio/refresh.yaml | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 endpoint-exposer/workflows/istio/refresh.yaml diff --git a/endpoint-exposer/container-scope-override/deployment/sync_exposer b/endpoint-exposer/container-scope-override/deployment/sync_exposer index da6e3a4..06d1058 100755 --- a/endpoint-exposer/container-scope-override/deployment/sync_exposer +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -37,8 +37,8 @@ SERVICE=$(jq -c --arg spec_id "$SERVICE_SPEC_ID" ' SERVICE_ID=$(jq -r .id <<< "$SERVICE") if [[ -z "$SERVICE_ID" || "$SERVICE_ID" == "null" ]]; then - echo "Error: Could not find service instance for specification '$SERVICE_SPEC_ID'" - exit 1 + echo "Could not find service instance for specification '$SERVICE_SPEC_ID', skipping exposer sync" + exit 0 fi echo "DEBUG: SERVICE_ID=$SERVICE_ID" diff --git a/endpoint-exposer/workflows/istio/refresh.yaml b/endpoint-exposer/workflows/istio/refresh.yaml deleted file mode 100644 index f2f102d..0000000 --- a/endpoint-exposer/workflows/istio/refresh.yaml +++ /dev/null @@ -1,2 +0,0 @@ -include: - - "$SERVICE_PATH/workflows/istio/update.yaml" \ No newline at end of file From f7b425a2e76a7ec1f092e65d234fba7b6b14736b Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 8 Jan 2026 11:44:04 -0300 Subject: [PATCH 020/101] feat: remove links --- endpoint-exposer/scripts/common/echo | 3 - endpoint-exposer/scripts/istio/build_context | 18 +-- endpoint-exposer/scripts/istio/build_ingress | 19 +-- .../scripts/istio/build_ingress_with_rule | 14 +- .../scripts/istio/clean_httproute_rules | 8 - .../scripts/istio/fetch_httproute | 17 +-- endpoint-exposer/scripts/istio/process_routes | 137 ++++++------------ .../scripts/istio/update_httproute_hostname | 10 +- endpoint-exposer/specs/link-spec.json.tpl | 28 ---- endpoint-exposer/specs/links/refresh.json.tpl | 25 ---- endpoint-exposer/specs/service-spec.json.tpl | 24 +-- .../templates/istio/empty-internal.yaml.tpl | 25 ---- endpoint-exposer/workflows/istio/create.yaml | 20 +-- endpoint-exposer/workflows/istio/link.yaml | 6 - endpoint-exposer/workflows/istio/unlink.yaml | 6 - endpoint-exposer/workflows/istio/update.yaml | 41 +----- 16 files changed, 74 insertions(+), 327 deletions(-) delete mode 100755 endpoint-exposer/scripts/common/echo delete mode 100644 endpoint-exposer/specs/link-spec.json.tpl delete mode 100644 endpoint-exposer/specs/links/refresh.json.tpl delete mode 100644 endpoint-exposer/templates/istio/empty-internal.yaml.tpl delete mode 100644 endpoint-exposer/workflows/istio/link.yaml delete mode 100644 endpoint-exposer/workflows/istio/unlink.yaml diff --git a/endpoint-exposer/scripts/common/echo b/endpoint-exposer/scripts/common/echo deleted file mode 100755 index ace31ee..0000000 --- a/endpoint-exposer/scripts/common/echo +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -echo "$MESSAGE" \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index 5303734..9bbb3c1 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -8,20 +8,12 @@ SERVICE_NAME=$(echo "$CONTEXT" | jq -r .service.slug) ACTION_ID=$(echo "$CONTEXT" | jq -r .id) ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) -# Extract domains from parameters +# Extract domain from parameters PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // ""') -PRIVATE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain // ""') # Extract routes array from parameters ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') -# For backward compatibility with link-based workflow -LINK_ID=$(echo "$CONTEXT" | jq -r '.link.id // ""') -LINK_NAME=$(echo "$CONTEXT" | jq -r '.link.slug // ""') -SCOPE_ID=$(echo "$CONTEXT" | jq -r '.tags.scope_id // ""') -SCOPE_SLUG=$(echo "$CONTEXT" | jq -r '.tags.scope // ""') -RULE_PATH=$(echo "$CONTEXT" | jq -r '.link.attributes.path // .parameters.path // ""') - CONTEXT=$(echo "$CONTEXT" | jq \ --arg k8s_namespace "$K8S_NAMESPACE" \ --arg alb_name "$ALB_NAME" \ @@ -36,10 +28,4 @@ export SERVICE_NAME export ACTION_ID export ACTION_NAME export PUBLIC_DOMAIN -export PRIVATE_DOMAIN -export ROUTES_JSON -export LINK_ID -export LINK_NAME -export SCOPE_ID -export SCOPE_SLUG -export RULE_PATH \ No newline at end of file +export ROUTES_JSON \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_ingress b/endpoint-exposer/scripts/istio/build_ingress index ad54d62..cfa9bb8 100644 --- a/endpoint-exposer/scripts/istio/build_ingress +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -2,20 +2,11 @@ set -euo pipefail -# Determine which domain to use and output file based on template -if [[ "$TEMPLATE" == *"internal"* ]]; then - # Internal HTTPRoute - SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.private_domain // .service.attributes.private_domain') - HTTPROUTE_FILE="${OUTPUT_FILE:-$OUTPUT_DIR/httproute-$SERVICE_ID-internal.yaml}" - echo "Creating internal HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" - echo "Output file: $HTTPROUTE_FILE" -else - # Public HTTPRoute - SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') - HTTPROUTE_FILE="${OUTPUT_FILE:-$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml}" - echo "Creating public HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" - echo "Output file: $HTTPROUTE_FILE" -fi +# Determine domain and output file +SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') +HTTPROUTE_FILE="${OUTPUT_FILE:-$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml}" +echo "Creating HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" +echo "Output file: $HTTPROUTE_FILE" CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" diff --git a/endpoint-exposer/scripts/istio/build_ingress_with_rule b/endpoint-exposer/scripts/istio/build_ingress_with_rule index b3d5f1f..bfbf2ff 100755 --- a/endpoint-exposer/scripts/istio/build_ingress_with_rule +++ b/endpoint-exposer/scripts/istio/build_ingress_with_rule @@ -71,7 +71,6 @@ create_http_rule() { local service_json="$2" local blue_green_config="$3" local method="${4:-}" - local visibility="${5:-public}" local service_name local service_port @@ -264,13 +263,12 @@ replace_existing_rule() { local blue_green_config="$4" local rule_index="$5" local method="${6:-}" - local visibility="${7:-public}" local updated_hr local new_rule # Create the new rule - new_rule=$(create_http_rule "$rule_path" "$service_json" "$blue_green_config" "$method" "$visibility") + new_rule=$(create_http_rule "$rule_path" "$service_json" "$blue_green_config" "$method") # Replace the existing rule at the found index updated_hr=$(echo "$hr_yaml" | yq eval -o=json '.' | \ @@ -286,13 +284,12 @@ add_new_rule() { local service_json="$3" local blue_green_config="$4" local method="${5:-}" - local visibility="${6:-public}" local updated_hr local new_rule # Create the new http rule - new_rule=$(create_http_rule "$rule_path" "$service_json" "$blue_green_config" "$method" "$visibility") + new_rule=$(create_http_rule "$rule_path" "$service_json" "$blue_green_config" "$method") # Add the new rule to the existing rules array updated_hr=$(echo "$hr_yaml" | yq eval ".spec.rules += [$new_rule]") @@ -307,7 +304,6 @@ echo "SERVICE_ID: $SERVICE_ID" echo "SCOPE_ID: $SCOPE_ID" echo "RULE_PATH: $RULE_PATH" echo "METHOD: ${METHOD:-not specified}" -echo "VISIBILITY: ${VISIBILITY:-public}" # Check if HTTPROUTE_FILE is set (called from process_routes) if [[ -n "${HTTPROUTE_FILE:-}" && -f "$HTTPROUTE_FILE" ]]; then @@ -353,7 +349,7 @@ echo "Is HTTPRoute empty? $IS_EMPTY" if [[ "$IS_EMPTY" == "true" ]]; then echo "Case 1: Empty HTTPRoute - replacing fallback rule (index 0) with new rule" - UPDATED_HR=$(replace_existing_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "0" "${METHOD:-}" "${VISIBILITY:-public}") + UPDATED_HR=$(replace_existing_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "0" "${METHOD:-}") echo "$UPDATED_HR" | yq "." > "$HTTPROUTE_FILE" else # Detect the converted path value to match against existing rules @@ -366,12 +362,12 @@ else # if there is a rule for the path we replace it if [[ "$RULE_INDEX" != "-1" ]]; then echo "Case 2: Replacing existing rule at index $RULE_INDEX" - UPDATED_HR=$(replace_existing_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "$RULE_INDEX" "${METHOD:-}" "${VISIBILITY:-public}") + UPDATED_HR=$(replace_existing_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "$RULE_INDEX" "${METHOD:-}") echo "$UPDATED_HR" | yq "." > "$HTTPROUTE_FILE" else # if there is no rule for the path we add a new one echo "Case 3: Adding new rule" - UPDATED_HR=$(add_new_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "${METHOD:-}" "${VISIBILITY:-public}") + UPDATED_HR=$(add_new_rule "$HTTPROUTE" "$RULE_PATH" "$SERVICE" "$BLUE_GREEN_CONFIG" "${METHOD:-}") # Debug: Check if hostnames and parentRefs are present before saving echo "DEBUG: Checking HTTPRoute before saving..." diff --git a/endpoint-exposer/scripts/istio/clean_httproute_rules b/endpoint-exposer/scripts/istio/clean_httproute_rules index ae6087c..85179f6 100755 --- a/endpoint-exposer/scripts/istio/clean_httproute_rules +++ b/endpoint-exposer/scripts/istio/clean_httproute_rules @@ -2,14 +2,6 @@ set -euo pipefail -# If HTTPROUTE_FILE_VAR is provided, use it to get the file path from that variable -if [[ -n "${HTTPROUTE_FILE_VAR:-}" ]]; then - # Use indirect variable expansion safely - if [[ -n "${!HTTPROUTE_FILE_VAR:-}" ]]; then - HTTPROUTE_FILE="${!HTTPROUTE_FILE_VAR}" - fi -fi - echo "=== Cleaning HTTPRoute rules ===" echo "HTTPROUTE_FILE: $HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/fetch_httproute b/endpoint-exposer/scripts/istio/fetch_httproute index a30d3c7..1846bef 100755 --- a/endpoint-exposer/scripts/istio/fetch_httproute +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -7,25 +7,16 @@ echo "SERVICE_ID: $SERVICE_ID" echo "SERVICE_NAME: $SERVICE_NAME" echo "K8S_NAMESPACE: $K8S_NAMESPACE" -# Determine visibility (default to public if not set) -VISIBILITY="${VISIBILITY:-public}" -echo "Visibility: $VISIBILITY" - # Build the HTTPRoute name using the same pattern as the template -if [[ "$VISIBILITY" == "internal" ]]; then - HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route-internal" - TEMPLATE="$SERVICE_PATH/templates/istio/empty-internal.yaml.tpl" -else - HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" - TEMPLATE="$SERVICE_PATH/templates/istio/empty.yaml.tpl" -fi +HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" +TEMPLATE="$SERVICE_PATH/templates/istio/empty.yaml.tpl" echo "HTTPRoute name: $HTTPROUTE_NAME" -# Define output file - use OUTPUT_FILE if set, otherwise default based on visibility +# Define output file if [[ -n "${OUTPUT_FILE:-}" ]]; then HTTPROUTE_FILE="$OUTPUT_FILE" else - HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-$VISIBILITY.yaml" + HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" fi # Create output directory if it doesn't exist diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes index 44bb233..670d1a7 100755 --- a/endpoint-exposer/scripts/istio/process_routes +++ b/endpoint-exposer/scripts/istio/process_routes @@ -44,110 +44,67 @@ SORTED_ROUTES=$(echo "$ROUTES_JSON" | jq 'sort_by( ROUTES_JSON="$SORTED_ROUTES" echo "Routes sorted by specificity (Exact > RegularExpression > PathPrefix)" -# Separate routes by visibility -echo "" -echo "=== Separating routes by visibility ===" -PUBLIC_ROUTES=$(echo "$ROUTES_JSON" | jq '[.[] | select(.visibility == "public")]') -INTERNAL_ROUTES=$(echo "$ROUTES_JSON" | jq '[.[] | select(.visibility == "internal")]') - -NUM_PUBLIC=$(echo "$PUBLIC_ROUTES" | jq 'length') -NUM_INTERNAL=$(echo "$INTERNAL_ROUTES" | jq 'length') +# Define HTTPRoute file +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" -echo "Public routes: $NUM_PUBLIC" -echo "Internal routes: $NUM_INTERNAL" +export HTTPROUTE_FILE +echo "HTTPRoute file: $HTTPROUTE_FILE" +echo "HTTPRoute name: $HTTPROUTE_NAME" -# Function to process routes for a specific visibility -process_routes_for_visibility() { - local visibility="$1" - local routes_json="$2" - local num_routes="$3" - - if [[ "$num_routes" -eq 0 ]]; then - echo "No $visibility routes to process" - return 0 - fi +# Read the HTTPRoute from the file created in the previous step +if [[ ! -f "$HTTPROUTE_FILE" ]]; then + echo "ERROR: HTTPRoute file not found at $HTTPROUTE_FILE" + exit 1 +fi +# Process each route +for ((i=0; i Date: Fri, 9 Jan 2026 12:48:24 -0300 Subject: [PATCH 021/101] feat: wip authz plugins --- .../plugins/authorization-policy/README.md | 241 ++++++++++++++++++ .../plugins/authorization-policy/generate | 145 +++++++++++ .../authorization-policy/template.yaml.tpl | 38 +++ endpoint-exposer/specs/service-spec.json.tpl | 45 ++++ .../templates/istio/opa-authz.yaml | 196 ++++++++++++++ .../templates/istio/policies.yaml | 50 ++++ endpoint-exposer/workflows/istio/create.yaml | 9 + endpoint-exposer/workflows/istio/update.yaml | 9 + 8 files changed, 733 insertions(+) create mode 100644 endpoint-exposer/plugins/authorization-policy/README.md create mode 100755 endpoint-exposer/plugins/authorization-policy/generate create mode 100644 endpoint-exposer/plugins/authorization-policy/template.yaml.tpl create mode 100644 endpoint-exposer/templates/istio/opa-authz.yaml create mode 100644 endpoint-exposer/templates/istio/policies.yaml diff --git a/endpoint-exposer/plugins/authorization-policy/README.md b/endpoint-exposer/plugins/authorization-policy/README.md new file mode 100644 index 0000000..9bb3e47 --- /dev/null +++ b/endpoint-exposer/plugins/authorization-policy/README.md @@ -0,0 +1,241 @@ +# Authorization Policy Plugin + +Este plugin genera recursos `AuthorizationPolicy` de Istio que delegan la autorización a OPA (Open Policy Agent) para validar JWT tokens. + +## Descripción + +El plugin procesa las rutas configuradas en el HTTPRoute y genera un AuthorizationPolicy que: +- Se aplica al Gateway (no requiere sidecar en los pods de aplicación) +- Delega la validación a OPA usando `action: CUSTOM` con provider `opa-ext-authz` +- Respeta los mismos tipos de matching de path (Exact, PathPrefix, RegularExpression) +- OPA valida JWT tokens según la policy configurada en Rego + +## Configuración + +La configuración se agrega en el service specification bajo `parameters.authorization`: + +```json +{ + "publicDomain": "api.example.com", + "authorization": { + "enabled": true + }, + "routes": [ + { + "method": "GET", + "path": "/api/users", + "scope": "user-service" + }, + { + "method": "POST", + "path": "/api/users/:id", + "scope": "user-service" + } + ] +} +``` + +### Parámetros + +| Parámetro | Tipo | Requerido | Default | Descripción | +|-----------|------|-----------|---------|-------------| +| `enabled` | boolean | No | `false` | Activa o desactiva la validación JWT via OPA para todas las rutas del servicio | + +## Funcionamiento + +1. **Deshabilitado por defecto**: Si `authorization.enabled` no es `true`, el script termina sin generar nada. + +2. **Procesamiento de rutas**: Para cada ruta en `routes`: + - Detecta el tipo de path (Exact, PathPrefix, RegularExpression) + - Convierte paths con parámetros (`:id`) a prefix matching + - Genera una regla de autorización que coincide con el path y método + +3. **Delegación a OPA**: El AuthorizationPolicy usa `action: CUSTOM` con provider `opa-ext-authz`: + ```yaml + spec: + action: CUSTOM + provider: + name: opa-ext-authz + rules: + - to: + - operation: + hosts: ["api.example.com"] + methods: ["GET"] + paths: ["/api/users"] + ``` + +4. **Validación JWT en OPA**: Cuando una request coincide con las reglas, OPA evalúa: + - Si el token JWT es válido (firma, emisor, expiración) + - Si tiene los claims requeridos (configurados en la policy Rego) + - Si el método HTTP está permitido + +5. **Salida**: Genera `authorization-policy-{SERVICE_ID}.yaml` en el `OUTPUT_DIR` y se aplica al namespace `gateways` + +## Tipos de Path Matching + +### Exact Match +```json +{ + "path": "/api/users", + "method": "GET" +} +``` +Genera: +```yaml +paths: +- "/api/users" +methods: ["GET"] +``` + +### Path with Parameters (RegularExpression) +```json +{ + "path": "/api/users/:id", + "method": "GET" +} +``` +Genera: +```yaml +# Note: Istio AuthorizationPolicy no soporta regex en paths +# Se usa prefix matching como fallback +paths: +- "/api/users/*" +methods: ["GET"] +``` + +### Wildcard Prefix +```json +{ + "path": "/api/*", + "method": "GET" +} +``` +Genera: +```yaml +paths: +- "/api/*" +methods: ["GET"] +``` + +## Ejemplo Completo + +### Configuración del Servicio +```json +{ + "publicDomain": "api.example.com", + "authorization": { + "enabled": true + }, + "routes": [ + { + "method": "GET", + "path": "/api/config", + "scope": "config-service" + }, + { + "method": "GET", + "path": "/api/leaderboard", + "scope": "leaderboard-service" + }, + { + "method": "POST", + "path": "/api/admin/:resource", + "scope": "admin-service" + } + ] +} +``` + +### AuthorizationPolicy Generado +```yaml +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: my-service-123-authz + namespace: gateways + labels: + app.kubernetes.io/name: my-service + nullplatform.com/service-id: "123" + nullplatform.com/managed-by: endpoint-exposer +spec: + # Apply to the Gateway workload (not service pods) + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-public + action: CUSTOM + provider: + name: opa-ext-authz + rules: + # Rule for: GET /api/config (scope: config-service) + - to: + - operation: + hosts: ["api.example.com"] + methods: ["GET"] + paths: + - "/api/config" + + # Rule for: GET /api/leaderboard (scope: leaderboard-service) + - to: + - operation: + hosts: ["api.example.com"] + methods: ["GET"] + paths: + - "/api/leaderboard" + + # Rule for: POST /api/admin/:resource (scope: admin-service) + - to: + - operation: + hosts: ["api.example.com"] + methods: ["POST"] + paths: + - "/api/admin/*" +``` + +## Testing + +### Generar un JWT de prueba + +Puedes usar el JWT de ejemplo de Istio (configurado en la policy OPA): + +```bash +TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6IkRIRmJwb0lVcXJZOHQyenBBMnFYZkNtcjVWTzVaRXI0UnpIVV8tZW52dlEiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjQ2ODU5ODk3MDAsImZvbyI6ImJhciIsImlhdCI6MTUzMjM4OTcwMCwiaXNzIjoidGVzdGluZ0BzZWN1cmUuaXN0aW8uaW8iLCJzdWIiOiJ0ZXN0aW5nQHNlY3VyZS5pc3Rpby5pbyJ9.CfNW-W_F_zdHuqIzOWOQW7gJQqZzHk3qVHlKx_W4mbQ7QH0pKm9RG6zKnRW-3cjKrxGUWfPwFODNNvqHVNEKCNMDfHO_SbxWc4v2lmYu3qHx_VGQzHU-xVJGqKY4eG4Z0-kJqBGiJPx-kHXW-P3gKcGaZyUQMPZQKIIqLCKWqLGqGcHZGzQ8YZr_Q1F_F6qL_KX6E5_XUZzqCjCKLqvKvZqZWqGqhZYqGkKYqZqZYqGqKY" +``` + +### Sin token JWT +```bash +curl -X GET https://api.example.com/api/config +# Response: 401 Unauthorized +# Body: "Authorization header required" +``` + +### Con token JWT válido +```bash +curl -X GET https://api.example.com/api/config \ + -H "Authorization: Bearer $TOKEN" +# Response: 200 OK (si el servicio responde) +# Headers inyectados por OPA: x-user-id, x-validated-by +``` + +### Con token JWT inválido +```bash +curl -X GET https://api.example.com/api/config \ + -H "Authorization: Bearer invalid-token" +# Response: 401 Unauthorized +# Body: "Invalid or expired token" +``` + +## Limitaciones + +1. **Regex no soportado**: Istio AuthorizationPolicy no soporta expresiones regulares en `paths`. Paths con parámetros (`:id`) se convierten a prefix matching (`/api/users/*`). + +2. **Policy global por servicio**: La policy se aplica a nivel Gateway para todas las rutas del servicio. No puedes tener rutas con auth y rutas públicas en el mismo servicio. + +3. **Requiere OPA configurado**: Necesitas tener OPA desplegado con el extension provider `opa-ext-authz` configurado en el mesh config de Istio. + +4. **No requiere sidecar**: Esta solución aplica autorización a nivel Gateway, por lo que NO necesitas inyectar Istio sidecar en los pods de aplicación. + +## Archivos + +- `template.yaml.tpl`: Template de gomplate para el recurso AuthorizationPolicy +- `generate`: Script bash que procesa routes y genera el YAML +- `README.md`: Esta documentación diff --git a/endpoint-exposer/plugins/authorization-policy/generate b/endpoint-exposer/plugins/authorization-policy/generate new file mode 100755 index 0000000..981d18b --- /dev/null +++ b/endpoint-exposer/plugins/authorization-policy/generate @@ -0,0 +1,145 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== Generating Authorization Policy ===" + +# Check if plugin is enabled +AUTHZ_ENABLED=$(echo "$CONTEXT" | jq -r '.parameters.authorization.enabled // false') + +if [[ "$AUTHZ_ENABLED" != "true" ]]; then + echo "Authorization Policy plugin is disabled, skipping..." + exit 0 +fi + +echo "Authorization enabled - JWT validation via OPA will be applied to all routes" + +# Extract application ID from context +APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.service.dimensions.application_id // ""') + +if [[ -z "$APPLICATION_ID" || "$APPLICATION_ID" == "null" ]]; then + echo "ERROR: application_id not found in service dimensions" + exit 1 +fi + +# Helper function to detect path type (same logic as build_ingress_with_rule) +detect_path_type() { + local path="$1" + + if [[ "$path" == *":"* ]]; then + # Contains parameter like :id -> RegularExpression + # Convert /api/users/:id to /api/users/[^/]+ + local converted_path="${path//:[^\/]*/[^/]+}" + # Extract prefix for fallback + local prefix="${path%%:*}" + echo "RegularExpression:$converted_path:$prefix" + elif [[ "$path" == *"*" ]]; then + # Contains wildcard -> PathPrefix + # Remove trailing /* or * + local clean_path="${path%/\*}" + clean_path="${clean_path%\*}" + echo "PathPrefix:$clean_path" + else + # No special characters -> Exact + echo "Exact:$path" + fi +} + +# Build authorization rules array +AUTHZ_RULES="[]" + +# Process each route and create authorization rule +NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') +echo "Processing $NUM_ROUTES routes for authorization..." + +for ((i=0; i "$CONTEXT_PATH" + +# Generate YAML from template +TEMPLATE="${TEMPLATE:-$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl}" +AUTHZ_POLICY_FILE="$OUTPUT_DIR/authorization-policy-$SERVICE_ID.yaml" + +echo "Generating AuthorizationPolicy YAML..." +gomplate -c .="$CONTEXT_PATH" \ + -f "$TEMPLATE" \ + -o "$AUTHZ_POLICY_FILE" + +rm "$CONTEXT_PATH" + +echo "✅ Authorization Policy generated: $AUTHZ_POLICY_FILE" +export AUTHZ_POLICY_FILE diff --git a/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl new file mode 100644 index 0000000..0e77b27 --- /dev/null +++ b/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl @@ -0,0 +1,38 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: {{ .service_name }}-{{ .service_id }}-authz + namespace: gateways + labels: + app.kubernetes.io/name: {{ .service_name }} + nullplatform.com/service-id: "{{ .service_id }}" + nullplatform.com/managed-by: endpoint-exposer +spec: + # Apply to the Gateway workload (not service pods) + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-public + action: CUSTOM + provider: + name: opa-ext-authz + rules: + {{- range .rules }} + # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) + - to: + - operation: + hosts: ["{{ $.public_domain }}"] + {{- if ne .method "" }} + methods: ["{{ .method }}"] + {{- end }} + paths: + {{- if eq .path_type "Exact" }} + - "{{ .path }}" + {{- else if eq .path_type "PathPrefix" }} + - "{{ .path }}*" + {{- else if eq .path_type "RegularExpression" }} + # Regex: {{ .path_regex }} + # Note: Istio AuthorizationPolicy doesn't support regex in paths + # Using prefix matching as fallback for regex patterns + - "{{ .path_prefix }}*" + {{- end }} + {{- end }} diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 343245c..0f344c3 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -17,6 +17,38 @@ } ] }, + { + "type": "Group", + "label": "Authorization", + "elements": [ + { + "type": "Control", + "scope": "#/properties/authorization/properties/enabled" + }, + { + "type": "Control", + "scope": "#/properties/authorization/properties/headerName", + "rule": { + "effect": "SHOW", + "condition": { + "scope": "#/properties/authorization/properties/enabled", + "schema": { "const": true } + } + } + }, + { + "type": "Control", + "scope": "#/properties/authorization/properties/allowedValues", + "rule": { + "effect": "SHOW", + "condition": { + "scope": "#/properties/authorization/properties/enabled", + "schema": { "const": true } + } + } + } + ] + }, { "type": "Group", "label": "Routes", @@ -58,6 +90,19 @@ "type": "string", "editableOn": ["create", "update"] }, + "authorization": { + "type": "object", + "title": "Authorization", + "description": "JWT authorization policy configuration using OPA", + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Authorization Policy", + "description": "Enable JWT validation via OPA for all routes in this service", + "default": false + } + } + }, "routes": { "items": { "properties": { diff --git a/endpoint-exposer/templates/istio/opa-authz.yaml b/endpoint-exposer/templates/istio/opa-authz.yaml new file mode 100644 index 0000000..53abe56 --- /dev/null +++ b/endpoint-exposer/templates/istio/opa-authz.yaml @@ -0,0 +1,196 @@ +# ============================================================================= +# OPA External Authorization usando AuthorizationPolicy CUSTOM +# Esta es la forma RECOMENDADA por Istio +# +# IMPORTANTE: Requiere configurar el extensionProvider en el mesh config +# Ver instrucciones al final del archivo +# ============================================================================= +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: opa-jwt-policies + namespace: istio-system + labels: + app: opa-ext-authz +data: + policy.rego: | + package envoy.authz + import future.keywords.if + import future.keywords.in + # Default: denegar + default allow := { + "allowed": false, + "http_status": 401, + "body": "Authorization required" + } + # Sin token + allow := { + "allowed": false, + "http_status": 401, + "body": "Authorization header required" + } if { + not bearer_token + } + # Token inválido + allow := { + "allowed": false, + "http_status": 401, + "body": "Invalid or expired token" + } if { + bearer_token + not is_token_valid + } + # Token válido pero sin claims + allow := { + "allowed": false, + "http_status": 403, + "body": "Missing required claim: foo=bar" + } if { + is_token_valid + not has_required_claims + } + # Método no permitido + allow := { + "allowed": false, + "http_status": 405, + "body": "Method not allowed" + } if { + is_token_valid + has_required_claims + not is_valid_method + } + # Todo válido - PERMITIR + allow := { + "allowed": true, + "headers": { + "x-user-id": token.payload.sub, + "x-validated-by": "opa-ext-authz" + } + } if { + is_token_valid + has_required_claims + is_valid_method + } + # ========================================================================= + # HELPERS + # ========================================================================= + is_valid_method if { + input.attributes.request.http.method in ["GET", "POST"] + } + is_token_valid if { + token.valid + token.payload.iss == "testing@secure.istio.io" + now := time.now_ns() / 1000000000 + token.payload.exp > now + } + has_required_claims if { + token.payload.foo == "bar" + } + token := {"valid": valid, "payload": payload} if { + [valid, _, payload] := io.jwt.decode_verify(bearer_token, { + "cert": data.keys.jwks, + "iss": "testing@secure.istio.io" + }) + } + token := {"valid": false, "payload": {}} if { + not bearer_token + } + bearer_token := t if { + auth_header := input.attributes.request.http.headers.authorization + startswith(auth_header, "Bearer ") + t := substring(auth_header, 7, -1) + } + data.json: | + { + "keys": { + "jwks": "{\"keys\":[{\"e\":\"AQAB\",\"kid\":\"DHFbpoIUqrY8t2zpA2qXfCmr5VO5ZEr4RzHU_-envvQ\",\"kty\":\"RSA\",\"n\":\"xAE7eB6qugXyCAG3yhh7pkDkT65pHymX-P7KfIupjf59vsdo91bSP9C8H07pSAGQO1MV_xFj9VswgsCg4R6otmg5PV2He95lZdHtOcU5DXIg_pbhLdKXbi66GlVeK6ABZOUW3WYtnNHD-91gVuoeJT_DwtGGcp4ignkgXfkiEm4sw-4sfb4qdt5oLbyVpmW6x9cfa7vs2WTfURiCrBoUqgBo_-4WTiULmmHSGZHOjzwa8WtrtOQGsAFjIbno85jp6MnGGGZPYZbDAa_b3y5u-YpW7ypZrvD8BgtKVjgtQgZhLAGezMt0ua3DRrWnKqTZ0BJ_EyxOGuHJrLsn00fnMQ\"}]}" + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: opa-ext-authz + namespace: istio-system + labels: + app: opa-ext-authz +spec: + replicas: 2 + selector: + matchLabels: + app: opa-ext-authz + template: + metadata: + labels: + app: opa-ext-authz + annotations: + sidecar.istio.io/inject: "false" + spec: + containers: + - name: opa + image: openpolicyagent/opa:0.60.0-envoy + args: + - "run" + - "--server" + - "--addr=0.0.0.0:8181" + - "--diagnostic-addr=0.0.0.0:8282" + - "--set=plugins.envoy_ext_authz_grpc.addr=:9191" + - "--set=plugins.envoy_ext_authz_grpc.path=envoy/authz/allow" + - "--set=decision_logs.console=true" + - "--ignore=.*" + - "/policies/policy.rego" + - "/policies/data.json" + ports: + - name: grpc + containerPort: 9191 + - name: http + containerPort: 8181 + - name: diagnostics + containerPort: 8282 + volumeMounts: + - name: policies + mountPath: /policies + readOnly: true + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + livenessProbe: + httpGet: + path: /health?plugins + port: 8282 + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health?plugins + port: 8282 + initialDelaySeconds: 5 + periodSeconds: 5 + volumes: + - name: policies + configMap: + name: opa-jwt-policies +--- +apiVersion: v1 +kind: Service +metadata: + name: opa-ext-authz + namespace: istio-system + labels: + app: opa-ext-authz +spec: + selector: + app: opa-ext-authz + ports: + - name: grpc + port: 9191 + targetPort: 9191 + - name: http + port: 8181 + targetPort: 8181 + type: ClusterIP \ No newline at end of file diff --git a/endpoint-exposer/templates/istio/policies.yaml b/endpoint-exposer/templates/istio/policies.yaml new file mode 100644 index 0000000..e312861 --- /dev/null +++ b/endpoint-exposer/templates/istio/policies.yaml @@ -0,0 +1,50 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: birds-35f2062a-8cbf-447c-bc4e-d2d0b8577e75-authz + namespace: nullplatform + labels: + app.kubernetes.io/name: birds + nullplatform.com/service-id: "35f2062a-8cbf-447c-bc4e-d2d0b8577e75" + nullplatform.com/managed-by: endpoint-exposer +spec: + selector: + matchLabels: + nullplatform: "true" + action: CUSTOM + provider: + name: opa-ext-authz + rules: + # Aplicar OPA solo a /api/config + - to: + - operation: + hosts: + - birds.edenred.nullimplementation.com + methods: + - GET + paths: + - /api/config +--- +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: birds-leaderboard-allow + namespace: nullplatform + labels: + app.kubernetes.io/name: birds + nullplatform.com/managed-by: endpoint-exposer +spec: + selector: + matchLabels: + nullplatform: "true" + action: ALLOW + rules: + # Permitir /api/leaderboard sin validación + - to: + - operation: + hosts: + - birds.edenred.nullimplementation.com + methods: + - GET + paths: + - /api/leaderboard \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index a104121..e0bd928 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -38,6 +38,15 @@ steps: - name: HTTPROUTE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "generate authorization policy" + type: script + file: "$SERVICE_PATH/plugins/authorization-policy/generate" + configuration: + TEMPLATE: "$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" + output: + - name: AUTHZ_POLICY_FILE + type: file + file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 7d12cd8..2df9c91 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -45,6 +45,15 @@ steps: - name: HTTPROUTE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "generate authorization policy" + type: script + file: "$SERVICE_PATH/plugins/authorization-policy/generate" + configuration: + TEMPLATE: "$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" + output: + - name: AUTHZ_POLICY_FILE + type: file + file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From 4eb2a97e13cc3d63919db65c1b1f44fa000842a4 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 9 Jan 2026 12:59:12 -0300 Subject: [PATCH 022/101] feat: public and private domains --- endpoint-exposer/scripts/istio/build_context | 14 ++++- .../scripts/istio/build_private_httproute | 63 +++++++++++++++++++ .../scripts/istio/build_public_httproute | 63 +++++++++++++++++++ endpoint-exposer/scripts/istio/process_routes | 7 ++- endpoint-exposer/specs/service-spec.json.tpl | 29 ++++++++- .../istio/httproute-private.yaml.tpl | 26 ++++++++ .../templates/istio/httproute-public.yaml.tpl | 26 ++++++++ endpoint-exposer/workflows/istio/create.yaml | 25 +++++--- 8 files changed, 236 insertions(+), 17 deletions(-) create mode 100755 endpoint-exposer/scripts/istio/build_private_httproute create mode 100755 endpoint-exposer/scripts/istio/build_public_httproute create mode 100644 endpoint-exposer/templates/istio/httproute-private.yaml.tpl create mode 100644 endpoint-exposer/templates/istio/httproute-public.yaml.tpl diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index 9bbb3c1..3220be0 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -8,12 +8,17 @@ SERVICE_NAME=$(echo "$CONTEXT" | jq -r .service.slug) ACTION_ID=$(echo "$CONTEXT" | jq -r .id) ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) -# Extract domain from parameters -PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // ""') +# Extract domains from parameters +PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.publicDomain // .parameters.public_domain // ""') +PRIVATE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.privateDomain // .parameters.private_domain // ""') # Extract routes array from parameters ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') +# Split routes by visibility +PUBLIC_ROUTES_JSON=$(echo "$ROUTES_JSON" | jq -c '[.[] | select(.visibility == "public" or .visibility == null)]') +PRIVATE_ROUTES_JSON=$(echo "$ROUTES_JSON" | jq -c '[.[] | select(.visibility == "private")]') + CONTEXT=$(echo "$CONTEXT" | jq \ --arg k8s_namespace "$K8S_NAMESPACE" \ --arg alb_name "$ALB_NAME" \ @@ -28,4 +33,7 @@ export SERVICE_NAME export ACTION_ID export ACTION_NAME export PUBLIC_DOMAIN -export ROUTES_JSON \ No newline at end of file +export PRIVATE_DOMAIN +export ROUTES_JSON +export PUBLIC_ROUTES_JSON +export PRIVATE_ROUTES_JSON \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_private_httproute b/endpoint-exposer/scripts/istio/build_private_httproute new file mode 100755 index 0000000..8125ae9 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_private_httproute @@ -0,0 +1,63 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== Building Private HTTPRoute ===" + +# Check if we have private routes and private domain +NUM_ROUTES=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') +echo "Number of private routes: $NUM_ROUTES" + +if [[ "$NUM_ROUTES" -eq 0 ]] || [[ -z "$PRIVATE_DOMAIN" ]] || [[ "$PRIVATE_DOMAIN" == "null" ]]; then + echo "No private routes or no private domain configured, skipping..." + # Create empty file to satisfy workflow + touch "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + exit 0 +fi + +echo "Private Domain: $PRIVATE_DOMAIN" + +# Create empty HTTPRoute from template +TEMPLATE="${TEMPLATE:-$SERVICE_PATH/templates/istio/httproute-private.yaml.tpl}" +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + +# Build context for template +HTTPROUTE_CONTEXT=$(jq -n \ + --arg service_name "$SERVICE_NAME" \ + --arg service_id "$SERVICE_ID" \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg private_domain "$PRIVATE_DOMAIN" \ + '{ + service_name: $service_name, + service_id: $service_id, + k8s_namespace: $k8s_namespace, + private_domain: $private_domain, + gateway_name: "gateway-private", + gateway_namespace: "gateways" + }') + +CONTEXT_PATH="$OUTPUT_DIR/httproute-private-context-$SERVICE_ID.json" +echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" + +echo "Generating HTTPRoute from template: $TEMPLATE" +gomplate -c .="$CONTEXT_PATH" \ + -f "$TEMPLATE" \ + -o "$HTTPROUTE_FILE" + +rm "$CONTEXT_PATH" + +# Now process routes using the existing logic +# Temporarily override ROUTES_JSON with private routes only +ORIGINAL_ROUTES_JSON="$ROUTES_JSON" +export ROUTES_JSON="$PRIVATE_ROUTES_JSON" + +# Process routes (skip if no routes) +if [[ "$NUM_ROUTES" -gt 0 ]]; then + source "$SERVICE_PATH/scripts/istio/process_routes" +fi + +# Restore original ROUTES_JSON +export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" + +echo "✅ Private HTTPRoute created: $HTTPROUTE_FILE" +export HTTPROUTE_PRIVATE_FILE="$HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/build_public_httproute b/endpoint-exposer/scripts/istio/build_public_httproute new file mode 100755 index 0000000..3e9edea --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_public_httproute @@ -0,0 +1,63 @@ +#!/bin/bash + +set -euo pipefail + +echo "=== Building Public HTTPRoute ===" + +# Check if we have public routes and public domain +NUM_ROUTES=$(echo "$PUBLIC_ROUTES_JSON" | jq 'length') +echo "Number of public routes: $NUM_ROUTES" + +if [[ "$NUM_ROUTES" -eq 0 ]] || [[ -z "$PUBLIC_DOMAIN" ]] || [[ "$PUBLIC_DOMAIN" == "null" ]]; then + echo "No public routes or no public domain configured, skipping..." + # Create empty file to satisfy workflow + touch "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + exit 0 +fi + +echo "Public Domain: $PUBLIC_DOMAIN" + +# Create empty HTTPRoute from template +TEMPLATE="${TEMPLATE:-$SERVICE_PATH/templates/istio/httproute-public.yaml.tpl}" +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + +# Build context for template +HTTPROUTE_CONTEXT=$(jq -n \ + --arg service_name "$SERVICE_NAME" \ + --arg service_id "$SERVICE_ID" \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg public_domain "$PUBLIC_DOMAIN" \ + '{ + service_name: $service_name, + service_id: $service_id, + k8s_namespace: $k8s_namespace, + public_domain: $public_domain, + gateway_name: "gateway-public", + gateway_namespace: "gateways" + }') + +CONTEXT_PATH="$OUTPUT_DIR/httproute-public-context-$SERVICE_ID.json" +echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" + +echo "Generating HTTPRoute from template: $TEMPLATE" +gomplate -c .="$CONTEXT_PATH" \ + -f "$TEMPLATE" \ + -o "$HTTPROUTE_FILE" + +rm "$CONTEXT_PATH" + +# Now process routes using the existing logic +# Temporarily override ROUTES_JSON with public routes only +ORIGINAL_ROUTES_JSON="$ROUTES_JSON" +export ROUTES_JSON="$PUBLIC_ROUTES_JSON" + +# Process routes (skip if no routes) +if [[ "$NUM_ROUTES" -gt 0 ]]; then + source "$SERVICE_PATH/scripts/istio/process_routes" +fi + +# Restore original ROUTES_JSON +export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" + +echo "✅ Public HTTPRoute created: $HTTPROUTE_FILE" +export HTTPROUTE_PUBLIC_FILE="$HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes index 670d1a7..8c5ee72 100755 --- a/endpoint-exposer/scripts/istio/process_routes +++ b/endpoint-exposer/scripts/istio/process_routes @@ -44,8 +44,11 @@ SORTED_ROUTES=$(echo "$ROUTES_JSON" | jq 'sort_by( ROUTES_JSON="$SORTED_ROUTES" echo "Routes sorted by specificity (Exact > RegularExpression > PathPrefix)" -# Define HTTPRoute file -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +# Use existing HTTPROUTE_FILE if set, otherwise default to public +if [[ -z "${HTTPROUTE_FILE:-}" ]]; then + HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +fi + HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" export HTTPROUTE_FILE diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 343245c..f9a0f11 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -9,11 +9,15 @@ "elements": [ { "type": "Group", - "label": "Domain", + "label": "Domains", "elements": [ { "type": "Control", "scope": "#/properties/publicDomain" + }, + { + "type": "Control", + "scope": "#/properties/privateDomain" } ] }, @@ -43,6 +47,11 @@ "type": "Control", "label": "Scope", "scope": "#/items/properties/scope" + }, + { + "type": "Control", + "label": "Visibility", + "scope": "#/items/properties/visibility" } ] }, @@ -56,6 +65,14 @@ "properties": { "publicDomain": { "type": "string", + "title": "Public Domain", + "description": "Domain for public routes (e.g., api.example.com)", + "editableOn": ["create", "update"] + }, + "privateDomain": { + "type": "string", + "title": "Private Domain", + "description": "Domain for private routes (e.g., internal-api.example.com)", "editableOn": ["create", "update"] }, "routes": { @@ -77,12 +94,20 @@ "additionalKeywords": { "enum": "[.scopes[]?.slug]" } + }, + "visibility": { + "type": "string", + "title": "Visibility", + "description": "Route visibility level", + "enum": ["public", "private"], + "default": "public" } }, "required": [ "method", "path", - "scope" + "scope", + "visibility" ], "type": "object" }, diff --git a/endpoint-exposer/templates/istio/httproute-private.yaml.tpl b/endpoint-exposer/templates/istio/httproute-private.yaml.tpl new file mode 100644 index 0000000..fb0b63a --- /dev/null +++ b/endpoint-exposer/templates/istio/httproute-private.yaml.tpl @@ -0,0 +1,26 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .service_name }}-{{ .service_id }}-private + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service_name }} + service_id: {{ .service_id }} +spec: + parentRefs: + - name: {{ .gateway_name }} + namespace: {{ .gateway_namespace }} + group: gateway.networking.k8s.io + kind: Gateway + hostnames: + - {{ .private_domain }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: response-404 + port: 80 + weight: 0 diff --git a/endpoint-exposer/templates/istio/httproute-public.yaml.tpl b/endpoint-exposer/templates/istio/httproute-public.yaml.tpl new file mode 100644 index 0000000..33b3be0 --- /dev/null +++ b/endpoint-exposer/templates/istio/httproute-public.yaml.tpl @@ -0,0 +1,26 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ .service_name }}-{{ .service_id }}-public + namespace: {{ .k8s_namespace }} + labels: + nullplatform: "true" + service: {{ .service_name }} + service_id: {{ .service_id }} +spec: + parentRefs: + - name: {{ .gateway_name }} + namespace: {{ .gateway_namespace }} + group: gateway.networking.k8s.io + kind: Gateway + hostnames: + - {{ .public_domain }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: response-404 + port: 80 + weight: 0 diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index a104121..73d407e 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -21,23 +21,28 @@ steps: type: environment - name: PUBLIC_DOMAIN type: environment + - name: PRIVATE_DOMAIN + type: environment - name: ROUTES_JSON type: environment - - name: "build httproute" - type: script - file: "$SERVICE_PATH/scripts/istio/build_ingress" - configuration: - TEMPLATE: "$SERVICE_PATH/templates/istio/empty.yaml.tpl" - output: - - name: HTTPROUTE_FILE + - name: PUBLIC_ROUTES_JSON type: environment - - name: "process routes" + - name: PRIVATE_ROUTES_JSON + type: environment + - name: "build public httproute" type: script - file: "$SERVICE_PATH/scripts/istio/process_routes" + file: "$SERVICE_PATH/scripts/istio/build_public_httproute" output: - - name: HTTPROUTE_FILE + - name: HTTPROUTE_PUBLIC_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "build private httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/build_private_httproute" + output: + - name: HTTPROUTE_PRIVATE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From 0a5d9fa489f32d36493716fcfd1136f70be8eb67 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 9 Jan 2026 13:03:18 -0300 Subject: [PATCH 023/101] feat: public and private domains --- endpoint-exposer/scripts/istio/build_private_httproute | 4 +--- endpoint-exposer/scripts/istio/build_public_httproute | 4 +--- .../templates/istio/httproute-private.yaml.tpl | 8 ++++---- .../templates/istio/httproute-public.yaml.tpl | 8 ++++---- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_private_httproute b/endpoint-exposer/scripts/istio/build_private_httproute index 8125ae9..9413eaa 100755 --- a/endpoint-exposer/scripts/istio/build_private_httproute +++ b/endpoint-exposer/scripts/istio/build_private_httproute @@ -31,9 +31,7 @@ HTTPROUTE_CONTEXT=$(jq -n \ service_name: $service_name, service_id: $service_id, k8s_namespace: $k8s_namespace, - private_domain: $private_domain, - gateway_name: "gateway-private", - gateway_namespace: "gateways" + private_domain: $private_domain }') CONTEXT_PATH="$OUTPUT_DIR/httproute-private-context-$SERVICE_ID.json" diff --git a/endpoint-exposer/scripts/istio/build_public_httproute b/endpoint-exposer/scripts/istio/build_public_httproute index 3e9edea..b3ba7f8 100755 --- a/endpoint-exposer/scripts/istio/build_public_httproute +++ b/endpoint-exposer/scripts/istio/build_public_httproute @@ -31,9 +31,7 @@ HTTPROUTE_CONTEXT=$(jq -n \ service_name: $service_name, service_id: $service_id, k8s_namespace: $k8s_namespace, - public_domain: $public_domain, - gateway_name: "gateway-public", - gateway_namespace: "gateways" + public_domain: $public_domain }') CONTEXT_PATH="$OUTPUT_DIR/httproute-public-context-$SERVICE_ID.json" diff --git a/endpoint-exposer/templates/istio/httproute-private.yaml.tpl b/endpoint-exposer/templates/istio/httproute-private.yaml.tpl index fb0b63a..86a7f83 100644 --- a/endpoint-exposer/templates/istio/httproute-private.yaml.tpl +++ b/endpoint-exposer/templates/istio/httproute-private.yaml.tpl @@ -1,16 +1,16 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: {{ .service_name }}-{{ .service_id }}-private + name: {{ .service_slug }}-{{ .service_id }}-private namespace: {{ .k8s_namespace }} labels: nullplatform: "true" - service: {{ .service_name }} + service_slug: {{ .service_slug }} service_id: {{ .service_id }} spec: parentRefs: - - name: {{ .gateway_name }} - namespace: {{ .gateway_namespace }} + - name: gateway-private + namespace: gateways group: gateway.networking.k8s.io kind: Gateway hostnames: diff --git a/endpoint-exposer/templates/istio/httproute-public.yaml.tpl b/endpoint-exposer/templates/istio/httproute-public.yaml.tpl index 33b3be0..e659763 100644 --- a/endpoint-exposer/templates/istio/httproute-public.yaml.tpl +++ b/endpoint-exposer/templates/istio/httproute-public.yaml.tpl @@ -1,16 +1,16 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: {{ .service_name }}-{{ .service_id }}-public + name: {{ .service_slug }}-{{ .service_id }}-public namespace: {{ .k8s_namespace }} labels: nullplatform: "true" - service: {{ .service_name }} + service_slug: {{ .service_slug }} service_id: {{ .service_id }} spec: parentRefs: - - name: {{ .gateway_name }} - namespace: {{ .gateway_namespace }} + - name: gateway-public + namespace: gateways group: gateway.networking.k8s.io kind: Gateway hostnames: From 684593fb3bc5c63704759912aa46bce45159cf7f Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 9 Jan 2026 13:25:30 -0300 Subject: [PATCH 024/101] feat: public and private domains --- endpoint-exposer/scripts/istio/build_context | 6 +++--- endpoint-exposer/scripts/istio/build_ingress | 2 +- endpoint-exposer/scripts/istio/build_private_httproute | 4 ++-- endpoint-exposer/scripts/istio/build_public_httproute | 4 ++-- endpoint-exposer/scripts/istio/fetch_httproute | 4 ++-- endpoint-exposer/scripts/istio/process_routes | 4 ++-- endpoint-exposer/workflows/istio/create.yaml | 2 +- endpoint-exposer/workflows/istio/read.yaml | 2 +- endpoint-exposer/workflows/istio/update.yaml | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index 3220be0..0900bc8 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -3,7 +3,7 @@ set -euo pipefail SERVICE_ID=$(echo "$CONTEXT" | jq -r .service.id) -SERVICE_NAME=$(echo "$CONTEXT" | jq -r .service.slug) +SERVICE_SLUG=$(echo "$CONTEXT" | jq -r .service.slug) ACTION_ID=$(echo "$CONTEXT" | jq -r .id) ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) @@ -24,12 +24,12 @@ CONTEXT=$(echo "$CONTEXT" | jq \ --arg alb_name "$ALB_NAME" \ '. + {k8s_namespace: $k8s_namespace, alb_name: $alb_name}') -export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_NAME-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" +export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_SLUG-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" mkdir -p "$OUTPUT_DIR" export SERVICE_ID -export SERVICE_NAME +export SERVICE_SLUG export ACTION_ID export ACTION_NAME export PUBLIC_DOMAIN diff --git a/endpoint-exposer/scripts/istio/build_ingress b/endpoint-exposer/scripts/istio/build_ingress index cfa9bb8..a6df0d3 100644 --- a/endpoint-exposer/scripts/istio/build_ingress +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -5,7 +5,7 @@ set -euo pipefail # Determine domain and output file SERVICE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // .service.attributes.domain') HTTPROUTE_FILE="${OUTPUT_FILE:-$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml}" -echo "Creating HTTPRoute for service $SERVICE_NAME with domain $SERVICE_DOMAIN" +echo "Creating HTTPRoute for service $SERVICE_SLUG with domain $SERVICE_DOMAIN" echo "Output file: $HTTPROUTE_FILE" CONTEXT_PATH="$OUTPUT_DIR/context-$SERVICE_ID.json" diff --git a/endpoint-exposer/scripts/istio/build_private_httproute b/endpoint-exposer/scripts/istio/build_private_httproute index 9413eaa..4bb4576 100755 --- a/endpoint-exposer/scripts/istio/build_private_httproute +++ b/endpoint-exposer/scripts/istio/build_private_httproute @@ -23,12 +23,12 @@ HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" # Build context for template HTTPROUTE_CONTEXT=$(jq -n \ - --arg service_name "$SERVICE_NAME" \ + --arg service_slug "$SERVICE_SLUG" \ --arg service_id "$SERVICE_ID" \ --arg k8s_namespace "$K8S_NAMESPACE" \ --arg private_domain "$PRIVATE_DOMAIN" \ '{ - service_name: $service_name, + service_slug: $service_slug, service_id: $service_id, k8s_namespace: $k8s_namespace, private_domain: $private_domain diff --git a/endpoint-exposer/scripts/istio/build_public_httproute b/endpoint-exposer/scripts/istio/build_public_httproute index b3ba7f8..ba838c9 100755 --- a/endpoint-exposer/scripts/istio/build_public_httproute +++ b/endpoint-exposer/scripts/istio/build_public_httproute @@ -23,12 +23,12 @@ HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" # Build context for template HTTPROUTE_CONTEXT=$(jq -n \ - --arg service_name "$SERVICE_NAME" \ + --arg service_slug "$SERVICE_SLUG" \ --arg service_id "$SERVICE_ID" \ --arg k8s_namespace "$K8S_NAMESPACE" \ --arg public_domain "$PUBLIC_DOMAIN" \ '{ - service_name: $service_name, + service_slug: $service_slug, service_id: $service_id, k8s_namespace: $k8s_namespace, public_domain: $public_domain diff --git a/endpoint-exposer/scripts/istio/fetch_httproute b/endpoint-exposer/scripts/istio/fetch_httproute index 1846bef..dd1800a 100755 --- a/endpoint-exposer/scripts/istio/fetch_httproute +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -4,11 +4,11 @@ set -euo pipefail echo "=== Fetching existing HTTPRoute from Kubernetes ===" echo "SERVICE_ID: $SERVICE_ID" -echo "SERVICE_NAME: $SERVICE_NAME" +echo "SERVICE_SLUG: $SERVICE_SLUG" echo "K8S_NAMESPACE: $K8S_NAMESPACE" # Build the HTTPRoute name using the same pattern as the template -HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" +HTTPROUTE_NAME="${SERVICE_SLUG}-${SERVICE_ID}-route" TEMPLATE="$SERVICE_PATH/templates/istio/empty.yaml.tpl" echo "HTTPRoute name: $HTTPROUTE_NAME" diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes index 8c5ee72..80b78cb 100755 --- a/endpoint-exposer/scripts/istio/process_routes +++ b/endpoint-exposer/scripts/istio/process_routes @@ -4,7 +4,7 @@ set -euo pipefail echo "=== Starting process_routes script ===" echo "SERVICE_ID: $SERVICE_ID" -echo "SERVICE_NAME: $SERVICE_NAME" +echo "SERVICE_SLUG: $SERVICE_SLUG" echo "K8S_NAMESPACE: $K8S_NAMESPACE" echo "ROUTES_JSON: $ROUTES_JSON" @@ -49,7 +49,7 @@ if [[ -z "${HTTPROUTE_FILE:-}" ]]; then HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" fi -HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" +HTTPROUTE_NAME="${SERVICE_SLUG}-${SERVICE_ID}-route" export HTTPROUTE_FILE echo "HTTPRoute file: $HTTPROUTE_FILE" diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 73d407e..294f7b6 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -13,7 +13,7 @@ steps: output: - name: SERVICE_ID type: environment - - name: SERVICE_NAME + - name: SERVICE_SLUG type: environment - name: ACTION_ID type: environment diff --git a/endpoint-exposer/workflows/istio/read.yaml b/endpoint-exposer/workflows/istio/read.yaml index 5251e42..d9ee607 100644 --- a/endpoint-exposer/workflows/istio/read.yaml +++ b/endpoint-exposer/workflows/istio/read.yaml @@ -13,7 +13,7 @@ steps: output: - name: SERVICE_ID type: environment - - name: SERVICE_NAME + - name: SERVICE_SLUG type: environment - name: ACTION_ID type: environment diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 7d12cd8..3354f0d 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -13,7 +13,7 @@ steps: output: - name: SERVICE_ID type: environment - - name: SERVICE_NAME + - name: SERVICE_SLUG type: environment - name: ACTION_ID type: environment From be6025036a26ef2aca099803ab27f8776cfd5ea1 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 9 Jan 2026 13:50:25 -0300 Subject: [PATCH 025/101] fix: private httproute --- .../scripts/istio/build_private_httproute | 17 ++++++++++++----- .../scripts/istio/build_public_httproute | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_private_httproute b/endpoint-exposer/scripts/istio/build_private_httproute index 4bb4576..a7b249c 100755 --- a/endpoint-exposer/scripts/istio/build_private_httproute +++ b/endpoint-exposer/scripts/istio/build_private_httproute @@ -7,18 +7,25 @@ echo "=== Building Private HTTPRoute ===" # Check if we have private routes and private domain NUM_ROUTES=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') echo "Number of private routes: $NUM_ROUTES" +echo "Private Domain: '$PRIVATE_DOMAIN'" -if [[ "$NUM_ROUTES" -eq 0 ]] || [[ -z "$PRIVATE_DOMAIN" ]] || [[ "$PRIVATE_DOMAIN" == "null" ]]; then - echo "No private routes or no private domain configured, skipping..." - # Create empty file to satisfy workflow +# Skip if no routes +if [[ "$NUM_ROUTES" -eq 0 ]]; then + echo "No private routes configured, skipping..." touch "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" exit 0 fi -echo "Private Domain: $PRIVATE_DOMAIN" +# Skip if domain is empty/null +if [[ -z "$PRIVATE_DOMAIN" ]] || [[ "$PRIVATE_DOMAIN" == "null" ]] || [[ "$PRIVATE_DOMAIN" == "\"null\"" ]]; then + echo "WARNING: Private routes exist but no private domain configured!" + echo "Skipping private HTTPRoute generation..." + touch "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + exit 0 +fi # Create empty HTTPRoute from template -TEMPLATE="${TEMPLATE:-$SERVICE_PATH/templates/istio/httproute-private.yaml.tpl}" +TEMPLATE="$SERVICE_PATH/templates/istio/httproute-private.yaml.tpl" HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" # Build context for template diff --git a/endpoint-exposer/scripts/istio/build_public_httproute b/endpoint-exposer/scripts/istio/build_public_httproute index ba838c9..dada905 100755 --- a/endpoint-exposer/scripts/istio/build_public_httproute +++ b/endpoint-exposer/scripts/istio/build_public_httproute @@ -18,7 +18,7 @@ fi echo "Public Domain: $PUBLIC_DOMAIN" # Create empty HTTPRoute from template -TEMPLATE="${TEMPLATE:-$SERVICE_PATH/templates/istio/httproute-public.yaml.tpl}" +TEMPLATE="$SERVICE_PATH/templates/istio/httproute-public.yaml.tpl" HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" # Build context for template From 9215168788bce86b92bde6696d9e47990b1063ea Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 9 Jan 2026 17:17:29 -0300 Subject: [PATCH 026/101] fix: update flow --- endpoint-exposer/specs/service-spec.json.tpl | 12 ++++++-- endpoint-exposer/workflows/istio/update.yaml | 32 +++++++++----------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index f9a0f11..2ac6916 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -66,13 +66,21 @@ "publicDomain": { "type": "string", "title": "Public Domain", - "description": "Domain for public routes (e.g., api.example.com)", + "description": "Domain for public routes", + "enum": [ + "birds.edenred.nullimplementation.com", + "api.edenred.nullimplementation.com" + ], "editableOn": ["create", "update"] }, "privateDomain": { "type": "string", "title": "Private Domain", - "description": "Domain for private routes (e.g., internal-api.example.com)", + "description": "Domain for private routes", + "enum": [ + "birds-private.edenred.nullimplementation.com", + "api-private.edenred.nullimplementation.com" + ], "editableOn": ["create", "update"] }, "routes": { diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 3354f0d..bdfa0c3 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -21,30 +21,28 @@ steps: type: environment - name: PUBLIC_DOMAIN type: environment + - name: PRIVATE_DOMAIN + type: environment - name: ROUTES_JSON type: environment - - name: "fetch existing httproute" - type: script - file: "$SERVICE_PATH/scripts/istio/fetch_httproute" - output: - - name: HTTPROUTE_FILE + - name: PUBLIC_ROUTES_JSON type: environment - - name: "update httproute hostname" - type: script - file: "$SERVICE_PATH/scripts/istio/update_httproute_hostname" - configuration: - DOMAIN: "$PUBLIC_DOMAIN" - GATEWAY: "gateway-public" - - name: "clean httproute rules" - type: script - file: "$SERVICE_PATH/scripts/istio/clean_httproute_rules" - - name: "process routes" + - name: PRIVATE_ROUTES_JSON + type: environment + - name: "build public httproute" type: script - file: "$SERVICE_PATH/scripts/istio/process_routes" + file: "$SERVICE_PATH/scripts/istio/build_public_httproute" output: - - name: HTTPROUTE_FILE + - name: HTTPROUTE_PUBLIC_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: "build private httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/build_private_httproute" + output: + - name: HTTPROUTE_PRIVATE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From 31e2d6369411cf1c0649e4af63366d962a7d6899 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 12 Jan 2026 14:23:24 -0300 Subject: [PATCH 027/101] feat: basic authorization --- .../plugins/authorization-policy/generate | 231 ++++++++++-------- .../template-private.yaml.tpl | 38 +++ .../template-public.yaml.tpl | 38 +++ endpoint-exposer/workflows/istio/create.yaml | 9 +- endpoint-exposer/workflows/istio/update.yaml | 9 +- 5 files changed, 219 insertions(+), 106 deletions(-) create mode 100644 endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl create mode 100644 endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl diff --git a/endpoint-exposer/plugins/authorization-policy/generate b/endpoint-exposer/plugins/authorization-policy/generate index 981d18b..510e2b3 100755 --- a/endpoint-exposer/plugins/authorization-policy/generate +++ b/endpoint-exposer/plugins/authorization-policy/generate @@ -9,18 +9,13 @@ AUTHZ_ENABLED=$(echo "$CONTEXT" | jq -r '.parameters.authorization.enabled // fa if [[ "$AUTHZ_ENABLED" != "true" ]]; then echo "Authorization Policy plugin is disabled, skipping..." + # Create empty files to satisfy workflow + touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" + touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" exit 0 fi -echo "Authorization enabled - JWT validation via OPA will be applied to all routes" - -# Extract application ID from context -APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.service.dimensions.application_id // ""') - -if [[ -z "$APPLICATION_ID" || "$APPLICATION_ID" == "null" ]]; then - echo "ERROR: application_id not found in service dimensions" - exit 1 -fi +echo "Authorization enabled" # Helper function to detect path type (same logic as build_ingress_with_rule) detect_path_type() { @@ -45,101 +40,141 @@ detect_path_type() { fi } -# Build authorization rules array -AUTHZ_RULES="[]" +# Function to process routes and build rules +build_authz_rules() { + local routes_json="$1" + local rules="[]" + + local num_routes=$(echo "$routes_json" | jq 'length') + + for ((i=0; i "$PUBLIC_CONTEXT_PATH" - echo " Route $((i+1)): $METHOD $ROUTE_PATH (scope: $SCOPE_SLUG)" + # Generate public AuthorizationPolicy YAML + PUBLIC_TEMPLATE="$SERVICE_PATH/plugins/authorization-policy/template-public.yaml.tpl" + PUBLIC_AUTHZ_FILE="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" - # Detect path type - PATH_TYPE_INFO=$(detect_path_type "$ROUTE_PATH") - PATH_TYPE=$(echo "$PATH_TYPE_INFO" | cut -d: -f1) + gomplate -c .="$PUBLIC_CONTEXT_PATH" \ + -f "$PUBLIC_TEMPLATE" \ + -o "$PUBLIC_AUTHZ_FILE" - # Extract path values based on type - if [[ "$PATH_TYPE" == "RegularExpression" ]]; then - PATH_REGEX=$(echo "$PATH_TYPE_INFO" | cut -d: -f2) - PATH_PREFIX=$(echo "$PATH_TYPE_INFO" | cut -d: -f3) - FINAL_PATH="$PATH_REGEX" - elif [[ "$PATH_TYPE" == "PathPrefix" ]]; then - FINAL_PATH=$(echo "$PATH_TYPE_INFO" | cut -d: -f2) - else - # Exact - FINAL_PATH=$(echo "$PATH_TYPE_INFO" | cut -d: -f2) - fi + rm "$PUBLIC_CONTEXT_PATH" + echo "✅ Public AuthorizationPolicy generated: $PUBLIC_AUTHZ_FILE" +else + echo "No public routes or domain, skipping public AuthorizationPolicy..." + touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" +fi - # Build when conditions for header validation - WHEN_CONDITIONS=$(jq -n \ - --arg header_name "request.headers[$AUTHZ_HEADER_NAME]" \ - --argjson allowed_values "$AUTHZ_HEADER_VALUES" \ - '[{ - "key": $header_name, - "values": $allowed_values - }]') - - # Create rule object - RULE=$(jq -n \ - --arg method "$METHOD" \ - --arg path "$FINAL_PATH" \ - --arg path_type "$PATH_TYPE" \ - --arg path_regex "${PATH_REGEX:-}" \ - --arg path_prefix "${PATH_PREFIX:-}" \ - --arg scope_slug "$SCOPE_SLUG" \ - --argjson when_conditions "$WHEN_CONDITIONS" \ +# Process PRIVATE routes +echo "Processing private routes for authorization..." +NUM_PRIVATE_ROUTES=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') +echo "Number of private routes: $NUM_PRIVATE_ROUTES" + +if [[ "$NUM_PRIVATE_ROUTES" -gt 0 ]] && [[ -n "$PRIVATE_DOMAIN" ]] && [[ "$PRIVATE_DOMAIN" != "null" ]]; then + echo "Generating private AuthorizationPolicy..." + PRIVATE_AUTHZ_RULES=$(build_authz_rules "$PRIVATE_ROUTES_JSON") + + # Build context for private template + PRIVATE_CONTEXT=$(jq -n \ + --arg service_slug "$SERVICE_SLUG" \ + --arg service_id "$SERVICE_ID" \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg private_domain "$PRIVATE_DOMAIN" \ + --argjson rules "$PRIVATE_AUTHZ_RULES" \ '{ - method: $method, - path: $path, - path_type: $path_type, - path_regex: $path_regex, - path_prefix: $path_prefix, - scope_slug: $scope_slug, - when_conditions: $when_conditions + service_slug: $service_slug, + service_id: $service_id, + k8s_namespace: $k8s_namespace, + private_domain: $private_domain, + rules: $rules }') - # Add rule to array - AUTHZ_RULES=$(echo "$AUTHZ_RULES" | jq --argjson rule "$RULE" '. += [$rule]') -done - -echo "Generated $(echo "$AUTHZ_RULES" | jq 'length') authorization rules" - -# Build context for gomplate template -PLUGIN_CONTEXT=$(jq -n \ - --arg service_name "$SERVICE_NAME" \ - --arg service_id "$SERVICE_ID" \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg public_domain "$PUBLIC_DOMAIN" \ - --arg application_id "$APPLICATION_ID" \ - --argjson rules "$AUTHZ_RULES" \ - '{ - service_name: $service_name, - service_id: $service_id, - k8s_namespace: $k8s_namespace, - public_domain: $public_domain, - application_id: $application_id, - rules: $rules - }') - -CONTEXT_PATH="$OUTPUT_DIR/authz-context-$SERVICE_ID.json" -echo "$PLUGIN_CONTEXT" > "$CONTEXT_PATH" - -# Generate YAML from template -TEMPLATE="${TEMPLATE:-$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl}" -AUTHZ_POLICY_FILE="$OUTPUT_DIR/authorization-policy-$SERVICE_ID.yaml" - -echo "Generating AuthorizationPolicy YAML..." -gomplate -c .="$CONTEXT_PATH" \ - -f "$TEMPLATE" \ - -o "$AUTHZ_POLICY_FILE" - -rm "$CONTEXT_PATH" - -echo "✅ Authorization Policy generated: $AUTHZ_POLICY_FILE" -export AUTHZ_POLICY_FILE + PRIVATE_CONTEXT_PATH="$OUTPUT_DIR/authz-private-context-$SERVICE_ID.json" + echo "$PRIVATE_CONTEXT" > "$PRIVATE_CONTEXT_PATH" + + # Generate private AuthorizationPolicy YAML + PRIVATE_TEMPLATE="$SERVICE_PATH/plugins/authorization-policy/template-private.yaml.tpl" + PRIVATE_AUTHZ_FILE="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" + + gomplate -c .="$PRIVATE_CONTEXT_PATH" \ + -f "$PRIVATE_TEMPLATE" \ + -o "$PRIVATE_AUTHZ_FILE" + + rm "$PRIVATE_CONTEXT_PATH" + echo "✅ Private AuthorizationPolicy generated: $PRIVATE_AUTHZ_FILE" +else + echo "No private routes or domain, skipping private AuthorizationPolicy..." + touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" +fi + +echo "=== Authorization Policy generation complete ===" diff --git a/endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl new file mode 100644 index 0000000..7308881 --- /dev/null +++ b/endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl @@ -0,0 +1,38 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: {{ .service_slug }}-{{ .service_id }}-authz-private + namespace: gateways + labels: + app.kubernetes.io/name: {{ .service_slug }} + nullplatform.com/service-id: "{{ .service_id }}" + nullplatform.com/managed-by: endpoint-exposer +spec: + # Apply to the private Gateway workload + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-private + action: CUSTOM + provider: + name: opa-ext-authz + rules: + {{- range .rules }} + # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) + - to: + - operation: + hosts: ["{{ $.private_domain }}"] + {{- if ne .method "" }} + methods: ["{{ .method }}"] + {{- end }} + paths: + {{- if eq .path_type "Exact" }} + - "{{ .path }}" + {{- else if eq .path_type "PathPrefix" }} + - "{{ .path }}*" + {{- else if eq .path_type "RegularExpression" }} + # Regex: {{ .path_regex }} + # Note: Istio AuthorizationPolicy doesn't support regex in paths + # Using prefix matching as fallback for regex patterns + - "{{ .path_prefix }}*" + {{- end }} + {{- end }} diff --git a/endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl new file mode 100644 index 0000000..8d75dea --- /dev/null +++ b/endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl @@ -0,0 +1,38 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: {{ .service_slug }}-{{ .service_id }}-authz-public + namespace: gateways + labels: + app.kubernetes.io/name: {{ .service_slug }} + nullplatform.com/service-id: "{{ .service_id }}" + nullplatform.com/managed-by: endpoint-exposer +spec: + # Apply to the public Gateway workload + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-public + action: CUSTOM + provider: + name: opa-ext-authz + rules: + {{- range .rules }} + # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) + - to: + - operation: + hosts: ["{{ $.public_domain }}"] + {{- if ne .method "" }} + methods: ["{{ .method }}"] + {{- end }} + paths: + {{- if eq .path_type "Exact" }} + - "{{ .path }}" + {{- else if eq .path_type "PathPrefix" }} + - "{{ .path }}*" + {{- else if eq .path_type "RegularExpression" }} + # Regex: {{ .path_regex }} + # Note: Istio AuthorizationPolicy doesn't support regex in paths + # Using prefix matching as fallback for regex patterns + - "{{ .path_prefix }}*" + {{- end }} + {{- end }} diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index bc9fffe..dd0fa4b 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -46,12 +46,13 @@ steps: - name: "generate authorization policy" type: script file: "$SERVICE_PATH/plugins/authorization-policy/generate" - configuration: - TEMPLATE: "$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" output: - - name: AUTHZ_POLICY_FILE + - name: AUTHZ_POLICY_PUBLIC_FILE + type: file + file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" + - name: AUTHZ_POLICY_PRIVATE_FILE type: file - file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID.yaml" + file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index e1f91df..37dd7ed 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -46,12 +46,13 @@ steps: - name: "generate authorization policy" type: script file: "$SERVICE_PATH/plugins/authorization-policy/generate" - configuration: - TEMPLATE: "$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" output: - - name: AUTHZ_POLICY_FILE + - name: AUTHZ_POLICY_PUBLIC_FILE + type: file + file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" + - name: AUTHZ_POLICY_PRIVATE_FILE type: file - file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID.yaml" + file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From 33abaaf03995038d17e9726d66cb26514759deb0 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 12 Jan 2026 14:40:37 -0300 Subject: [PATCH 028/101] feat: basic authorization --- endpoint-exposer/plugins/authorization-policy/generate | 5 ----- endpoint-exposer/scripts/istio/build_private_httproute | 2 -- endpoint-exposer/scripts/istio/build_public_httproute | 2 -- 3 files changed, 9 deletions(-) diff --git a/endpoint-exposer/plugins/authorization-policy/generate b/endpoint-exposer/plugins/authorization-policy/generate index 510e2b3..70464ed 100755 --- a/endpoint-exposer/plugins/authorization-policy/generate +++ b/endpoint-exposer/plugins/authorization-policy/generate @@ -9,9 +9,6 @@ AUTHZ_ENABLED=$(echo "$CONTEXT" | jq -r '.parameters.authorization.enabled // fa if [[ "$AUTHZ_ENABLED" != "true" ]]; then echo "Authorization Policy plugin is disabled, skipping..." - # Create empty files to satisfy workflow - touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" - touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" exit 0 fi @@ -132,7 +129,6 @@ if [[ "$NUM_PUBLIC_ROUTES" -gt 0 ]] && [[ -n "$PUBLIC_DOMAIN" ]] && [[ "$PUBLIC_ echo "✅ Public AuthorizationPolicy generated: $PUBLIC_AUTHZ_FILE" else echo "No public routes or domain, skipping public AuthorizationPolicy..." - touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" fi # Process PRIVATE routes @@ -174,7 +170,6 @@ if [[ "$NUM_PRIVATE_ROUTES" -gt 0 ]] && [[ -n "$PRIVATE_DOMAIN" ]] && [[ "$PRIVA echo "✅ Private AuthorizationPolicy generated: $PRIVATE_AUTHZ_FILE" else echo "No private routes or domain, skipping private AuthorizationPolicy..." - touch "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" fi echo "=== Authorization Policy generation complete ===" diff --git a/endpoint-exposer/scripts/istio/build_private_httproute b/endpoint-exposer/scripts/istio/build_private_httproute index a7b249c..6241222 100755 --- a/endpoint-exposer/scripts/istio/build_private_httproute +++ b/endpoint-exposer/scripts/istio/build_private_httproute @@ -12,7 +12,6 @@ echo "Private Domain: '$PRIVATE_DOMAIN'" # Skip if no routes if [[ "$NUM_ROUTES" -eq 0 ]]; then echo "No private routes configured, skipping..." - touch "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" exit 0 fi @@ -20,7 +19,6 @@ fi if [[ -z "$PRIVATE_DOMAIN" ]] || [[ "$PRIVATE_DOMAIN" == "null" ]] || [[ "$PRIVATE_DOMAIN" == "\"null\"" ]]; then echo "WARNING: Private routes exist but no private domain configured!" echo "Skipping private HTTPRoute generation..." - touch "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" exit 0 fi diff --git a/endpoint-exposer/scripts/istio/build_public_httproute b/endpoint-exposer/scripts/istio/build_public_httproute index dada905..a06f205 100755 --- a/endpoint-exposer/scripts/istio/build_public_httproute +++ b/endpoint-exposer/scripts/istio/build_public_httproute @@ -10,8 +10,6 @@ echo "Number of public routes: $NUM_ROUTES" if [[ "$NUM_ROUTES" -eq 0 ]] || [[ -z "$PUBLIC_DOMAIN" ]] || [[ "$PUBLIC_DOMAIN" == "null" ]]; then echo "No public routes or no public domain configured, skipping..." - # Create empty file to satisfy workflow - touch "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" exit 0 fi From 8edf8c0cd854d41cf3b5b2f7bc75881ee7c34a33 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 12 Jan 2026 16:00:56 -0300 Subject: [PATCH 029/101] feat: basic authorization --- .../plugins/authorization-policy/generate | 126 +++++++++--------- .../template-private.yaml.tpl | 38 ------ .../template-public.yaml.tpl | 38 ------ .../authorization-policy/template.yaml.tpl | 14 +- .../scripts/istio/build_httproute | 101 ++++++++++++++ .../scripts/istio/build_private_httproute | 66 --------- .../scripts/istio/build_public_httproute | 59 -------- endpoint-exposer/scripts/istio/config | 10 ++ .../templates/istio/httproute-public.yaml.tpl | 26 ---- ...te-private.yaml.tpl => httproute.yaml.tpl} | 8 +- endpoint-exposer/workflows/istio/create.yaml | 8 +- endpoint-exposer/workflows/istio/update.yaml | 8 +- 12 files changed, 196 insertions(+), 306 deletions(-) delete mode 100644 endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl delete mode 100644 endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl create mode 100755 endpoint-exposer/scripts/istio/build_httproute delete mode 100755 endpoint-exposer/scripts/istio/build_private_httproute delete mode 100755 endpoint-exposer/scripts/istio/build_public_httproute create mode 100755 endpoint-exposer/scripts/istio/config delete mode 100644 endpoint-exposer/templates/istio/httproute-public.yaml.tpl rename endpoint-exposer/templates/istio/{httproute-private.yaml.tpl => httproute.yaml.tpl} (75%) diff --git a/endpoint-exposer/plugins/authorization-policy/generate b/endpoint-exposer/plugins/authorization-policy/generate index 70464ed..0485ba8 100755 --- a/endpoint-exposer/plugins/authorization-policy/generate +++ b/endpoint-exposer/plugins/authorization-policy/generate @@ -2,6 +2,9 @@ set -euo pipefail +# Load configuration +source "$SERVICE_PATH/scripts/istio/config" + echo "=== Generating Authorization Policy ===" # Check if plugin is enabled @@ -13,6 +16,10 @@ if [[ "$AUTHZ_ENABLED" != "true" ]]; then fi echo "Authorization enabled" +echo "Using OPA provider: $OPA_PROVIDER_NAME" +echo "Public gateway: $PUBLIC_GATEWAY_NAME" +echo "Private gateway: $PRIVATE_GATEWAY_NAME" +echo "Gateway namespace: $GATEWAY_NAMESPACE" # Helper function to detect path type (same logic as build_ingress_with_rule) detect_path_type() { @@ -90,86 +97,77 @@ build_authz_rules() { echo "$rules" } -# Process PUBLIC routes -echo "Processing public routes for authorization..." -NUM_PUBLIC_ROUTES=$(echo "$PUBLIC_ROUTES_JSON" | jq 'length') -echo "Number of public routes: $NUM_PUBLIC_ROUTES" - -if [[ "$NUM_PUBLIC_ROUTES" -gt 0 ]] && [[ -n "$PUBLIC_DOMAIN" ]] && [[ "$PUBLIC_DOMAIN" != "null" ]]; then - echo "Generating public AuthorizationPolicy..." - PUBLIC_AUTHZ_RULES=$(build_authz_rules "$PUBLIC_ROUTES_JSON") - - # Build context for public template - PUBLIC_CONTEXT=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg public_domain "$PUBLIC_DOMAIN" \ - --argjson rules "$PUBLIC_AUTHZ_RULES" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - k8s_namespace: $k8s_namespace, - public_domain: $public_domain, - rules: $rules - }') - - PUBLIC_CONTEXT_PATH="$OUTPUT_DIR/authz-public-context-$SERVICE_ID.json" - echo "$PUBLIC_CONTEXT" > "$PUBLIC_CONTEXT_PATH" - - # Generate public AuthorizationPolicy YAML - PUBLIC_TEMPLATE="$SERVICE_PATH/plugins/authorization-policy/template-public.yaml.tpl" - PUBLIC_AUTHZ_FILE="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" - - gomplate -c .="$PUBLIC_CONTEXT_PATH" \ - -f "$PUBLIC_TEMPLATE" \ - -o "$PUBLIC_AUTHZ_FILE" +# Function to generate AuthorizationPolicy for a specific visibility +generate_authz_policy() { + local visibility="$1" # "public" or "private" + + if [[ "$visibility" == "public" ]]; then + local routes_json="$PUBLIC_ROUTES_JSON" + local domain="$PUBLIC_DOMAIN" + local gateway_name="$PUBLIC_GATEWAY_NAME" + local suffix="public" + elif [[ "$visibility" == "private" ]]; then + local routes_json="$PRIVATE_ROUTES_JSON" + local domain="$PRIVATE_DOMAIN" + local gateway_name="$PRIVATE_GATEWAY_NAME" + local suffix="private" + else + echo "ERROR: Invalid visibility: $visibility" + return 1 + fi - rm "$PUBLIC_CONTEXT_PATH" - echo "✅ Public AuthorizationPolicy generated: $PUBLIC_AUTHZ_FILE" -else - echo "No public routes or domain, skipping public AuthorizationPolicy..." -fi + echo "Processing $visibility routes for authorization..." + local num_routes=$(echo "$routes_json" | jq 'length') + echo "Number of $visibility routes: $num_routes" -# Process PRIVATE routes -echo "Processing private routes for authorization..." -NUM_PRIVATE_ROUTES=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') -echo "Number of private routes: $NUM_PRIVATE_ROUTES" + if [[ "$num_routes" -eq 0 ]] || [[ -z "$domain" ]] || [[ "$domain" == "null" ]]; then + echo "No $visibility routes or domain, skipping $visibility AuthorizationPolicy..." + return 0 + fi -if [[ "$NUM_PRIVATE_ROUTES" -gt 0 ]] && [[ -n "$PRIVATE_DOMAIN" ]] && [[ "$PRIVATE_DOMAIN" != "null" ]]; then - echo "Generating private AuthorizationPolicy..." - PRIVATE_AUTHZ_RULES=$(build_authz_rules "$PRIVATE_ROUTES_JSON") + echo "Generating $visibility AuthorizationPolicy..." + local authz_rules=$(build_authz_rules "$routes_json") - # Build context for private template - PRIVATE_CONTEXT=$(jq -n \ + # Build context for template + local context=$(jq -n \ --arg service_slug "$SERVICE_SLUG" \ --arg service_id "$SERVICE_ID" \ --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg private_domain "$PRIVATE_DOMAIN" \ - --argjson rules "$PRIVATE_AUTHZ_RULES" \ + --arg domain "$domain" \ + --arg opa_provider "$OPA_PROVIDER_NAME" \ + --arg gateway_name "$gateway_name" \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg suffix "$suffix" \ + --argjson rules "$authz_rules" \ '{ service_slug: $service_slug, service_id: $service_id, k8s_namespace: $k8s_namespace, - private_domain: $private_domain, + domain: $domain, + opa_provider: $opa_provider, + gateway_name: $gateway_name, + gateway_namespace: $gateway_namespace, + suffix: $suffix, rules: $rules }') - PRIVATE_CONTEXT_PATH="$OUTPUT_DIR/authz-private-context-$SERVICE_ID.json" - echo "$PRIVATE_CONTEXT" > "$PRIVATE_CONTEXT_PATH" + local context_path="$OUTPUT_DIR/authz-$suffix-context-$SERVICE_ID.json" + echo "$context" > "$context_path" - # Generate private AuthorizationPolicy YAML - PRIVATE_TEMPLATE="$SERVICE_PATH/plugins/authorization-policy/template-private.yaml.tpl" - PRIVATE_AUTHZ_FILE="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" + # Generate AuthorizationPolicy YAML + local template="$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" + local authz_file="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-$suffix.yaml" - gomplate -c .="$PRIVATE_CONTEXT_PATH" \ - -f "$PRIVATE_TEMPLATE" \ - -o "$PRIVATE_AUTHZ_FILE" + gomplate -c .="$context_path" \ + -f "$template" \ + -o "$authz_file" - rm "$PRIVATE_CONTEXT_PATH" - echo "✅ Private AuthorizationPolicy generated: $PRIVATE_AUTHZ_FILE" -else - echo "No private routes or domain, skipping private AuthorizationPolicy..." -fi + rm "$context_path" + echo "✅ $visibility AuthorizationPolicy generated: $authz_file" +} + +# Generate AuthorizationPolicies for both visibilities +generate_authz_policy "public" +generate_authz_policy "private" echo "=== Authorization Policy generation complete ===" diff --git a/endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl deleted file mode 100644 index 7308881..0000000 --- a/endpoint-exposer/plugins/authorization-policy/template-private.yaml.tpl +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: {{ .service_slug }}-{{ .service_id }}-authz-private - namespace: gateways - labels: - app.kubernetes.io/name: {{ .service_slug }} - nullplatform.com/service-id: "{{ .service_id }}" - nullplatform.com/managed-by: endpoint-exposer -spec: - # Apply to the private Gateway workload - selector: - matchLabels: - gateway.networking.k8s.io/gateway-name: gateway-private - action: CUSTOM - provider: - name: opa-ext-authz - rules: - {{- range .rules }} - # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) - - to: - - operation: - hosts: ["{{ $.private_domain }}"] - {{- if ne .method "" }} - methods: ["{{ .method }}"] - {{- end }} - paths: - {{- if eq .path_type "Exact" }} - - "{{ .path }}" - {{- else if eq .path_type "PathPrefix" }} - - "{{ .path }}*" - {{- else if eq .path_type "RegularExpression" }} - # Regex: {{ .path_regex }} - # Note: Istio AuthorizationPolicy doesn't support regex in paths - # Using prefix matching as fallback for regex patterns - - "{{ .path_prefix }}*" - {{- end }} - {{- end }} diff --git a/endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl deleted file mode 100644 index 8d75dea..0000000 --- a/endpoint-exposer/plugins/authorization-policy/template-public.yaml.tpl +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: {{ .service_slug }}-{{ .service_id }}-authz-public - namespace: gateways - labels: - app.kubernetes.io/name: {{ .service_slug }} - nullplatform.com/service-id: "{{ .service_id }}" - nullplatform.com/managed-by: endpoint-exposer -spec: - # Apply to the public Gateway workload - selector: - matchLabels: - gateway.networking.k8s.io/gateway-name: gateway-public - action: CUSTOM - provider: - name: opa-ext-authz - rules: - {{- range .rules }} - # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) - - to: - - operation: - hosts: ["{{ $.public_domain }}"] - {{- if ne .method "" }} - methods: ["{{ .method }}"] - {{- end }} - paths: - {{- if eq .path_type "Exact" }} - - "{{ .path }}" - {{- else if eq .path_type "PathPrefix" }} - - "{{ .path }}*" - {{- else if eq .path_type "RegularExpression" }} - # Regex: {{ .path_regex }} - # Note: Istio AuthorizationPolicy doesn't support regex in paths - # Using prefix matching as fallback for regex patterns - - "{{ .path_prefix }}*" - {{- end }} - {{- end }} diff --git a/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl index 0e77b27..dfac449 100644 --- a/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl +++ b/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl @@ -1,26 +1,26 @@ apiVersion: security.istio.io/v1 kind: AuthorizationPolicy metadata: - name: {{ .service_name }}-{{ .service_id }}-authz - namespace: gateways + name: {{ .service_slug }}-{{ .service_id }}-authz-{{ .suffix }} + namespace: {{ .gateway_namespace }} labels: - app.kubernetes.io/name: {{ .service_name }} + app.kubernetes.io/name: {{ .service_slug }} nullplatform.com/service-id: "{{ .service_id }}" nullplatform.com/managed-by: endpoint-exposer spec: - # Apply to the Gateway workload (not service pods) + # Apply to the Gateway workload selector: matchLabels: - gateway.networking.k8s.io/gateway-name: gateway-public + gateway.networking.k8s.io/gateway-name: {{ .gateway_name }} action: CUSTOM provider: - name: opa-ext-authz + name: {{ .opa_provider }} rules: {{- range .rules }} # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) - to: - operation: - hosts: ["{{ $.public_domain }}"] + hosts: ["{{ $.domain }}"] {{- if ne .method "" }} methods: ["{{ .method }}"] {{- end }} diff --git a/endpoint-exposer/scripts/istio/build_httproute b/endpoint-exposer/scripts/istio/build_httproute new file mode 100755 index 0000000..ae92b9c --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_httproute @@ -0,0 +1,101 @@ +#!/bin/bash + +set -euo pipefail + +# Load configuration +source "$SERVICE_PATH/scripts/istio/config" + +# Parameters (must be set by caller) +VISIBILITY="${VISIBILITY:-public}" # "public" or "private" + +echo "=== Building ${VISIBILITY^} HTTPRoute ===" + +# Set variables based on visibility +if [[ "$VISIBILITY" == "public" ]]; then + ROUTES_JSON="$PUBLIC_ROUTES_JSON" + DOMAIN="$PUBLIC_DOMAIN" + GATEWAY_NAME="$PUBLIC_GATEWAY_NAME" + SUFFIX="public" +elif [[ "$VISIBILITY" == "private" ]]; then + ROUTES_JSON="$PRIVATE_ROUTES_JSON" + DOMAIN="$PRIVATE_DOMAIN" + GATEWAY_NAME="$PRIVATE_GATEWAY_NAME" + SUFFIX="private" +else + echo "ERROR: Invalid VISIBILITY value: $VISIBILITY" + exit 1 +fi + +# Check if we have routes and domain +NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') +echo "Number of $VISIBILITY routes: $NUM_ROUTES" +echo "$VISIBILITY Domain: '$DOMAIN'" + +# Skip if no routes +if [[ "$NUM_ROUTES" -eq 0 ]]; then + echo "No $VISIBILITY routes configured, skipping..." + exit 0 +fi + +# Skip if domain is empty/null +if [[ -z "$DOMAIN" ]] || [[ "$DOMAIN" == "null" ]] || [[ "$DOMAIN" == "\"null\"" ]]; then + echo "WARNING: $VISIBILITY routes exist but no $VISIBILITY domain configured!" + echo "Skipping $VISIBILITY HTTPRoute generation..." + exit 0 +fi + +# Create HTTPRoute from template +TEMPLATE="$SERVICE_PATH/templates/istio/httproute.yaml.tpl" +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-$SUFFIX.yaml" + +# Build context for template +HTTPROUTE_CONTEXT=$(jq -n \ + --arg service_slug "$SERVICE_SLUG" \ + --arg service_id "$SERVICE_ID" \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg domain "$DOMAIN" \ + --arg gateway_name "$GATEWAY_NAME" \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg suffix "$SUFFIX" \ + '{ + service_slug: $service_slug, + service_id: $service_id, + k8s_namespace: $k8s_namespace, + domain: $domain, + gateway_name: $gateway_name, + gateway_namespace: $gateway_namespace, + suffix: $suffix + }') + +CONTEXT_PATH="$OUTPUT_DIR/httproute-$SUFFIX-context-$SERVICE_ID.json" +echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" + +echo "Generating HTTPRoute from template: $TEMPLATE" +gomplate -c .="$CONTEXT_PATH" \ + -f "$TEMPLATE" \ + -o "$HTTPROUTE_FILE" + +rm "$CONTEXT_PATH" + +# Now process routes using the existing logic +# Temporarily override ROUTES_JSON with visibility-specific routes +ORIGINAL_ROUTES_JSON="${ROUTES_JSON:-[]}" +export ROUTES_JSON="$ROUTES_JSON" +export HTTPROUTE_FILE="$HTTPROUTE_FILE" + +# Process routes (skip if no routes) +if [[ "$NUM_ROUTES" -gt 0 ]]; then + source "$SERVICE_PATH/scripts/istio/process_routes" +fi + +# Restore original ROUTES_JSON +export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" + +echo "✅ $VISIBILITY HTTPRoute created: $HTTPROUTE_FILE" + +# Export file path based on visibility +if [[ "$VISIBILITY" == "public" ]]; then + export HTTPROUTE_PUBLIC_FILE="$HTTPROUTE_FILE" +elif [[ "$VISIBILITY" == "private" ]]; then + export HTTPROUTE_PRIVATE_FILE="$HTTPROUTE_FILE" +fi diff --git a/endpoint-exposer/scripts/istio/build_private_httproute b/endpoint-exposer/scripts/istio/build_private_httproute deleted file mode 100755 index 6241222..0000000 --- a/endpoint-exposer/scripts/istio/build_private_httproute +++ /dev/null @@ -1,66 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "=== Building Private HTTPRoute ===" - -# Check if we have private routes and private domain -NUM_ROUTES=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') -echo "Number of private routes: $NUM_ROUTES" -echo "Private Domain: '$PRIVATE_DOMAIN'" - -# Skip if no routes -if [[ "$NUM_ROUTES" -eq 0 ]]; then - echo "No private routes configured, skipping..." - exit 0 -fi - -# Skip if domain is empty/null -if [[ -z "$PRIVATE_DOMAIN" ]] || [[ "$PRIVATE_DOMAIN" == "null" ]] || [[ "$PRIVATE_DOMAIN" == "\"null\"" ]]; then - echo "WARNING: Private routes exist but no private domain configured!" - echo "Skipping private HTTPRoute generation..." - exit 0 -fi - -# Create empty HTTPRoute from template -TEMPLATE="$SERVICE_PATH/templates/istio/httproute-private.yaml.tpl" -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - -# Build context for template -HTTPROUTE_CONTEXT=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg private_domain "$PRIVATE_DOMAIN" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - k8s_namespace: $k8s_namespace, - private_domain: $private_domain - }') - -CONTEXT_PATH="$OUTPUT_DIR/httproute-private-context-$SERVICE_ID.json" -echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" - -echo "Generating HTTPRoute from template: $TEMPLATE" -gomplate -c .="$CONTEXT_PATH" \ - -f "$TEMPLATE" \ - -o "$HTTPROUTE_FILE" - -rm "$CONTEXT_PATH" - -# Now process routes using the existing logic -# Temporarily override ROUTES_JSON with private routes only -ORIGINAL_ROUTES_JSON="$ROUTES_JSON" -export ROUTES_JSON="$PRIVATE_ROUTES_JSON" - -# Process routes (skip if no routes) -if [[ "$NUM_ROUTES" -gt 0 ]]; then - source "$SERVICE_PATH/scripts/istio/process_routes" -fi - -# Restore original ROUTES_JSON -export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" - -echo "✅ Private HTTPRoute created: $HTTPROUTE_FILE" -export HTTPROUTE_PRIVATE_FILE="$HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/build_public_httproute b/endpoint-exposer/scripts/istio/build_public_httproute deleted file mode 100755 index a06f205..0000000 --- a/endpoint-exposer/scripts/istio/build_public_httproute +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -echo "=== Building Public HTTPRoute ===" - -# Check if we have public routes and public domain -NUM_ROUTES=$(echo "$PUBLIC_ROUTES_JSON" | jq 'length') -echo "Number of public routes: $NUM_ROUTES" - -if [[ "$NUM_ROUTES" -eq 0 ]] || [[ -z "$PUBLIC_DOMAIN" ]] || [[ "$PUBLIC_DOMAIN" == "null" ]]; then - echo "No public routes or no public domain configured, skipping..." - exit 0 -fi - -echo "Public Domain: $PUBLIC_DOMAIN" - -# Create empty HTTPRoute from template -TEMPLATE="$SERVICE_PATH/templates/istio/httproute-public.yaml.tpl" -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - -# Build context for template -HTTPROUTE_CONTEXT=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg public_domain "$PUBLIC_DOMAIN" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - k8s_namespace: $k8s_namespace, - public_domain: $public_domain - }') - -CONTEXT_PATH="$OUTPUT_DIR/httproute-public-context-$SERVICE_ID.json" -echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" - -echo "Generating HTTPRoute from template: $TEMPLATE" -gomplate -c .="$CONTEXT_PATH" \ - -f "$TEMPLATE" \ - -o "$HTTPROUTE_FILE" - -rm "$CONTEXT_PATH" - -# Now process routes using the existing logic -# Temporarily override ROUTES_JSON with public routes only -ORIGINAL_ROUTES_JSON="$ROUTES_JSON" -export ROUTES_JSON="$PUBLIC_ROUTES_JSON" - -# Process routes (skip if no routes) -if [[ "$NUM_ROUTES" -gt 0 ]]; then - source "$SERVICE_PATH/scripts/istio/process_routes" -fi - -# Restore original ROUTES_JSON -export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" - -echo "✅ Public HTTPRoute created: $HTTPROUTE_FILE" -export HTTPROUTE_PUBLIC_FILE="$HTTPROUTE_FILE" diff --git a/endpoint-exposer/scripts/istio/config b/endpoint-exposer/scripts/istio/config new file mode 100755 index 0000000..c64693b --- /dev/null +++ b/endpoint-exposer/scripts/istio/config @@ -0,0 +1,10 @@ +#!/bin/bash + +# Gateway configuration +# These values can be overridden by environment variables +export PUBLIC_GATEWAY_NAME="${PUBLIC_GATEWAY_NAME:-gateway-public}" +export PRIVATE_GATEWAY_NAME="${PRIVATE_GATEWAY_NAME:-gateway-private}" +export GATEWAY_NAMESPACE="${GATEWAY_NAMESPACE:-gateways}" + +# OPA configuration +export OPA_PROVIDER_NAME="${OPA_PROVIDER_NAME:-opa-ext-authz}" diff --git a/endpoint-exposer/templates/istio/httproute-public.yaml.tpl b/endpoint-exposer/templates/istio/httproute-public.yaml.tpl deleted file mode 100644 index e659763..0000000 --- a/endpoint-exposer/templates/istio/httproute-public.yaml.tpl +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: {{ .service_slug }}-{{ .service_id }}-public - namespace: {{ .k8s_namespace }} - labels: - nullplatform: "true" - service_slug: {{ .service_slug }} - service_id: {{ .service_id }} -spec: - parentRefs: - - name: gateway-public - namespace: gateways - group: gateway.networking.k8s.io - kind: Gateway - hostnames: - - {{ .public_domain }} - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: response-404 - port: 80 - weight: 0 diff --git a/endpoint-exposer/templates/istio/httproute-private.yaml.tpl b/endpoint-exposer/templates/istio/httproute.yaml.tpl similarity index 75% rename from endpoint-exposer/templates/istio/httproute-private.yaml.tpl rename to endpoint-exposer/templates/istio/httproute.yaml.tpl index 86a7f83..ded2bba 100644 --- a/endpoint-exposer/templates/istio/httproute-private.yaml.tpl +++ b/endpoint-exposer/templates/istio/httproute.yaml.tpl @@ -1,7 +1,7 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: - name: {{ .service_slug }}-{{ .service_id }}-private + name: {{ .service_slug }}-{{ .service_id }}-{{ .suffix }} namespace: {{ .k8s_namespace }} labels: nullplatform: "true" @@ -9,12 +9,12 @@ metadata: service_id: {{ .service_id }} spec: parentRefs: - - name: gateway-private - namespace: gateways + - name: {{ .gateway_name }} + namespace: {{ .gateway_namespace }} group: gateway.networking.k8s.io kind: Gateway hostnames: - - {{ .private_domain }} + - {{ .domain }} rules: - matches: - path: diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index dd0fa4b..3084876 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -31,14 +31,18 @@ steps: type: environment - name: "build public httproute" type: script - file: "$SERVICE_PATH/scripts/istio/build_public_httproute" + file: "$SERVICE_PATH/scripts/istio/build_httproute" + configuration: + VISIBILITY: "public" output: - name: HTTPROUTE_PUBLIC_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - name: "build private httproute" type: script - file: "$SERVICE_PATH/scripts/istio/build_private_httproute" + file: "$SERVICE_PATH/scripts/istio/build_httproute" + configuration: + VISIBILITY: "private" output: - name: HTTPROUTE_PRIVATE_FILE type: file diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 37dd7ed..0a3eba2 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -31,14 +31,18 @@ steps: type: environment - name: "build public httproute" type: script - file: "$SERVICE_PATH/scripts/istio/build_public_httproute" + file: "$SERVICE_PATH/scripts/istio/build_httproute" + configuration: + VISIBILITY: "public" output: - name: HTTPROUTE_PUBLIC_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - name: "build private httproute" type: script - file: "$SERVICE_PATH/scripts/istio/build_private_httproute" + file: "$SERVICE_PATH/scripts/istio/build_httproute" + configuration: + VISIBILITY: "private" output: - name: HTTPROUTE_PRIVATE_FILE type: file From 786009fbfcdc4fb2e49a7e7dd2654c7ebc6c42ae Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 12 Jan 2026 16:35:44 -0300 Subject: [PATCH 030/101] feat: unify private and public logic --- .../plugins/authorization-policy/generate | 106 +++++++++-------- endpoint-exposer/scripts/common/apply | 34 +++++- .../scripts/istio/build_httproute | 111 ++++++++++-------- .../templates/istio/httproute.yaml.tpl | 6 +- 4 files changed, 148 insertions(+), 109 deletions(-) diff --git a/endpoint-exposer/plugins/authorization-policy/generate b/endpoint-exposer/plugins/authorization-policy/generate index 0485ba8..1df93da 100755 --- a/endpoint-exposer/plugins/authorization-policy/generate +++ b/endpoint-exposer/plugins/authorization-policy/generate @@ -10,17 +10,16 @@ echo "=== Generating Authorization Policy ===" # Check if plugin is enabled AUTHZ_ENABLED=$(echo "$CONTEXT" | jq -r '.parameters.authorization.enabled // false') -if [[ "$AUTHZ_ENABLED" != "true" ]]; then - echo "Authorization Policy plugin is disabled, skipping..." - exit 0 +if [[ "$AUTHZ_ENABLED" == "true" ]]; then + echo "Authorization enabled" + echo "Using OPA provider: $OPA_PROVIDER_NAME" + echo "Public gateway: $PUBLIC_GATEWAY_NAME" + echo "Private gateway: $PRIVATE_GATEWAY_NAME" + echo "Gateway namespace: $GATEWAY_NAMESPACE" +else + echo "Authorization disabled - will create empty resources for cleanup" fi -echo "Authorization enabled" -echo "Using OPA provider: $OPA_PROVIDER_NAME" -echo "Public gateway: $PUBLIC_GATEWAY_NAME" -echo "Private gateway: $PRIVATE_GATEWAY_NAME" -echo "Gateway namespace: $GATEWAY_NAMESPACE" - # Helper function to detect path type (same logic as build_ingress_with_rule) detect_path_type() { local path="$1" @@ -120,50 +119,59 @@ generate_authz_policy() { local num_routes=$(echo "$routes_json" | jq 'length') echo "Number of $visibility routes: $num_routes" - if [[ "$num_routes" -eq 0 ]] || [[ -z "$domain" ]] || [[ "$domain" == "null" ]]; then - echo "No $visibility routes or domain, skipping $visibility AuthorizationPolicy..." - return 0 + local authz_file="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-$suffix.yaml" + + # Determine if we should create empty resource + local create_empty=false + if [[ "$AUTHZ_ENABLED" != "true" ]] || [[ "$num_routes" -eq 0 ]] || [[ -z "$domain" ]] || [[ "$domain" == "null" ]]; then + create_empty=true fi - echo "Generating $visibility AuthorizationPolicy..." - local authz_rules=$(build_authz_rules "$routes_json") - - # Build context for template - local context=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg domain "$domain" \ - --arg opa_provider "$OPA_PROVIDER_NAME" \ - --arg gateway_name "$gateway_name" \ - --arg gateway_namespace "$GATEWAY_NAMESPACE" \ - --arg suffix "$suffix" \ - --argjson rules "$authz_rules" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - k8s_namespace: $k8s_namespace, - domain: $domain, - opa_provider: $opa_provider, - gateway_name: $gateway_name, - gateway_namespace: $gateway_namespace, - suffix: $suffix, - rules: $rules - }') - - local context_path="$OUTPUT_DIR/authz-$suffix-context-$SERVICE_ID.json" - echo "$context" > "$context_path" - - # Generate AuthorizationPolicy YAML - local template="$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" - local authz_file="$OUTPUT_DIR/authorization-policy-$SERVICE_ID-$suffix.yaml" + if [[ "$create_empty" == "true" ]]; then + # Don't create the file - kubectl apply with --prune will delete it + echo "Skipping $visibility AuthorizationPolicy generation - resource will be pruned if it exists" + # Create a marker file for tracking + touch "$OUTPUT_DIR/.authz-$suffix-deleted" + else + echo "Generating $visibility AuthorizationPolicy..." + local authz_rules=$(build_authz_rules "$routes_json") + + # Build context for template + local context=$(jq -n \ + --arg service_slug "$SERVICE_SLUG" \ + --arg service_id "$SERVICE_ID" \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg domain "$domain" \ + --arg opa_provider "$OPA_PROVIDER_NAME" \ + --arg gateway_name "$gateway_name" \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg suffix "$suffix" \ + --argjson rules "$authz_rules" \ + '{ + service_slug: $service_slug, + service_id: $service_id, + k8s_namespace: $k8s_namespace, + domain: $domain, + opa_provider: $opa_provider, + gateway_name: $gateway_name, + gateway_namespace: $gateway_namespace, + suffix: $suffix, + rules: $rules + }') + + local context_path="$OUTPUT_DIR/authz-$suffix-context-$SERVICE_ID.json" + echo "$context" > "$context_path" - gomplate -c .="$context_path" \ - -f "$template" \ - -o "$authz_file" + # Generate AuthorizationPolicy YAML + local template="$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" - rm "$context_path" - echo "✅ $visibility AuthorizationPolicy generated: $authz_file" + gomplate -c .="$context_path" \ + -f "$template" \ + -o "$authz_file" + + rm "$context_path" + echo "✅ $visibility AuthorizationPolicy generated: $authz_file" + fi } # Generate AuthorizationPolicies for both visibilities diff --git a/endpoint-exposer/scripts/common/apply b/endpoint-exposer/scripts/common/apply index 2692c3c..45d699b 100644 --- a/endpoint-exposer/scripts/common/apply +++ b/endpoint-exposer/scripts/common/apply @@ -4,14 +4,35 @@ set -euo pipefail echo "TEMPLATE DIR: $OUTPUT_DIR, ACTION: $ACTION, DRY_RUN: $DRY_RUN" +# Collect all yaml files into a temporary directory for batch apply +TEMP_APPLY_DIR="$OUTPUT_DIR/batch-apply" +mkdir -p "$TEMP_APPLY_DIR" + # Find all .yaml files that were not yet applied / deleted -find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" \) -prune -o -type f -name "*.yaml" -print | while read -r TEMPLATE_FILE; do - echo "kubectl $ACTION $TEMPLATE_FILE" +find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" -o -path "*/batch-apply" \) -prune -o -type f -name "*.yaml" -print | while read -r TEMPLATE_FILE; do + FILENAME="$(basename "$TEMPLATE_FILE")" + cp "$TEMPLATE_FILE" "$TEMP_APPLY_DIR/$FILENAME" +done - if [[ "$DRY_RUN" == "false" ]]; then - kubectl "$ACTION" -f "$TEMPLATE_FILE" - fi +# Count files to apply +NUM_FILES=$(find "$TEMP_APPLY_DIR" -type f -name "*.yaml" | wc -l | tr -d ' ') +if [[ "$NUM_FILES" -gt 0 ]]; then + echo "Applying $NUM_FILES resources with prune enabled..." + echo "kubectl $ACTION -f $TEMP_APPLY_DIR/ --prune -l nullplatform.com/managed-by=endpoint-exposer,nullplatform.com/service-id=$SERVICE_ID" + + if [[ "$DRY_RUN" == "false" ]]; then + # Apply all resources with prune to remove resources no longer in the manifests + kubectl "$ACTION" -f "$TEMP_APPLY_DIR/" \ + --prune \ + -l "nullplatform.com/managed-by=endpoint-exposer,nullplatform.com/service-id=$SERVICE_ID" + fi +else + echo "No resources to apply" +fi + +# Move processed files to apply directory +find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" -o -path "*/batch-apply" \) -prune -o -type f -name "*.yaml" -print | while read -r TEMPLATE_FILE; do BASE_DIR="$(dirname "$TEMPLATE_FILE")" FILENAME="$(basename "$TEMPLATE_FILE")" DEST_DIR="${BASE_DIR}/$ACTION" @@ -20,6 +41,9 @@ find "$OUTPUT_DIR" \( -path "*/apply" -o -path "*/delete" \) -prune -o -type f - mv "$TEMPLATE_FILE" "$DEST_DIR/$FILENAME" done +# Cleanup temporary directory +rm -rf "$TEMP_APPLY_DIR" + if [[ "$DRY_RUN" == "true" ]]; then exit 1 fi \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_httproute b/endpoint-exposer/scripts/istio/build_httproute index ae92b9c..63b2455 100755 --- a/endpoint-exposer/scripts/istio/build_httproute +++ b/endpoint-exposer/scripts/istio/build_httproute @@ -31,67 +31,74 @@ NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') echo "Number of $VISIBILITY routes: $NUM_ROUTES" echo "$VISIBILITY Domain: '$DOMAIN'" -# Skip if no routes -if [[ "$NUM_ROUTES" -eq 0 ]]; then - echo "No $VISIBILITY routes configured, skipping..." - exit 0 +# Check if domain is empty/null +DOMAIN_EMPTY=false +if [[ -z "$DOMAIN" ]] || [[ "$DOMAIN" == "null" ]] || [[ "$DOMAIN" == "\"null\"" ]]; then + DOMAIN_EMPTY=true fi -# Skip if domain is empty/null -if [[ -z "$DOMAIN" ]] || [[ "$DOMAIN" == "null" ]] || [[ "$DOMAIN" == "\"null\"" ]]; then - echo "WARNING: $VISIBILITY routes exist but no $VISIBILITY domain configured!" - echo "Skipping $VISIBILITY HTTPRoute generation..." - exit 0 +# Determine if we should create an empty resource +CREATE_EMPTY=false +if [[ "$NUM_ROUTES" -eq 0 ]] || [[ "$DOMAIN_EMPTY" == "true" ]]; then + CREATE_EMPTY=true + echo "No $VISIBILITY routes or domain configured, creating empty resource for cleanup..." fi -# Create HTTPRoute from template -TEMPLATE="$SERVICE_PATH/templates/istio/httproute.yaml.tpl" +# Create HTTPRoute HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-$SUFFIX.yaml" -# Build context for template -HTTPROUTE_CONTEXT=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg domain "$DOMAIN" \ - --arg gateway_name "$GATEWAY_NAME" \ - --arg gateway_namespace "$GATEWAY_NAMESPACE" \ - --arg suffix "$SUFFIX" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - k8s_namespace: $k8s_namespace, - domain: $domain, - gateway_name: $gateway_name, - gateway_namespace: $gateway_namespace, - suffix: $suffix - }') - -CONTEXT_PATH="$OUTPUT_DIR/httproute-$SUFFIX-context-$SERVICE_ID.json" -echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" - -echo "Generating HTTPRoute from template: $TEMPLATE" -gomplate -c .="$CONTEXT_PATH" \ - -f "$TEMPLATE" \ - -o "$HTTPROUTE_FILE" - -rm "$CONTEXT_PATH" - -# Now process routes using the existing logic -# Temporarily override ROUTES_JSON with visibility-specific routes -ORIGINAL_ROUTES_JSON="${ROUTES_JSON:-[]}" -export ROUTES_JSON="$ROUTES_JSON" -export HTTPROUTE_FILE="$HTTPROUTE_FILE" - -# Process routes (skip if no routes) -if [[ "$NUM_ROUTES" -gt 0 ]]; then +if [[ "$CREATE_EMPTY" == "true" ]]; then + # Don't create the file - kubectl apply with --prune will delete it + echo "Skipping $VISIBILITY HTTPRoute generation - resource will be pruned if it exists" + # Still export the file path but point to a marker file for cleanup tracking + touch "$OUTPUT_DIR/.httproute-$SUFFIX-deleted" +else + # Create HTTPRoute from template + TEMPLATE="$SERVICE_PATH/templates/istio/httproute.yaml.tpl" + + # Build context for template + HTTPROUTE_CONTEXT=$(jq -n \ + --arg service_slug "$SERVICE_SLUG" \ + --arg service_id "$SERVICE_ID" \ + --arg k8s_namespace "$K8S_NAMESPACE" \ + --arg domain "$DOMAIN" \ + --arg gateway_name "$GATEWAY_NAME" \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg suffix "$SUFFIX" \ + '{ + service_slug: $service_slug, + service_id: $service_id, + k8s_namespace: $k8s_namespace, + domain: $domain, + gateway_name: $gateway_name, + gateway_namespace: $gateway_namespace, + suffix: $suffix + }') + + CONTEXT_PATH="$OUTPUT_DIR/httproute-$SUFFIX-context-$SERVICE_ID.json" + echo "$HTTPROUTE_CONTEXT" > "$CONTEXT_PATH" + + echo "Generating HTTPRoute from template: $TEMPLATE" + gomplate -c .="$CONTEXT_PATH" \ + -f "$TEMPLATE" \ + -o "$HTTPROUTE_FILE" + + rm "$CONTEXT_PATH" + + # Now process routes using the existing logic + # Temporarily override ROUTES_JSON with visibility-specific routes + ORIGINAL_ROUTES_JSON="${ROUTES_JSON:-[]}" + export ROUTES_JSON="$ROUTES_JSON" + export HTTPROUTE_FILE="$HTTPROUTE_FILE" + + # Process routes source "$SERVICE_PATH/scripts/istio/process_routes" -fi -# Restore original ROUTES_JSON -export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" + # Restore original ROUTES_JSON + export ROUTES_JSON="$ORIGINAL_ROUTES_JSON" -echo "✅ $VISIBILITY HTTPRoute created: $HTTPROUTE_FILE" + echo "✅ $VISIBILITY HTTPRoute created: $HTTPROUTE_FILE" +fi # Export file path based on visibility if [[ "$VISIBILITY" == "public" ]]; then diff --git a/endpoint-exposer/templates/istio/httproute.yaml.tpl b/endpoint-exposer/templates/istio/httproute.yaml.tpl index ded2bba..728c0aa 100644 --- a/endpoint-exposer/templates/istio/httproute.yaml.tpl +++ b/endpoint-exposer/templates/istio/httproute.yaml.tpl @@ -4,9 +4,9 @@ metadata: name: {{ .service_slug }}-{{ .service_id }}-{{ .suffix }} namespace: {{ .k8s_namespace }} labels: - nullplatform: "true" - service_slug: {{ .service_slug }} - service_id: {{ .service_id }} + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "{{ .service_id }}" + app.kubernetes.io/name: {{ .service_slug }} spec: parentRefs: - name: {{ .gateway_name }} From 0081af2a3ee838d4ccc0f7a7a5e2b92abc5a9b74 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 12 Jan 2026 16:44:26 -0300 Subject: [PATCH 031/101] feat: unify private and public logic --- endpoint-exposer/scripts/common/apply | 54 ++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/endpoint-exposer/scripts/common/apply b/endpoint-exposer/scripts/common/apply index 45d699b..5c0c937 100644 --- a/endpoint-exposer/scripts/common/apply +++ b/endpoint-exposer/scripts/common/apply @@ -2,8 +2,52 @@ set -euo pipefail +# Load configuration +source "$SERVICE_PATH/scripts/istio/config" + echo "TEMPLATE DIR: $OUTPUT_DIR, ACTION: $ACTION, DRY_RUN: $DRY_RUN" +# Helper function to delete a resource if it exists +delete_if_exists() { + local resource_type="$1" + local resource_name="$2" + local namespace="$3" + + if kubectl get "$resource_type" "$resource_name" -n "$namespace" &>/dev/null; then + echo "Deleting $resource_type: $resource_name in namespace $namespace" + if [[ "$DRY_RUN" == "false" ]]; then + kubectl delete "$resource_type" "$resource_name" -n "$namespace" + fi + else + echo "$resource_type $resource_name not found in namespace $namespace (already deleted or never existed)" + fi +} + +# Check for marker files indicating resources should be deleted +if [[ -f "$OUTPUT_DIR/.httproute-public-deleted" ]]; then + echo "Public HTTPRoute marked for deletion" + delete_if_exists "httproute" "$SERVICE_SLUG-$SERVICE_ID-public" "$K8S_NAMESPACE" + rm "$OUTPUT_DIR/.httproute-public-deleted" +fi + +if [[ -f "$OUTPUT_DIR/.httproute-private-deleted" ]]; then + echo "Private HTTPRoute marked for deletion" + delete_if_exists "httproute" "$SERVICE_SLUG-$SERVICE_ID-private" "$K8S_NAMESPACE" + rm "$OUTPUT_DIR/.httproute-private-deleted" +fi + +if [[ -f "$OUTPUT_DIR/.authz-public-deleted" ]]; then + echo "Public AuthorizationPolicy marked for deletion" + delete_if_exists "authorizationpolicy" "$SERVICE_SLUG-$SERVICE_ID-authz-public" "$GATEWAY_NAMESPACE" + rm "$OUTPUT_DIR/.authz-public-deleted" +fi + +if [[ -f "$OUTPUT_DIR/.authz-private-deleted" ]]; then + echo "Private AuthorizationPolicy marked for deletion" + delete_if_exists "authorizationpolicy" "$SERVICE_SLUG-$SERVICE_ID-authz-private" "$GATEWAY_NAMESPACE" + rm "$OUTPUT_DIR/.authz-private-deleted" +fi + # Collect all yaml files into a temporary directory for batch apply TEMP_APPLY_DIR="$OUTPUT_DIR/batch-apply" mkdir -p "$TEMP_APPLY_DIR" @@ -18,14 +62,12 @@ done NUM_FILES=$(find "$TEMP_APPLY_DIR" -type f -name "*.yaml" | wc -l | tr -d ' ') if [[ "$NUM_FILES" -gt 0 ]]; then - echo "Applying $NUM_FILES resources with prune enabled..." - echo "kubectl $ACTION -f $TEMP_APPLY_DIR/ --prune -l nullplatform.com/managed-by=endpoint-exposer,nullplatform.com/service-id=$SERVICE_ID" + echo "Applying $NUM_FILES resources..." + echo "kubectl $ACTION -f $TEMP_APPLY_DIR/" if [[ "$DRY_RUN" == "false" ]]; then - # Apply all resources with prune to remove resources no longer in the manifests - kubectl "$ACTION" -f "$TEMP_APPLY_DIR/" \ - --prune \ - -l "nullplatform.com/managed-by=endpoint-exposer,nullplatform.com/service-id=$SERVICE_ID" + # Apply all resources + kubectl "$ACTION" -f "$TEMP_APPLY_DIR/" fi else echo "No resources to apply" From 70abf3acf70d0d500841a8eb6d9f47ceef83f106 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 12 Jan 2026 20:56:33 -0300 Subject: [PATCH 032/101] feat: stable authorization and tests --- .../.authz-private-deleted | 0 .../.authz-public-deleted | 0 .../.httproute-private-deleted | 0 .../.httproute-public-deleted | 0 ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 11 + ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 11 + ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 12 + ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 12 + ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 7 + ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 7 + ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 8 + ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 8 + .../null-null/.authz-private-deleted | 0 .../null-null/.authz-public-deleted | 0 .../null-null/.httproute-public-deleted | 0 endpoint-exposer/scripts/common/apply | 5 +- endpoint-exposer/scripts/istio/build_context | 10 +- .../scripts/istio/build_httproute | 2 +- .../scripts/istio/fetch_provider_data | 7 +- endpoint-exposer/test/.gitignore | 11 + endpoint-exposer/test/CONTRIBUTING.md | 305 ++++++++++++++++++ endpoint-exposer/test/README.md | 88 +++++ .../test/fixtures/authorization-disabled.json | 83 +++++ .../test/fixtures/no-public-routes.json | 71 ++++ .../fixtures/public-and-private-routes.json | 95 ++++++ .../test/fixtures/simple-public-routes.json | 83 +++++ endpoint-exposer/test/helpers.bash | 298 +++++++++++++++++ endpoint-exposer/test/run-tests.sh | 83 +++++ endpoint-exposer/test/test_apply_cleanup.bats | 139 ++++++++ .../test/test_authorization_policy.bats | 127 ++++++++ endpoint-exposer/test/test_build_context.bats | 98 ++++++ .../test/test_build_httproute.bats | 179 ++++++++++ endpoint-exposer/test/test_integration.bats | 262 +++++++++++++++ endpoint-exposer/workflows/istio/create.yaml | 2 - endpoint-exposer/workflows/istio/read.yaml | 2 - endpoint-exposer/workflows/istio/update.yaml | 2 - 36 files changed, 2009 insertions(+), 19 deletions(-) create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-private-deleted create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-public-deleted create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-private-deleted create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-public-deleted create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml create mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml create mode 100644 endpoint-exposer/output/test-test-id/null-null/.authz-private-deleted create mode 100644 endpoint-exposer/output/test-test-id/null-null/.authz-public-deleted create mode 100644 endpoint-exposer/output/test-test-id/null-null/.httproute-public-deleted create mode 100644 endpoint-exposer/test/.gitignore create mode 100644 endpoint-exposer/test/CONTRIBUTING.md create mode 100644 endpoint-exposer/test/README.md create mode 100644 endpoint-exposer/test/fixtures/authorization-disabled.json create mode 100644 endpoint-exposer/test/fixtures/no-public-routes.json create mode 100644 endpoint-exposer/test/fixtures/public-and-private-routes.json create mode 100644 endpoint-exposer/test/fixtures/simple-public-routes.json create mode 100644 endpoint-exposer/test/helpers.bash create mode 100755 endpoint-exposer/test/run-tests.sh create mode 100644 endpoint-exposer/test/test_apply_cleanup.bats create mode 100644 endpoint-exposer/test/test_authorization_policy.bats create mode 100644 endpoint-exposer/test/test_build_context.bats create mode 100644 endpoint-exposer/test/test_build_httproute.bats create mode 100644 endpoint-exposer/test/test_integration.bats diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-private-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-private-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-public-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-public-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-private-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-private-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-public-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-public-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml new file mode 100644 index 0000000..0901ea7 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml @@ -0,0 +1,11 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-private + namespace: nullplatform + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" + app.kubernetes.io/name: api +spec: + action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml new file mode 100644 index 0000000..d6aa398 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml @@ -0,0 +1,11 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-public + namespace: nullplatform + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" + app.kubernetes.io/name: api +spec: + action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml new file mode 100644 index 0000000..7a0b160 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private + namespace: nullplatform + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" + app.kubernetes.io/name: api +spec: + hostnames: + - api-private.edenred.nullimplementation.com diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml new file mode 100644 index 0000000..bfe1e9d --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public + namespace: nullplatform + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" + app.kubernetes.io/name: api +spec: + hostnames: + - api.edenred.nullimplementation.com diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml new file mode 100644 index 0000000..d3d3ae3 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml @@ -0,0 +1,7 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-test + namespace: gateways +spec: + action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml new file mode 100644 index 0000000..d3d3ae3 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml @@ -0,0 +1,7 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-test + namespace: gateways +spec: + action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml new file mode 100644 index 0000000..2a72402 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml @@ -0,0 +1,8 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public + namespace: nullplatform +spec: + hostnames: + - diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml new file mode 100644 index 0000000..2a72402 --- /dev/null +++ b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml @@ -0,0 +1,8 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public + namespace: nullplatform +spec: + hostnames: + - diff --git a/endpoint-exposer/output/test-test-id/null-null/.authz-private-deleted b/endpoint-exposer/output/test-test-id/null-null/.authz-private-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/output/test-test-id/null-null/.authz-public-deleted b/endpoint-exposer/output/test-test-id/null-null/.authz-public-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/output/test-test-id/null-null/.httproute-public-deleted b/endpoint-exposer/output/test-test-id/null-null/.httproute-public-deleted new file mode 100644 index 0000000..e69de29 diff --git a/endpoint-exposer/scripts/common/apply b/endpoint-exposer/scripts/common/apply index 5c0c937..1b092ff 100644 --- a/endpoint-exposer/scripts/common/apply +++ b/endpoint-exposer/scripts/common/apply @@ -86,6 +86,5 @@ done # Cleanup temporary directory rm -rf "$TEMP_APPLY_DIR" -if [[ "$DRY_RUN" == "true" ]]; then - exit 1 -fi \ No newline at end of file +# Note: DRY_RUN is for testing - we exit 0 even in dry run mode +exit 0 \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index 0900bc8..080403a 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -12,6 +12,8 @@ ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.publicDomain // .parameters.public_domain // ""') PRIVATE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.privateDomain // .parameters.private_domain // ""') +K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" + # Extract routes array from parameters ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') @@ -21,10 +23,12 @@ PRIVATE_ROUTES_JSON=$(echo "$ROUTES_JSON" | jq -c '[.[] | select(.visibility == CONTEXT=$(echo "$CONTEXT" | jq \ --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg alb_name "$ALB_NAME" \ - '. + {k8s_namespace: $k8s_namespace, alb_name: $alb_name}') + '. + {k8s_namespace: $k8s_namespace}') -export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_SLUG-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" +# Only set OUTPUT_DIR if not already set (allows tests to override) +if [[ -z "$OUTPUT_DIR" ]]; then + export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_SLUG-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" +fi mkdir -p "$OUTPUT_DIR" diff --git a/endpoint-exposer/scripts/istio/build_httproute b/endpoint-exposer/scripts/istio/build_httproute index 63b2455..64162f4 100755 --- a/endpoint-exposer/scripts/istio/build_httproute +++ b/endpoint-exposer/scripts/istio/build_httproute @@ -8,7 +8,7 @@ source "$SERVICE_PATH/scripts/istio/config" # Parameters (must be set by caller) VISIBILITY="${VISIBILITY:-public}" # "public" or "private" -echo "=== Building ${VISIBILITY^} HTTPRoute ===" +echo "=== Building ${VISIBILITY} HTTPRoute ===" # Set variables based on visibility if [[ "$VISIBILITY" == "public" ]]; then diff --git a/endpoint-exposer/scripts/istio/fetch_provider_data b/endpoint-exposer/scripts/istio/fetch_provider_data index 282c9a1..0b29ce3 100755 --- a/endpoint-exposer/scripts/istio/fetch_provider_data +++ b/endpoint-exposer/scripts/istio/fetch_provider_data @@ -14,9 +14,4 @@ fi # K8S_NAMESPACE=$(echo "$PROVIDER_DATA" | jq -r .attributes.cluster.namespace) -ALB_NAME=$(echo "$PROVIDER_DATA" | jq -r --arg default "k8s-nullplatform-internet-facing" ' - .attributes.balancer.public_name // $default -') - -export K8S_NAMESPACE -export ALB_NAME \ No newline at end of file +export K8S_NAMESPACE \ No newline at end of file diff --git a/endpoint-exposer/test/.gitignore b/endpoint-exposer/test/.gitignore new file mode 100644 index 0000000..f426236 --- /dev/null +++ b/endpoint-exposer/test/.gitignore @@ -0,0 +1,11 @@ +# Test temporary files +*.tmp +*.log + +# BATS test outputs +test-*.tap +test-*.xml + +# Temporary directories created during tests +tmp/ +temp/ diff --git a/endpoint-exposer/test/CONTRIBUTING.md b/endpoint-exposer/test/CONTRIBUTING.md new file mode 100644 index 0000000..9b2b888 --- /dev/null +++ b/endpoint-exposer/test/CONTRIBUTING.md @@ -0,0 +1,305 @@ +# Contributing to Tests + +## Adding New Tests + +### 1. Create a New Test File + +Create a new file named `test_.bats`: + +```bash +#!/usr/bin/env bats + +load helpers + +@test "feature: description of what is being tested" { + # Setup test data + export CONTEXT=$(load_fixture "fixture-name") + source "$SERVICE_PATH/scripts/istio/build_context" + + # Execute the code under test + run bash "$SERVICE_PATH/scripts/your-script" + + # Assert results + assert_success + assert_output --partial "expected output" + assert_file_exists "$OUTPUT_DIR/expected-file.yaml" + assert_file_contains "$OUTPUT_DIR/expected-file.yaml" "expected content" +} +``` + +### 2. Add Test Fixtures + +Create fixture files in `fixtures/` directory: + +```bash +# fixtures/my-new-scenario.json +{ + "service": { + "id": "test-id", + "slug": "test-service" + }, + "parameters": { + "publicDomain": "test.example.com", + "privateDomain": "test-private.example.com", + "authorization": { + "enabled": true + } + }, + "routes": [ + { + "path": "/api/test", + "method": "GET", + "scope": "test:read", + "visibility": "public" + } + ] +} +``` + +### 3. Use Helper Functions + +Available helpers from `helpers.bash`: + +#### Setup/Teardown +- `setup()` - Automatically called before each test +- `teardown()` - Automatically called after each test + +#### File Assertions +- `assert_file_exists ` - Assert file exists +- `assert_file_not_exists ` - Assert file does not exist +- `assert_file_contains ` - Assert file contains string +- `assert_file_not_contains ` - Assert file does not contain string +- `assert_yaml_contains ` - Assert YAML has key-value pair + +#### Fixtures +- `load_fixture ` - Load a fixture JSON file +- `create_test_context ` - Create minimal context +- `add_route_to_context ` - Add route to context + +#### Mocking +- `mock_kubectl()` - Create a mock kubectl command + +### 4. Test Structure Best Practices + +#### Arrange-Act-Assert Pattern + +```bash +@test "description" { + # Arrange - Setup test data + export CONTEXT=$(load_fixture "scenario") + source "$SERVICE_PATH/scripts/istio/build_context" + + # Act - Execute the code + run bash "$SERVICE_PATH/scripts/my-script" + + # Assert - Verify results + assert_success + assert_file_exists "$OUTPUT_DIR/output.yaml" +} +``` + +#### Test One Thing + +Each test should verify one specific behavior: + +```bash +# Good - tests one thing +@test "build_httproute: creates public HTTPRoute when routes exist" { + # ... +} + +# Good - tests one thing +@test "build_httproute: creates marker when no routes exist" { + # ... +} + +# Bad - tests multiple things +@test "build_httproute: handles all scenarios" { + # ... tests too many things +} +``` + +#### Descriptive Test Names + +Use the format: `: ` + +```bash +@test "authorization_policy: generates policy when enabled and routes exist" +@test "authorization_policy: creates marker when disabled" +@test "build_httproute: fails with invalid visibility parameter" +``` + +### 5. Testing Different Scenarios + +#### Test Success Cases + +```bash +@test "script: succeeds with valid input" { + export CONTEXT=$(load_fixture "valid-scenario") + run bash "$SERVICE_PATH/scripts/my-script" + assert_success +} +``` + +#### Test Failure Cases + +```bash +@test "script: fails with invalid input" { + export CONTEXT='{"invalid": "data"}' + run bash "$SERVICE_PATH/scripts/my-script" + assert_failure +} +``` + +#### Test Edge Cases + +```bash +@test "script: handles empty routes array" { + export CONTEXT=$(create_test_context "id" "slug" "" "") + # ... +} + +@test "script: handles missing optional parameters" { + export CONTEXT='{ + "service": {"id": "test", "slug": "test"}, + "parameters": {}, + "routes": [] + }' + # ... +} +``` + +### 6. Integration Tests + +For end-to-end workflow tests: + +```bash +@test "integration: complete update workflow" { + export CONTEXT=$(load_fixture "complete-scenario") + + # Step 1: Build context + source "$SERVICE_PATH/scripts/istio/build_context" + + # Step 2: Build httproutes + export VISIBILITY="public" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + export VISIBILITY="private" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Step 3: Generate authorization + bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + # Step 4: Apply + run bash "$SERVICE_PATH/scripts/common/apply" + + # Assert complete workflow + assert_success + assert_file_exists "$OUTPUT_DIR/httproute-*-public.yaml" + # ... more assertions +} +``` + +### 7. Running Your Tests + +Run a specific test file: +```bash +bats test_my_feature.bats +``` + +Run all tests: +```bash +./run-tests.sh +``` + +Run with verbose output: +```bash +bats -t test_my_feature.bats +``` + +### 8. Debugging Tests + +Add debug output: +```bash +@test "my test" { + # Print variable values + echo "CONTEXT: $CONTEXT" >&3 + echo "OUTPUT_DIR: $OUTPUT_DIR" >&3 + + # Show file contents + cat "$OUTPUT_DIR/somefile.yaml" >&3 + + # ... rest of test +} +``` + +Run with trace: +```bash +bats -x test_my_feature.bats +``` + +### 9. Common Patterns + +#### Testing with Different Contexts + +```bash +@test "script: handles scenario A" { + export CONTEXT=$(load_fixture "scenario-a") + # ... test +} + +@test "script: handles scenario B" { + export CONTEXT=$(load_fixture "scenario-b") + # ... test +} +``` + +#### Testing File Generation + +```bash +@test "script: generates correct file" { + # ... run script + + # Check file exists + assert_file_exists "$OUTPUT_DIR/generated.yaml" + + # Check content + assert_file_contains "$OUTPUT_DIR/generated.yaml" "expected: value" + + # Check YAML structure + assert_yaml_contains "$OUTPUT_DIR/generated.yaml" ".metadata.name" "expected-name" +} +``` + +#### Testing Cleanup Behavior + +```bash +@test "script: creates cleanup marker when needed" { + # ... run script that should create marker + + assert_file_exists "$OUTPUT_DIR/.marker-deleted" + assert_file_not_exists "$OUTPUT_DIR/actual-resource.yaml" +} +``` + +### 10. Adding Tests to CI/CD + +The test suite can be integrated into CI/CD pipelines: + +```yaml +# Example GitHub Actions workflow +- name: Run tests + run: | + cd test + ./run-tests.sh +``` + +### 11. Test Coverage Guidelines + +Aim to test: +- ✅ Happy paths (normal operation) +- ✅ Error conditions (invalid input, missing data) +- ✅ Edge cases (empty arrays, null values, special characters) +- ✅ Integration scenarios (complete workflows) +- ✅ Cleanup behavior (resource deletion) +- ✅ Configuration variations (enabled/disabled features) diff --git a/endpoint-exposer/test/README.md b/endpoint-exposer/test/README.md new file mode 100644 index 0000000..6f0f906 --- /dev/null +++ b/endpoint-exposer/test/README.md @@ -0,0 +1,88 @@ +# Endpoint Exposer Tests + +This directory contains tests for the endpoint-exposer service using BATS (Bash Automated Testing System). + +## Prerequisites + +Install BATS: +```bash +# macOS +brew install bats-core + +# Linux +git clone https://github.com/bats-core/bats-core.git +cd bats-core +sudo ./install.sh /usr/local +``` + +## Running Tests + +Run all tests: +```bash +cd test +./run-tests.sh +``` + +Run a specific test file: +```bash +bats test_istio_workflows.bats +``` + +## Test Structure + +- `fixtures/` - Test data and context files +- `helpers.bash` - Common test helper functions +- `test_*.bats` - Test files +- `run-tests.sh` - Script to run all tests + +## Writing Tests + +Tests validate that given a specific context, the correct output files are generated without actually applying to Kubernetes. + +### Context Structure + +The test fixtures use the full nullplatform action context structure: + +```json +{ + "action": "service:action:update", + "id": "action-id", + "parameters": { + "routes": [...], + "public_domain": "...", + "private_domain": "...", + "authorization": { "enabled": true/false } + }, + "service": { + "id": "service-id", + "slug": "service-slug", + "attributes": { + "routes": [...], + "public_domain": "...", + "authorization": { "enabled": true/false } + } + }, + "tags": {...}, + ... +} +``` + +### Example Test + +```bash +@test "description" { + # Load a fixture with the full context structure + export CONTEXT=$(load_fixture "simple-public-routes") + + # Run workflow step + run bash "$SERVICE_PATH/scripts/istio/build_context" + + # Assert results + assert_success + assert_output --partial "expected output" + + # Verify generated files + assert_file_exists "$OUTPUT_DIR/httproute-service-id-public.yaml" + assert_file_contains "$OUTPUT_DIR/httproute-service-id-public.yaml" "expected content" +} +``` diff --git a/endpoint-exposer/test/fixtures/authorization-disabled.json b/endpoint-exposer/test/fixtures/authorization-disabled.json new file mode 100644 index 0000000..d9ed236 --- /dev/null +++ b/endpoint-exposer/test/fixtures/authorization-disabled.json @@ -0,0 +1,83 @@ +{ + "action": "service:action:update", + "id": "5b7636e1-304b-4ef9-92a9-2a0b102686f4", + "name": "update-api", + "slug": "update-api", + "status": "pending", + "created_at": "2026-01-12T19:50:09.357Z", + "updated_at": "2026-01-12T19:50:09.357Z", + "parameters": { + "routes": [ + { + "visibility": "public", + "path": "/api/users", + "scope": "users:read", + "method": "GET" + }, + { + "visibility": "private", + "path": "/api/admin", + "scope": "admin:read", + "method": "GET" + } + ], + "public_domain": "api.edenred.nullimplementation.com", + "authorization": { + "enabled": false + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "results": {}, + "type": "update", + "specification": { + "id": "4c85dfb2-b489-4cd0-af0f-651f670ac32a", + "slug": "update-endpoint-exposer" + }, + "service": { + "id": "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd", + "slug": "api", + "attributes": { + "routes": [ + { + "visibility": "public", + "path": "/api/users", + "scope": "users:read", + "method": "GET" + }, + { + "visibility": "private", + "path": "/api/admin", + "scope": "admin:read", + "method": "GET" + } + ], + "public_domain": "api.edenred.nullimplementation.com", + "authorization": { + "enabled": false + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "type": "dependency", + "specification": { + "id": "7e71962b-1282-4131-84ae-bf7687238c74", + "slug": "endpoint-exposer" + }, + "dimensions": {} + }, + "link": null, + "user": { + "id": 1621446846, + "email": "javier.solis+edenred@nullplatform.io" + }, + "tags": { + "organization_id": "1858797910", + "organization": "edenred", + "namespace_id": "1340017944", + "namespace": "playground", + "account_id": "758973013", + "account": "playground", + "application_id": "179976948", + "application": "floppy-bird-api" + }, + "entity_nrn": "organization=1858797910:account=758973013:namespace=1340017944:application=179976948" +} diff --git a/endpoint-exposer/test/fixtures/no-public-routes.json b/endpoint-exposer/test/fixtures/no-public-routes.json new file mode 100644 index 0000000..0980832 --- /dev/null +++ b/endpoint-exposer/test/fixtures/no-public-routes.json @@ -0,0 +1,71 @@ +{ + "action": "service:action:update", + "id": "5b7636e1-304b-4ef9-92a9-2a0b102686f4", + "name": "update-api", + "slug": "update-api", + "status": "pending", + "created_at": "2026-01-12T19:50:09.357Z", + "updated_at": "2026-01-12T19:50:09.357Z", + "parameters": { + "routes": [ + { + "visibility": "private", + "path": "/api/admin", + "scope": "admin:read", + "method": "GET" + } + ], + "public_domain": "", + "authorization": { + "enabled": false + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "results": {}, + "type": "update", + "specification": { + "id": "4c85dfb2-b489-4cd0-af0f-651f670ac32a", + "slug": "update-endpoint-exposer" + }, + "service": { + "id": "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd", + "slug": "api", + "attributes": { + "routes": [ + { + "visibility": "private", + "path": "/api/admin", + "scope": "admin:read", + "method": "GET" + } + ], + "public_domain": "", + "authorization": { + "enabled": false + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "type": "dependency", + "specification": { + "id": "7e71962b-1282-4131-84ae-bf7687238c74", + "slug": "endpoint-exposer" + }, + "dimensions": {} + }, + "link": null, + "user": { + "id": 1621446846, + "email": "javier.solis+edenred@nullplatform.io" + }, + "tags": { + "organization_id": "1858797910", + "organization": "edenred", + "namespace_id": "1340017944", + "namespace": "playground", + "account_id": "758973013", + "account": "playground", + "application_id": "179976948", + "application": "floppy-bird-api" + }, + "entity_nrn": "organization=1858797910:account=758973013:namespace=1340017944:application=179976948" +} diff --git a/endpoint-exposer/test/fixtures/public-and-private-routes.json b/endpoint-exposer/test/fixtures/public-and-private-routes.json new file mode 100644 index 0000000..efff3d4 --- /dev/null +++ b/endpoint-exposer/test/fixtures/public-and-private-routes.json @@ -0,0 +1,95 @@ +{ + "action": "service:action:update", + "id": "5b7636e1-304b-4ef9-92a9-2a0b102686f4", + "name": "update-api", + "slug": "update-api", + "status": "pending", + "created_at": "2026-01-12T19:50:09.357Z", + "updated_at": "2026-01-12T19:50:09.357Z", + "parameters": { + "routes": [ + { + "visibility": "public", + "path": "/api/users", + "scope": "users:read", + "method": "GET" + }, + { + "visibility": "private", + "path": "/api/admin", + "scope": "admin:read", + "method": "GET" + }, + { + "visibility": "private", + "path": "/api/admin/users", + "scope": "admin:users:write", + "method": "POST" + } + ], + "public_domain": "api.edenred.nullimplementation.com", + "authorization": { + "enabled": true + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "results": {}, + "type": "update", + "specification": { + "id": "4c85dfb2-b489-4cd0-af0f-651f670ac32a", + "slug": "update-endpoint-exposer" + }, + "service": { + "id": "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd", + "slug": "api", + "attributes": { + "routes": [ + { + "visibility": "public", + "path": "/api/users", + "scope": "users:read", + "method": "GET" + }, + { + "visibility": "private", + "path": "/api/admin", + "scope": "admin:read", + "method": "GET" + }, + { + "visibility": "private", + "path": "/api/admin/users", + "scope": "admin:users:write", + "method": "POST" + } + ], + "public_domain": "api.edenred.nullimplementation.com", + "authorization": { + "enabled": true + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "type": "dependency", + "specification": { + "id": "7e71962b-1282-4131-84ae-bf7687238c74", + "slug": "endpoint-exposer" + }, + "dimensions": {} + }, + "link": null, + "user": { + "id": 1621446846, + "email": "javier.solis+edenred@nullplatform.io" + }, + "tags": { + "organization_id": "1858797910", + "organization": "edenred", + "namespace_id": "1340017944", + "namespace": "playground", + "account_id": "758973013", + "account": "playground", + "application_id": "179976948", + "application": "floppy-bird-api" + }, + "entity_nrn": "organization=1858797910:account=758973013:namespace=1340017944:application=179976948" +} diff --git a/endpoint-exposer/test/fixtures/simple-public-routes.json b/endpoint-exposer/test/fixtures/simple-public-routes.json new file mode 100644 index 0000000..1a1d16e --- /dev/null +++ b/endpoint-exposer/test/fixtures/simple-public-routes.json @@ -0,0 +1,83 @@ +{ + "action": "service:action:update", + "id": "5b7636e1-304b-4ef9-92a9-2a0b102686f4", + "name": "update-api", + "slug": "update-api", + "status": "pending", + "created_at": "2026-01-12T19:50:09.357Z", + "updated_at": "2026-01-12T19:50:09.357Z", + "parameters": { + "routes": [ + { + "visibility": "public", + "path": "/api/users", + "scope": "users:read", + "method": "GET" + }, + { + "visibility": "public", + "path": "/api/users", + "scope": "users:write", + "method": "POST" + } + ], + "public_domain": "api.edenred.nullimplementation.com", + "authorization": { + "enabled": false + }, + "private_domain": "" + }, + "results": {}, + "type": "update", + "specification": { + "id": "4c85dfb2-b489-4cd0-af0f-651f670ac32a", + "slug": "update-endpoint-exposer" + }, + "service": { + "id": "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd", + "slug": "api", + "attributes": { + "routes": [ + { + "visibility": "public", + "path": "/api/users", + "scope": "users:read", + "method": "GET" + }, + { + "visibility": "public", + "path": "/api/users", + "scope": "users:write", + "method": "POST" + } + ], + "public_domain": "api.edenred.nullimplementation.com", + "authorization": { + "enabled": false + }, + "private_domain": "api-private.edenred.nullimplementation.com" + }, + "type": "dependency", + "specification": { + "id": "7e71962b-1282-4131-84ae-bf7687238c74", + "slug": "endpoint-exposer" + }, + "dimensions": {} + }, + "link": null, + "user": { + "id": 1621446846, + "email": "javier.solis+edenred@nullplatform.io" + }, + "tags": { + "organization_id": "1858797910", + "organization": "edenred", + "namespace_id": "1340017944", + "namespace": "playground", + "account_id": "758973013", + "account": "playground", + "application_id": "179976948", + "application": "floppy-bird-api" + }, + "entity_nrn": "organization=1858797910:account=758973013:namespace=1340017944:application=179976948" +} diff --git a/endpoint-exposer/test/helpers.bash b/endpoint-exposer/test/helpers.bash new file mode 100644 index 0000000..115840e --- /dev/null +++ b/endpoint-exposer/test/helpers.bash @@ -0,0 +1,298 @@ +#!/bin/bash + +# Test helpers for endpoint-exposer tests + +# Setup function called before each test +setup() { + # Create temporary output directory + export TEST_TEMP_DIR="$(mktemp -d)" + export OUTPUT_DIR="$TEST_TEMP_DIR/output" + mkdir -p "$OUTPUT_DIR" + + # Set SERVICE_PATH to parent directory + export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + + # Mock DRY_RUN to true by default to avoid actual kubectl calls + export DRY_RUN="${DRY_RUN:-true}" + export ACTION="${ACTION:-apply}" + + # Load bats support libraries if available + load_bats_support_libraries +} + +# Teardown function called after each test +teardown() { + # Clean up temporary directory + if [[ -n "$TEST_TEMP_DIR" ]] && [[ -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +# Load bats support libraries or define basic assertions +load_bats_support_libraries() { + # Try to load bats-support and bats-assert if available + local loaded=false + if [[ -f "/usr/local/lib/bats-support/load.bash" ]]; then + load "/usr/local/lib/bats-support/load.bash" + loaded=true + fi + if [[ -f "/usr/local/lib/bats-assert/load.bash" ]]; then + load "/usr/local/lib/bats-assert/load.bash" + loaded=true + fi + + # If libraries not loaded, define basic assertion functions + if [[ "$loaded" == "false" ]]; then + # Define assert_success + assert_success() { + if [[ "$status" -ne 0 ]]; then + echo "Expected success (exit 0) but got: $status" >&2 + echo "Output: $output" >&2 + return 1 + fi + } + + # Define assert_failure + assert_failure() { + if [[ "$status" -eq 0 ]]; then + echo "Expected failure (non-zero exit) but got: $status" >&2 + echo "Output: $output" >&2 + return 1 + fi + } + + # Define assert_output + assert_output() { + local expected="" + local partial=false + + while [[ $# -gt 0 ]]; do + case $1 in + --partial) + partial=true + shift + ;; + *) + expected="$1" + shift + ;; + esac + done + + if [[ "$partial" == "true" ]]; then + if [[ "$output" != *"$expected"* ]]; then + echo "Expected output to contain: $expected" >&2 + echo "Actual output: $output" >&2 + return 1 + fi + else + if [[ "$output" != "$expected" ]]; then + echo "Expected output: $expected" >&2 + echo "Actual output: $output" >&2 + return 1 + fi + fi + } + fi +} + +# Assert that a file exists +assert_file_exists() { + local file="$1" + if [[ ! -f "$file" ]]; then + echo "File does not exist: $file" >&2 + return 1 + fi +} + +# Assert that a file does not exist +assert_file_not_exists() { + local file="$1" + if [[ -f "$file" ]]; then + echo "File exists but should not: $file" >&2 + return 1 + fi +} + +# Assert that a file contains a string +assert_file_contains() { + local file="$1" + local expected="$2" + + if [[ ! -f "$file" ]]; then + echo "File does not exist: $file" >&2 + return 1 + fi + + if ! grep -q "$expected" "$file"; then + echo "File does not contain expected string: $expected" >&2 + echo "File contents:" >&2 + cat "$file" >&2 + return 1 + fi +} + +# Assert that a file does not contain a string +assert_file_not_contains() { + local file="$1" + local unexpected="$2" + + if [[ ! -f "$file" ]]; then + echo "File does not exist: $file" >&2 + return 1 + fi + + if grep -q "$unexpected" "$file"; then + echo "File contains unexpected string: $unexpected" >&2 + echo "File contents:" >&2 + cat "$file" >&2 + return 1 + fi +} + +# Assert that a YAML file has a specific key-value pair +assert_yaml_contains() { + local file="$1" + local key="$2" + local expected_value="$3" + + if [[ ! -f "$file" ]]; then + echo "File does not exist: $file" >&2 + return 1 + fi + + local actual_value + actual_value=$(yq eval "$key" "$file" 2>/dev/null || echo "") + + if [[ "$actual_value" != "$expected_value" ]]; then + echo "YAML key '$key' has unexpected value" >&2 + echo "Expected: $expected_value" >&2 + echo "Actual: $actual_value" >&2 + return 1 + fi +} + +# Count the number of YAML documents in a file +count_yaml_documents() { + local file="$1" + grep -c "^---" "$file" || echo "0" +} + +# Load a fixture context file +load_fixture() { + local fixture_name="$1" + local fixture_file="$BATS_TEST_DIRNAME/fixtures/$fixture_name.json" + + if [[ ! -f "$fixture_file" ]]; then + echo "Fixture not found: $fixture_file" >&2 + return 1 + fi + + cat "$fixture_file" +} + +# Mock kubectl to avoid actual API calls +mock_kubectl() { + # Create a mock kubectl script + cat > "$TEST_TEMP_DIR/kubectl" << 'EOF' +#!/bin/bash +echo "Mock kubectl called with: $@" >&2 +exit 0 +EOF + chmod +x "$TEST_TEMP_DIR/kubectl" + export PATH="$TEST_TEMP_DIR:$PATH" +} + +# Create a minimal valid context for testing with full structure +create_test_context() { + local service_id="${1:-test-service-id}" + local service_slug="${2:-test-service}" + local public_domain="${3:-test.example.com}" + local private_domain="${4:-test-private.example.com}" + + cat < /dev/null; then + echo -e "${RED}Error: bats is not installed${NC}" + echo "" + echo "Install bats:" + echo " macOS: brew install bats-core" + echo " Linux: git clone https://github.com/bats-core/bats-core.git && cd bats-core && sudo ./install.sh /usr/local" + echo "" + exit 1 +fi + +# Check if jq is installed (required by tests) +if ! command -v jq &> /dev/null; then + echo -e "${RED}Error: jq is not installed${NC}" + echo "" + echo "Install jq:" + echo " macOS: brew install jq" + echo " Linux: sudo apt-get install jq" + echo "" + exit 1 +fi + +# Change to test directory +cd "$(dirname "$0")" + +# Run tests +echo "Running tests..." +echo "" + +TEST_FILES=( + "test_build_context.bats" + "test_build_httproute.bats" + "test_authorization_policy.bats" + "test_apply_cleanup.bats" + "test_integration.bats" +) + +FAILED=0 +PASSED=0 + +for test_file in "${TEST_FILES[@]}"; do + if [[ -f "$test_file" ]]; then + echo -e "${YELLOW}Running $test_file...${NC}" + if bats "$test_file"; then + ((PASSED++)) + echo -e "${GREEN}✓ $test_file passed${NC}" + else + ((FAILED++)) + echo -e "${RED}✗ $test_file failed${NC}" + fi + echo "" + fi +done + +echo "================================================" +echo " Test Summary" +echo "================================================" +echo -e "Passed: ${GREEN}$PASSED${NC}" +echo -e "Failed: ${RED}$FAILED${NC}" +echo "" + +if [[ $FAILED -gt 0 ]]; then + echo -e "${RED}Some tests failed${NC}" + exit 1 +else + echo -e "${GREEN}All tests passed!${NC}" + exit 0 +fi diff --git a/endpoint-exposer/test/test_apply_cleanup.bats b/endpoint-exposer/test/test_apply_cleanup.bats new file mode 100644 index 0000000..cfc76bd --- /dev/null +++ b/endpoint-exposer/test/test_apply_cleanup.bats @@ -0,0 +1,139 @@ +#!/usr/bin/env bats + +load helpers + +setup() { + export TEST_TEMP_DIR="$(mktemp -d)" + export OUTPUT_DIR="$TEST_TEMP_DIR/output" + mkdir -p "$OUTPUT_DIR" + export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + + # Load assert functions + load_bats_support_libraries + + export K8S_NAMESPACE="test-namespace" + export SERVICE_ID="test-service-id" + export SERVICE_SLUG="test-service" + export ACTION="apply" + export DRY_RUN="true" + + # Mock kubectl + mock_kubectl +} + +@test "apply: detects public httproute marker and attempts deletion" { + # Create marker file + touch "$OUTPUT_DIR/.httproute-public-deleted" + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "Public HTTPRoute marked for deletion" + assert_output --partial "httproute" + assert_output --partial "$SERVICE_SLUG-$SERVICE_ID-public" +} + +@test "apply: detects private httproute marker and attempts deletion" { + # Create marker file + touch "$OUTPUT_DIR/.httproute-private-deleted" + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "Private HTTPRoute marked for deletion" + assert_output --partial "httproute" + assert_output --partial "$SERVICE_SLUG-$SERVICE_ID-private" +} + +@test "apply: detects public authz marker and attempts deletion" { + # Create marker file + touch "$OUTPUT_DIR/.authz-public-deleted" + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "Public AuthorizationPolicy marked for deletion" + assert_output --partial "authorizationpolicy" + assert_output --partial "$SERVICE_SLUG-$SERVICE_ID-authz-public" +} + +@test "apply: detects private authz marker and attempts deletion" { + # Create marker file + touch "$OUTPUT_DIR/.authz-private-deleted" + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "Private AuthorizationPolicy marked for deletion" + assert_output --partial "authorizationpolicy" + assert_output --partial "$SERVICE_SLUG-$SERVICE_ID-authz-private" +} + +@test "apply: handles multiple marker files" { + # Create multiple marker files + touch "$OUTPUT_DIR/.httproute-public-deleted" + touch "$OUTPUT_DIR/.httproute-private-deleted" + touch "$OUTPUT_DIR/.authz-public-deleted" + touch "$OUTPUT_DIR/.authz-private-deleted" + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "Public HTTPRoute marked for deletion" + assert_output --partial "Private HTTPRoute marked for deletion" + assert_output --partial "Public AuthorizationPolicy marked for deletion" + assert_output --partial "Private AuthorizationPolicy marked for deletion" +} + +@test "apply: applies yaml files when present" { + # Create a test yaml file + cat > "$OUTPUT_DIR/test-resource.yaml" << EOF +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +EOF + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "Applying 1 resources" +} + +@test "apply: handles no resources to apply" { + # No yaml files, no markers + + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + assert_output --partial "No resources to apply" +} + +@test "apply: removes marker files after processing" { + # Create marker files + touch "$OUTPUT_DIR/.httproute-public-deleted" + touch "$OUTPUT_DIR/.authz-private-deleted" + + bash "$SERVICE_PATH/scripts/common/apply" + + # Marker files should be removed + assert_file_not_exists "$OUTPUT_DIR/.httproute-public-deleted" + assert_file_not_exists "$OUTPUT_DIR/.authz-private-deleted" +} + +@test "apply: moves yaml files to apply directory after processing" { + # Create a test yaml file + cat > "$OUTPUT_DIR/test-resource.yaml" << EOF +apiVersion: v1 +kind: ConfigMap +metadata: + name: test +EOF + + bash "$SERVICE_PATH/scripts/common/apply" + + # Original file should be moved + assert_file_not_exists "$OUTPUT_DIR/test-resource.yaml" + # Should be in apply directory + assert_file_exists "$OUTPUT_DIR/apply/test-resource.yaml" +} diff --git a/endpoint-exposer/test/test_authorization_policy.bats b/endpoint-exposer/test/test_authorization_policy.bats new file mode 100644 index 0000000..1c73b13 --- /dev/null +++ b/endpoint-exposer/test/test_authorization_policy.bats @@ -0,0 +1,127 @@ +#!/usr/bin/env bats + +load helpers + +setup() { + export TEST_TEMP_DIR="$(mktemp -d)" + export OUTPUT_DIR="$TEST_TEMP_DIR/output" + mkdir -p "$OUTPUT_DIR" + export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + + # Load assert functions + load_bats_support_libraries + + export K8S_NAMESPACE="test-namespace" + + # Mock gomplate + cat > "$TEST_TEMP_DIR/gomplate" << 'EOF' +#!/bin/bash +TEMPLATE_FILE="" +OUTPUT_FILE="" + +while [[ $# -gt 0 ]]; do + case $1 in + -f) TEMPLATE_FILE="$2"; shift 2 ;; + -o) OUTPUT_FILE="$2"; shift 2 ;; + -c) shift 2 ;; + *) shift ;; + esac +done + +if [[ -n "$TEMPLATE_FILE" ]] && [[ -n "$OUTPUT_FILE" ]]; then + cat > "$OUTPUT_FILE" << YAML +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ${SERVICE_SLUG}-${SERVICE_ID}-authz-test + namespace: gateways +spec: + action: CUSTOM +YAML +fi +EOF + chmod +x "$TEST_TEMP_DIR/gomplate" + export PATH="$TEST_TEMP_DIR:$PATH" +} + +@test "authorization_policy: generates policies when enabled" { + export CONTEXT=$(load_fixture "public-and-private-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + run bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + assert_success + assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" +} + +@test "authorization_policy: creates marker files when disabled" { + export CONTEXT=$(load_fixture "authorization-disabled") + source "$SERVICE_PATH/scripts/istio/build_context" + + run bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + assert_success + assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" + assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" + assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" +} + +@test "authorization_policy: creates marker for public when no public routes" { + export CONTEXT=$(load_fixture "no-public-routes") + # Enable authorization + export CONTEXT=$(echo "$CONTEXT" | jq '.parameters.authorization.enabled = true') + source "$SERVICE_PATH/scripts/istio/build_context" + + run bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + assert_success + assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" + assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" +} + +@test "authorization_policy: creates marker for private when no private routes" { + export CONTEXT=$(load_fixture "simple-public-routes") + # Enable authorization + export CONTEXT=$(echo "$CONTEXT" | jq '.parameters.authorization.enabled = true') + source "$SERVICE_PATH/scripts/istio/build_context" + + run bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + assert_success + assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" + assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" +} + +@test "authorization_policy: creates marker when no domain" { + export CONTEXT='{ + "service": {"id": "test-id", "slug": "test"}, + "parameters": { + "publicDomain": "", + "privateDomain": "", + "authorization": {"enabled": true} + }, + "routes": [{"path": "/test", "method": "GET", "scope": "test", "visibility": "public"}] + }' + source "$SERVICE_PATH/scripts/istio/build_context" + + run bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + assert_success + assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" + assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" +} + +@test "authorization_policy: builds rules correctly" { + export CONTEXT=$(load_fixture "public-and-private-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + run bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + assert_success + + # Both files should be created + assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" +} diff --git a/endpoint-exposer/test/test_build_context.bats b/endpoint-exposer/test/test_build_context.bats new file mode 100644 index 0000000..a337351 --- /dev/null +++ b/endpoint-exposer/test/test_build_context.bats @@ -0,0 +1,98 @@ +#!/usr/bin/env bats + +load helpers + +setup() { + # Call parent setup + export TEST_TEMP_DIR="$(mktemp -d)" + export OUTPUT_DIR="$TEST_TEMP_DIR/output" + mkdir -p "$OUTPUT_DIR" + export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + + # Load assert functions + load_bats_support_libraries + + # Mock K8S_NAMESPACE (required by build_context) + export K8S_NAMESPACE="test-namespace" +} + +teardown() { + if [[ -n "$TEST_TEMP_DIR" ]] && [[ -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +@test "build_context: extracts service id and slug correctly" { + export CONTEXT=$(load_fixture "simple-public-routes") + + source "$SERVICE_PATH/scripts/istio/build_context" + + [[ "$SERVICE_ID" == "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" ]] + [[ "$SERVICE_SLUG" == "api" ]] +} + +@test "build_context: extracts public and private domains" { + export CONTEXT=$(load_fixture "public-and-private-routes") + + source "$SERVICE_PATH/scripts/istio/build_context" + + [[ "$PUBLIC_DOMAIN" == "api.edenred.nullimplementation.com" ]] + [[ "$PRIVATE_DOMAIN" == "api-private.edenred.nullimplementation.com" ]] +} + +@test "build_context: splits routes by visibility" { + export CONTEXT=$(load_fixture "public-and-private-routes") + + source "$SERVICE_PATH/scripts/istio/build_context" + + # Check public routes + local num_public=$(echo "$PUBLIC_ROUTES_JSON" | jq 'length') + [[ "$num_public" == "1" ]] + + # Check private routes + local num_private=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') + [[ "$num_private" == "2" ]] +} + +@test "build_context: handles missing visibility as public" { + export CONTEXT='{ + "service": {"id": "test-id", "slug": "test"}, + "parameters": {"publicDomain": "test.com", "privateDomain": ""}, + "routes": [ + {"path": "/test", "method": "GET", "scope": "test:read"} + ] + }' + + source "$SERVICE_PATH/scripts/istio/build_context" + + # Route without visibility should be treated as public + local num_public=$(echo "$PUBLIC_ROUTES_JSON" | jq 'length') + [[ "$num_public" == "1" ]] + + local num_private=$(echo "$PRIVATE_ROUTES_JSON" | jq 'length') + [[ "$num_private" == "0" ]] +} + +@test "build_context: handles empty private domain" { + export CONTEXT=$(load_fixture "simple-public-routes") + + source "$SERVICE_PATH/scripts/istio/build_context" + + [[ "$PUBLIC_DOMAIN" == "api.edenred.nullimplementation.com" ]] + [[ -z "$PRIVATE_DOMAIN" ]] +} + +@test "build_context: exports all required variables" { + export CONTEXT=$(load_fixture "public-and-private-routes") + + source "$SERVICE_PATH/scripts/istio/build_context" + + # Check that all required variables are exported + [[ -n "$SERVICE_ID" ]] + [[ -n "$SERVICE_SLUG" ]] + [[ -n "$PUBLIC_DOMAIN" ]] + [[ -n "$PRIVATE_DOMAIN" ]] + [[ -n "$ROUTES_JSON" ]] + [[ -n "$PUBLIC_ROUTES_JSON" ]] + [[ -n "$PRIVATE_ROUTES_JSON" ]] +} diff --git a/endpoint-exposer/test/test_build_httproute.bats b/endpoint-exposer/test/test_build_httproute.bats new file mode 100644 index 0000000..6a9bf23 --- /dev/null +++ b/endpoint-exposer/test/test_build_httproute.bats @@ -0,0 +1,179 @@ +#!/usr/bin/env bats + +load helpers + +setup() { + # Call parent setup + export TEST_TEMP_DIR="$(mktemp -d)" + export OUTPUT_DIR="$TEST_TEMP_DIR/output" + mkdir -p "$OUTPUT_DIR" + export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + + # Load assert functions + load_bats_support_libraries + + # Mock kubectl and provider data + export K8S_NAMESPACE="test-namespace" + export ALB_NAME="test-alb" + + # Mock gomplate + cat > "$TEST_TEMP_DIR/gomplate" << 'EOF' +#!/bin/bash +# Simple gomplate mock - just copy template to output +TEMPLATE_FILE="" +OUTPUT_FILE="" + +while [[ $# -gt 0 ]]; do + case $1 in + -f) TEMPLATE_FILE="$2"; shift 2 ;; + -o) OUTPUT_FILE="$2"; shift 2 ;; + -c) shift 2 ;; # Ignore context + *) shift ;; + esac +done + +if [[ -n "$TEMPLATE_FILE" ]] && [[ -n "$OUTPUT_FILE" ]]; then + # For testing, just create a valid YAML with the service info + cat > "$OUTPUT_FILE" << YAML +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: ${SERVICE_SLUG}-${SERVICE_ID}-${SUFFIX:-public} + namespace: ${K8S_NAMESPACE} +spec: + hostnames: + - ${DOMAIN} +YAML +fi +EOF + chmod +x "$TEST_TEMP_DIR/gomplate" + export PATH="$TEST_TEMP_DIR:$PATH" + + # Mock process_routes script + if [[ ! -f "$SERVICE_PATH/scripts/istio/process_routes.bak" ]]; then + if [[ -f "$SERVICE_PATH/scripts/istio/process_routes" ]]; then + cp "$SERVICE_PATH/scripts/istio/process_routes" "$SERVICE_PATH/scripts/istio/process_routes.bak" + fi + fi + cat > "$SERVICE_PATH/scripts/istio/process_routes" << 'MOCKEOF' +#!/bin/bash +# Mock - does nothing +# Use return instead of exit so it doesn't exit the sourcing shell +return 0 2>/dev/null || true +MOCKEOF + chmod +x "$SERVICE_PATH/scripts/istio/process_routes" +} + +teardown() { + # Always restore original process_routes if backup exists + if [[ -f "$SERVICE_PATH/scripts/istio/process_routes.bak" ]]; then + mv -f "$SERVICE_PATH/scripts/istio/process_routes.bak" "$SERVICE_PATH/scripts/istio/process_routes" + fi + + # Clean up temp directory + if [[ -n "$TEST_TEMP_DIR" ]] && [[ -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +@test "build_httproute: generates public HTTPRoute with routes" { + export CONTEXT=$(load_fixture "simple-public-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="public" + + run bash "$SERVICE_PATH/scripts/istio/build_httproute" + + assert_success + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" +} + +@test "build_httproute: generates private HTTPRoute with routes" { + export CONTEXT=$(load_fixture "public-and-private-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="private" + + run bash "$SERVICE_PATH/scripts/istio/build_httproute" + + assert_success + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" +} + +@test "build_httproute: creates marker file when no public routes" { + export CONTEXT=$(load_fixture "no-public-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="public" + + run bash "$SERVICE_PATH/scripts/istio/build_httproute" + + assert_success + assert_file_not_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_exists "$OUTPUT_DIR/.httproute-public-deleted" +} + +@test "build_httproute: creates marker file when no public domain" { + export CONTEXT='{ + "service": {"id": "test-id", "slug": "test"}, + "parameters": {"publicDomain": "", "privateDomain": "private.test.com"}, + "routes": [{"path": "/test", "method": "GET", "scope": "test", "visibility": "public"}] + }' + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="public" + + run bash "$SERVICE_PATH/scripts/istio/build_httproute" + + assert_success + assert_file_not_exists "$OUTPUT_DIR/httproute-test-id-public.yaml" + assert_file_exists "$OUTPUT_DIR/.httproute-public-deleted" +} + +@test "build_httproute: creates marker file when no private routes" { + export CONTEXT=$(load_fixture "simple-public-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="private" + + run bash "$SERVICE_PATH/scripts/istio/build_httproute" + + assert_success + assert_file_not_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" + assert_file_exists "$OUTPUT_DIR/.httproute-private-deleted" +} + +@test "build_httproute: fails with invalid visibility" { + export CONTEXT=$(load_fixture "simple-public-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="invalid" + + run bash "$SERVICE_PATH/scripts/istio/build_httproute" + + assert_failure +} + +@test "build_httproute: exports HTTPROUTE_PUBLIC_FILE for public" { + export CONTEXT=$(load_fixture "simple-public-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="public" + + source "$SERVICE_PATH/scripts/istio/build_httproute" + + [[ -n "$HTTPROUTE_PUBLIC_FILE" ]] + [[ "$HTTPROUTE_PUBLIC_FILE" == *"public.yaml" ]] +} + +@test "build_httproute: exports HTTPROUTE_PRIVATE_FILE for private" { + export CONTEXT=$(load_fixture "public-and-private-routes") + source "$SERVICE_PATH/scripts/istio/build_context" + + export VISIBILITY="private" + + source "$SERVICE_PATH/scripts/istio/build_httproute" + + [[ -n "$HTTPROUTE_PRIVATE_FILE" ]] + [[ "$HTTPROUTE_PRIVATE_FILE" == *"private.yaml" ]] +} diff --git a/endpoint-exposer/test/test_integration.bats b/endpoint-exposer/test/test_integration.bats new file mode 100644 index 0000000..879348d --- /dev/null +++ b/endpoint-exposer/test/test_integration.bats @@ -0,0 +1,262 @@ +#!/usr/bin/env bats + +load helpers + +setup() { + export TEST_TEMP_DIR="$(mktemp -d)" + export OUTPUT_DIR="$TEST_TEMP_DIR/output" + mkdir -p "$OUTPUT_DIR" + export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" + + # Load assert functions + load_bats_support_libraries + + export K8S_NAMESPACE="test-namespace" + export ALB_NAME="test-alb" + export ACTION="apply" + export DRY_RUN="true" + + # Mock kubectl + mock_kubectl + + # Mock gomplate + cat > "$TEST_TEMP_DIR/gomplate" << 'EOF' +#!/bin/bash +TEMPLATE_FILE="" +OUTPUT_FILE="" +CONTEXT_FILE="" + +while [[ $# -gt 0 ]]; do + case $1 in + -f) TEMPLATE_FILE="$2"; shift 2 ;; + -o) OUTPUT_FILE="$2"; shift 2 ;; + -c) CONTEXT_FILE="${2#.=}"; shift 2 ;; + *) shift ;; + esac +done + +if [[ -n "$TEMPLATE_FILE" ]] && [[ -n "$OUTPUT_FILE" ]]; then + # Read context if provided + if [[ -n "$CONTEXT_FILE" ]] && [[ -f "$CONTEXT_FILE" ]]; then + CONTEXT_JSON=$(cat "$CONTEXT_FILE") + SERVICE_SLUG=$(echo "$CONTEXT_JSON" | jq -r '.service_slug // ""') + SERVICE_ID=$(echo "$CONTEXT_JSON" | jq -r '.service_id // ""') + SUFFIX=$(echo "$CONTEXT_JSON" | jq -r '.suffix // ""') + DOMAIN=$(echo "$CONTEXT_JSON" | jq -r '.domain // ""') + NAMESPACE=$(echo "$CONTEXT_JSON" | jq -r '.k8s_namespace // .gateway_namespace // ""') + fi + + # Determine resource type from template + if [[ "$TEMPLATE_FILE" == *"httproute"* ]]; then + cat > "$OUTPUT_FILE" << YAML +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: ${SERVICE_SLUG}-${SERVICE_ID}-${SUFFIX} + namespace: ${NAMESPACE} + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "${SERVICE_ID}" + app.kubernetes.io/name: ${SERVICE_SLUG} +spec: + hostnames: + - ${DOMAIN} +YAML + elif [[ "$TEMPLATE_FILE" == *"authorization"* ]]; then + cat > "$OUTPUT_FILE" << YAML +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: ${SERVICE_SLUG}-${SERVICE_ID}-authz-${SUFFIX} + namespace: ${NAMESPACE} + labels: + nullplatform.com/managed-by: endpoint-exposer + nullplatform.com/service-id: "${SERVICE_ID}" + app.kubernetes.io/name: ${SERVICE_SLUG} +spec: + action: CUSTOM +YAML + fi +fi +EOF + chmod +x "$TEST_TEMP_DIR/gomplate" + export PATH="$TEST_TEMP_DIR:$PATH" + + # Mock process_routes script (it's sourced by build_httproute) + mkdir -p "$SERVICE_PATH/scripts/istio" + if [[ ! -f "$SERVICE_PATH/scripts/istio/process_routes.bak" ]]; then + # Backup original if exists + if [[ -f "$SERVICE_PATH/scripts/istio/process_routes" ]]; then + cp "$SERVICE_PATH/scripts/istio/process_routes" "$SERVICE_PATH/scripts/istio/process_routes.bak" + fi + fi + + # Create a minimal mock that does nothing (for testing we just need the HTTPRoute YAML) + cat > "$SERVICE_PATH/scripts/istio/process_routes" << 'MOCKEOF' +#!/bin/bash +# Mock process_routes for testing - does nothing +# In real tests, the gomplate mock already creates the YAML we need +# Use return instead of exit so it doesn't exit the sourcing shell +return 0 2>/dev/null || true +MOCKEOF + chmod +x "$SERVICE_PATH/scripts/istio/process_routes" +} + +teardown() { + # Always restore original process_routes if backup exists + if [[ -f "$SERVICE_PATH/scripts/istio/process_routes.bak" ]]; then + mv -f "$SERVICE_PATH/scripts/istio/process_routes.bak" "$SERVICE_PATH/scripts/istio/process_routes" + fi + + # Clean up temp directory + if [[ -n "$TEST_TEMP_DIR" ]] && [[ -d "$TEST_TEMP_DIR" ]]; then + rm -rf "$TEST_TEMP_DIR" + fi +} + +@test "integration: complete workflow with public routes only" { + export CONTEXT=$(load_fixture "simple-public-routes") + + # Step 1: Build context + source "$SERVICE_PATH/scripts/istio/build_context" + + # Step 2: Build public httproute + export VISIBILITY="public" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Step 3: Build private httproute (should create marker) + export VISIBILITY="private" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Step 4: Generate authorization policies (should create markers when disabled) + bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + # Verify outputs + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_not_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" + assert_file_exists "$OUTPUT_DIR/.httproute-private-deleted" + assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" + assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" + + # Verify public HTTPRoute content + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "HTTPRoute" + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "api.edenred.nullimplementation.com" +} + +@test "integration: complete workflow with public and private routes" { + export CONTEXT=$(load_fixture "public-and-private-routes") + + # Build context + source "$SERVICE_PATH/scripts/istio/build_context" + + # Build httproutes + export VISIBILITY="public" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + export VISIBILITY="private" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Generate authorization policies + bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + # Verify all resources created + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" + assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" + + # Verify no marker files (all resources should be created) + assert_file_not_exists "$OUTPUT_DIR/.httproute-public-deleted" + assert_file_not_exists "$OUTPUT_DIR/.httproute-private-deleted" + assert_file_not_exists "$OUTPUT_DIR/.authz-public-deleted" + assert_file_not_exists "$OUTPUT_DIR/.authz-private-deleted" + + # Verify content + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "api.edenred.nullimplementation.com" + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "api-private.edenred.nullimplementation.com" + assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "AuthorizationPolicy" + assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "AuthorizationPolicy" +} + +@test "integration: workflow with authorization disabled creates cleanup markers" { + export CONTEXT=$(load_fixture "authorization-disabled") + + # Build context + source "$SERVICE_PATH/scripts/istio/build_context" + + # Build httproutes + export VISIBILITY="public" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + export VISIBILITY="private" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Generate authorization policies (should create markers) + bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + # Verify httproutes created + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" + + # Verify authz markers created (authorization disabled) + assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" + assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" + assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" + assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" +} + +@test "integration: apply step handles markers and resources correctly" { + export CONTEXT=$(load_fixture "simple-public-routes") + + # Build context + source "$SERVICE_PATH/scripts/istio/build_context" + + # Build httproutes + export VISIBILITY="public" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + export VISIBILITY="private" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Generate authorization policies + bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + # Run apply + run bash "$SERVICE_PATH/scripts/common/apply" + + assert_success + + # Should detect and process markers + assert_output --partial "Private HTTPRoute marked for deletion" + assert_output --partial "Public AuthorizationPolicy marked for deletion" + assert_output --partial "Private AuthorizationPolicy marked for deletion" + + # Should apply the public httproute + assert_output --partial "Applying 1 resources" +} + +@test "integration: all resources have correct labels for management" { + export CONTEXT=$(load_fixture "public-and-private-routes") + + # Build context + source "$SERVICE_PATH/scripts/istio/build_context" + + # Build httproutes + export VISIBILITY="public" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + export VISIBILITY="private" + bash "$SERVICE_PATH/scripts/istio/build_httproute" + + # Generate authorization policies + bash "$SERVICE_PATH/plugins/authorization-policy/generate" + + # Verify all resources have required labels + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "nullplatform.com/managed-by: endpoint-exposer" + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "nullplatform.com/managed-by: endpoint-exposer" + assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "nullplatform.com/managed-by: endpoint-exposer" + assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "nullplatform.com/managed-by: endpoint-exposer" + + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "nullplatform.com/service-id:" + assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "nullplatform.com/service-id:" +} diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 3084876..8d879b3 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -5,8 +5,6 @@ steps: output: - name: K8S_NAMESPACE type: environment - - name: ALB_NAME - type: environment - name: "build context" type: script file: "$SERVICE_PATH/scripts/istio/build_context" diff --git a/endpoint-exposer/workflows/istio/read.yaml b/endpoint-exposer/workflows/istio/read.yaml index d9ee607..8de54ea 100644 --- a/endpoint-exposer/workflows/istio/read.yaml +++ b/endpoint-exposer/workflows/istio/read.yaml @@ -5,8 +5,6 @@ steps: output: - name: K8S_NAMESPACE type: environment - - name: ALB_NAME - type: environment - name: "build context" type: script file: "$SERVICE_PATH/scripts/istio/build_context" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 0a3eba2..42f559d 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -5,8 +5,6 @@ steps: output: - name: K8S_NAMESPACE type: environment - - name: ALB_NAME - type: environment - name: "build context" type: script file: "$SERVICE_PATH/scripts/istio/build_context" From 81398558235a857ae6213cbcc654bd59733eacb9 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 13 Jan 2026 11:01:25 -0300 Subject: [PATCH 033/101] feat: add pre-commit hook to run tests Add git pre-commit hook that automatically runs BATS tests before each commit when endpoint-exposer files are modified. This ensures code quality and prevents breaking changes from being committed. - Create .githooks/pre-commit hook at repo root to run tests - Add scripts/setup-hooks.sh to configure git hooks path - Update test/README.md with hook setup instructions - Hook only runs tests when endpoint-exposer files are changed Co-Authored-By: Claude Sonnet 4.5 --- .githooks/pre-commit | 25 +++++++++++++++++++++++++ endpoint-exposer/scripts/setup-hooks.sh | 11 +++++++++++ endpoint-exposer/test/README.md | 15 +++++++++++++++ 3 files changed, 51 insertions(+) create mode 100755 .githooks/pre-commit create mode 100755 endpoint-exposer/scripts/setup-hooks.sh diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..925520b --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +echo "Running tests before commit..." + +# Change to the repository root +REPO_ROOT="$(git rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +# Check if endpoint-exposer tests need to be run (if any endpoint-exposer files changed) +if git diff --cached --name-only | grep -q "^endpoint-exposer/"; then + echo "Endpoint-exposer files changed, running tests..." + + if command -v bats &> /dev/null; then + cd endpoint-exposer + bats test/ + else + echo "⚠️ BATS not installed, skipping tests" + echo "Install BATS: brew install bats-core (macOS) or see https://bats-core.readthedocs.io" + exit 0 + fi + + echo "✅ All endpoint-exposer tests passed!" +fi diff --git a/endpoint-exposer/scripts/setup-hooks.sh b/endpoint-exposer/scripts/setup-hooks.sh new file mode 100755 index 0000000..2b9c89c --- /dev/null +++ b/endpoint-exposer/scripts/setup-hooks.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Get the git repository root +REPO_ROOT="$(git rev-parse --show-toplevel)" + +# Configure git to use .githooks directory instead of .git/hooks +cd "$REPO_ROOT" +git config core.hooksPath .githooks + +echo "✅ Git hooks configured successfully!" +echo "Pre-commit hook will run endpoint-exposer tests before each commit when endpoint-exposer files are changed" diff --git a/endpoint-exposer/test/README.md b/endpoint-exposer/test/README.md index 6f0f906..7fc102b 100644 --- a/endpoint-exposer/test/README.md +++ b/endpoint-exposer/test/README.md @@ -28,6 +28,20 @@ Run a specific test file: bats test_istio_workflows.bats ``` +## Git Hooks + +The repository includes a pre-commit hook that automatically runs tests before each commit. + +Setup the git hooks: +```bash +./scripts/setup-hooks.sh +``` + +This configures git to use the `.githooks` directory. The pre-commit hook will: +- Run all BATS tests before allowing a commit +- Skip tests if BATS is not installed (with a warning) +- Prevent commits if tests fail + ## Test Structure - `fixtures/` - Test data and context files @@ -86,3 +100,4 @@ The test fixtures use the full nullplatform action context structure: assert_file_contains "$OUTPUT_DIR/httproute-service-id-public.yaml" "expected content" } ``` +# Test From 27d26eb32f7954a8b81b72f664b312f94ed3be19 Mon Sep 17 00:00:00 2001 From: Javi Date: Thu, 15 Jan 2026 17:27:30 -0300 Subject: [PATCH 034/101] feat: wip plugins --- .../examples/jwt-working-solution.yaml | 60 +++ endpoint-exposer/plugins/README.md | 438 ++++++++++++++++++ endpoint-exposer/plugins/entrypoint | 215 +++++++++ endpoint-exposer/workflows/istio/create.yaml | 11 +- 4 files changed, 715 insertions(+), 9 deletions(-) create mode 100644 endpoint-exposer/examples/jwt-working-solution.yaml create mode 100644 endpoint-exposer/plugins/README.md create mode 100755 endpoint-exposer/plugins/entrypoint diff --git a/endpoint-exposer/examples/jwt-working-solution.yaml b/endpoint-exposer/examples/jwt-working-solution.yaml new file mode 100644 index 0000000..499269c --- /dev/null +++ b/endpoint-exposer/examples/jwt-working-solution.yaml @@ -0,0 +1,60 @@ +--- +# RequestAuthentication: Validates JWT tokens from nullplatform scope +# This validates the JWT signature and extracts claims, but doesn't enforce authentication +apiVersion: security.istio.io/v1 +kind: RequestAuthentication +metadata: + name: nullplatform-scope-jwt-auth + namespace: gateways + labels: + nullplatform.com/managed-by: endpoint-exposer +spec: + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-public + jwtRules: + - issuer: "https://api.nullplatform.com/scope" + jwksUri: "https://api.nullplatform.com/scope/.well-known/jwks.json" + fromHeaders: + - name: Authorization + prefix: "Bearer " + fromCookies: + - "np_scope_token" + outputClaimToHeaders: + - header: "X-User-ID" + claim: "sub" + - header: "X-User-Email" + claim: "email" + - header: "X-User-Scopes" + claim: "scope" + +--- +# AuthorizationPolicy: Denies access to specific host without valid JWT +# Uses DENY action with notValues to block requests without valid issuer claim +# Other hosts without specific policies remain accessible (Istio default: allow-by-default with only DENY policies) +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: require-jwt-production + namespace: gateways + labels: + nullplatform.com/managed-by: endpoint-exposer +spec: + action: DENY + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-public + rules: + # Deny requests to production host without valid JWT + - to: + - operation: + hosts: + - "playground-floppy-bird-api-production-kjstb.edenred.nullimplementation.com" + ports: ["443"] + notPaths: + - /health + - /ready + - /metrics + when: + - key: request.auth.claims[aud] + notValues: ["layground-floppy-bird-api-production-kjstb.edenred.nullimplementation.com"] diff --git a/endpoint-exposer/plugins/README.md b/endpoint-exposer/plugins/README.md new file mode 100644 index 0000000..a3ea00a --- /dev/null +++ b/endpoint-exposer/plugins/README.md @@ -0,0 +1,438 @@ +# Endpoint-Exposer Plugin System + +Sistema generalizado de plugins para el servicio endpoint-exposer que permite configurar y ejecutar plugins dinámicamente desde la service specification. + +## Descripción + +El sistema de plugins permite extender la funcionalidad de endpoint-exposer sin modificar el código del servicio. Los plugins se configuran en la service specification del platform y se ejecutan automáticamente durante los workflows. + +## Arquitectura + +### Componentes + +1. **Plugin Entrypoint** (`plugins/entrypoint`): Script bash que: + - Lee la service specification desde el platform + - Itera sobre los plugins configurados + - Ejecuta cada plugin con sus variables de entorno + - Maneja errores gracefully sin romper el workflow + +2. **Service Specification**: Configuración en el platform que define: + - Lista de plugins a ejecutar + - Configuración por plugin (path, ejecutable, env vars) + - Estado de habilitación de cada plugin + +3. **Plugins Externos**: Scripts ejecutables instalados en el sistema que: + - Implementan funcionalidad específica + - Reciben contexto via variables de entorno + - Retornan exit codes para indicar éxito/fallo + +## Configuración de Plugins + +### Service Specification Format + +Los plugins se configuran en la service specification del platform bajo `attributes.schema.plugins`: + +```json +{ + "id": "61e26a5e-ba14-4e76-856d-25929c90f0f0", + "slug": "endpoint-exposer", + "name": "Endpoint Exposer", + "type": "dependency", + "attributes": { + "schema": { + "plugins": [ + { + "name": "authorization-avp", + "exec": "/entrypoint", + "plugin_path": "/root/.np/nullplatform/services", + "enabled": true, + "environment": { + "JWT_ISSUER": "https://custom.issuer.com", + "JWT_ENABLED": "true", + "LOG_LEVEL": "debug" + } + }, + { + "name": "custom-monitoring", + "exec": "/run.sh", + "plugin_path": "/opt/plugins/monitoring", + "enabled": false, + "environment": { + "METRICS_ENDPOINT": "https://metrics.example.com", + "INTERVAL": "60" + } + } + ], + "properties": { + ... + } + } + } +} +``` + +### Campos de Configuración del Plugin + +| Campo | Tipo | Requerido | Descripción | +|-------|------|-----------|-------------| +| `name` | string | Sí | Identificador único del plugin. Se usa para construir el path completo. | +| `exec` | string | Sí | Script ejecutable dentro del directorio del plugin (ej: `/entrypoint`, `/run.sh`). | +| `plugin_path` | string | Sí | Directorio base donde está instalado el plugin. | +| `enabled` | boolean | No (default: true) | Flag para habilitar/deshabilitar el plugin. | +| `environment` | object | No | Variables de entorno específicas del plugin (key-value pairs). | + +### Path Resolution + +El path completo del ejecutable se construye como: + +```bash +PLUGIN_EXECUTABLE="$plugin_path/$name$exec" +``` + +**Ejemplo:** +- `plugin_path`: `/root/.np/nullplatform/services` +- `name`: `authorization-avp` +- `exec`: `/entrypoint` +- **Resultado**: `/root/.np/nullplatform/services/authorization-avp/entrypoint` + +## Integración en Workflows + +El plugin entrypoint se ejecuta como un step en los workflows: + +```yaml +steps: + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_context" + + - name: "build httproutes" + type: script + file: "$SERVICE_PATH/scripts/istio/build_httproute" + + - name: "execute plugins" + type: script + file: "$SERVICE_PATH/plugins/entrypoint" # Plugin execution + + - name: apply + type: script + file: "$SERVICE_PATH/scripts/common/apply" +``` + +**Nota:** El step de plugins debe ejecutarse **antes** del `apply` para que los recursos generados por plugins se apliquen correctamente. + +## Flujo de Ejecución + +1. **Context Validation**: Verifica que existe la variable `CONTEXT` + +2. **Extract Service Spec ID**: Obtiene el ID desde: + - `.service.specification.id` + - `.service_specification_id` + - `.specification.id` + +3. **Fetch Service Specification**: Llama a `np service specification read --id $ID --format json` + +4. **Extract Plugins Array**: Lee `.attributes.schema.plugins[]` + +5. **Iterate Plugins**: Para cada plugin: + - Verifica si está habilitado (`enabled: true`) + - Valida que existan `plugin_path` y `exec` + - Construye path completo del ejecutable + - Verifica que el archivo existe y es ejecutable + - Exporta variables de entorno del plugin + - Ejecuta el plugin + - Captura exit code y logs + +6. **Summary Report**: Muestra resumen de plugins ejecutados/saltados/fallidos + +## Comportamiento y Casos de Uso + +### Caso 1: Plugin Habilitado y Exitoso + +**Input:** +```json +{ + "name": "auth-plugin", + "plugin_path": "/opt/plugins", + "exec": "/entrypoint", + "enabled": true, + "environment": { + "JWT_ISSUER": "https://issuer.com" + } +} +``` + +**Output:** +``` +======================================== +Plugin 1/1: auth-plugin +======================================== +Status: ENABLED +Executable: /opt/plugins/auth-plugin/entrypoint +Environment variables: + export JWT_ISSUER='https://issuer.com' + +Executing plugin... +---------------------------------------- +[plugin output here] +---------------------------------------- +Plugin 'auth-plugin' completed successfully (exit code: 0) +``` + +### Caso 2: Plugin Deshabilitado + +**Input:** +```json +{ + "name": "monitoring-plugin", + "enabled": false, + ... +} +``` + +**Output:** +``` +======================================== +Plugin 1/1: monitoring-plugin +======================================== +Status: DISABLED +Skipping plugin 'monitoring-plugin' +``` + +### Caso 3: Plugin No Encontrado + +**Input:** +```json +{ + "name": "missing-plugin", + "plugin_path": "/opt/does-not-exist", + "exec": "/run.sh", + "enabled": true +} +``` + +**Output:** +``` +======================================== +Plugin 1/1: missing-plugin +======================================== +Status: ENABLED +Executable: /opt/does-not-exist/missing-plugin/run.sh +WARNING: Plugin executable not found at path: /opt/does-not-exist/missing-plugin/run.sh +Skipping plugin 'missing-plugin' +``` + +**Nota:** El workflow continúa sin fallar. + +### Caso 4: Plugin Falla con Error + +**Input:** Plugin retorna exit code != 0 + +**Output:** +``` +---------------------------------------- +ERROR: Plugin 'failing-plugin' failed with exit code: 1 +WARNING: Continuing with remaining plugins despite failure +``` + +**Nota:** Por diseño, los plugins fallidos no rompen el workflow completo. Esto puede configurarse en el futuro con un flag `fail_on_error`. + +### Caso 5: Sin Plugins Configurados + +**Input:** Service specification sin `attributes.schema.plugins` + +**Output:** +``` +No plugins configured in service specification +Path checked: .attributes.schema.plugins + +Plugin execution completed (no plugins configured) +``` + +**Exit Code:** 0 (éxito) + +## Variables de Entorno Disponibles + +Los plugins tienen acceso a todas las variables de entorno del workflow, incluyendo: + +### Variables del Context +- `CONTEXT`: JSON completo del contexto de ejecución +- `SERVICE_ID`, `SERVICE_SLUG`: Información del servicio +- `SCOPE_ID`, `SCOPE_DOMAIN`: Información del scope +- `K8S_NAMESPACE`: Namespace de Kubernetes +- `GATEWAY_NAME`: Nombre del gateway de Istio + +### Variables del Workflow +- `ACTION`: Acción del workflow (create/update/delete/apply) +- `OUTPUT_DIR`: Directorio para archivos generados +- `SERVICE_PATH`: Path del servicio + +### Variables del Plugin +- Variables definidas en `environment` del plugin en la service specification + +## Desarrollo de Plugins + +### Estructura Básica de un Plugin + +```bash +#!/bin/bash +set -euo pipefail + +# 1. Validar variables de entorno requeridas +if [ -z "${REQUIRED_VAR:-}" ]; then + echo "ERROR: REQUIRED_VAR is not set" + exit 1 +fi + +# 2. Leer configuración del CONTEXT +SCOPE_ID=$(echo "$CONTEXT" | jq -r '.scope.id') + +# 3. Ejecutar lógica del plugin +echo "Executing plugin logic for scope: $SCOPE_ID" + +# 4. Generar outputs (opcional) +OUTPUT_FILE="$OUTPUT_DIR/plugin-output-$SCOPE_ID.yaml" +cat > "$OUTPUT_FILE" < "$PLUGIN_CONTEXT_PATH" + +# Renderizar template +PLUGIN_OUTPUT="$OUTPUT_DIR/plugin-resource-$SCOPE_ID.yaml" +gomplate -c .="$PLUGIN_CONTEXT_PATH" \ + --file "$PLUGIN_PATH/templates/resource.yaml.tmpl" \ + --out "$PLUGIN_OUTPUT" + +echo "Generated resource: $PLUGIN_OUTPUT" +``` + +## Testing + +### Test Local + +```bash +# Setup +export CONTEXT='{"service":{"specification":{"id":"test-spec-id"}}}' +export OUTPUT_DIR="/tmp/test-output" +mkdir -p "$OUTPUT_DIR" + +# Mock np CLI para testing +np() { + cat <&1); then + echo "ERROR: Failed to read service specification" + echo "Command: np service specification read --id $SERVICE_SPEC_ID --format json" + echo "Output: $SERVICE_SPEC" + exit 1 +fi + +echo "Service specification retrieved successfully" +echo "" + +# 4. Extract plugins array from service specification +PLUGINS=$(echo "$SERVICE_SPEC" | jq -c '.attributes.schema.plugins // []') + +if [ "$PLUGINS" = "[]" ] || [ "$PLUGINS" = "null" ]; then + echo "No plugins configured in service specification" + echo "Path checked: .attributes.schema.plugins" + echo "" + echo "Plugin execution completed (no plugins configured)" + exit 0 +fi + +# Count plugins +PLUGIN_COUNT=$(echo "$PLUGINS" | jq 'length') +echo "Found $PLUGIN_COUNT plugin(s) in service specification" +echo "" + +# 5. Iterate over each plugin and execute it +# Use process substitution to avoid subshell and maintain counters +PLUGIN_INDEX=0 +PLUGINS_EXECUTED=0 +PLUGINS_SKIPPED=0 +PLUGINS_FAILED=0 + +while IFS= read -r plugin; do + PLUGIN_INDEX=$((PLUGIN_INDEX + 1)) + + # Extract plugin configuration + PLUGIN_NAME=$(echo "$plugin" | jq -r '.name // "unnamed-plugin"') + # Use 'if has("enabled") then .enabled else true end' to handle false correctly + PLUGIN_ENABLED=$(echo "$plugin" | jq -r 'if has("enabled") then .enabled else true end') + + echo "========================================" + echo "Plugin $PLUGIN_INDEX/$PLUGIN_COUNT: $PLUGIN_NAME" + echo "========================================" + + # Check if plugin is enabled + if [ "$PLUGIN_ENABLED" != "true" ]; then + echo "Status: DISABLED" + echo "Skipping plugin '$PLUGIN_NAME'" + echo "" + PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) + continue + fi + + echo "Status: ENABLED" + + # Extract paths and executable + PLUGIN_PATH=$(echo "$plugin" | jq -r '.plugin_path // ""') + PLUGIN_EXEC=$(echo "$plugin" | jq -r '.exec // ""') + + if [ -z "$PLUGIN_PATH" ] || [ "$PLUGIN_PATH" = "null" ]; then + echo "ERROR: Plugin '$PLUGIN_NAME' has no plugin_path configured" + echo "Skipping plugin" + echo "" + PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) + continue + fi + + if [ -z "$PLUGIN_EXEC" ] || [ "$PLUGIN_EXEC" = "null" ]; then + echo "ERROR: Plugin '$PLUGIN_NAME' has no exec configured" + echo "Skipping plugin" + echo "" + PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) + continue + fi + + # Build full executable path + PLUGIN_EXECUTABLE="$PLUGIN_PATH/$PLUGIN_NAME$PLUGIN_EXEC" + + echo "Executable: $PLUGIN_EXECUTABLE" + + # Check that executable exists + if [ ! -f "$PLUGIN_EXECUTABLE" ]; then + echo "WARNING: Plugin executable not found at path: $PLUGIN_EXECUTABLE" + echo "Skipping plugin '$PLUGIN_NAME'" + echo "" + PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) + continue + fi + + # Check that executable has execution permissions + if [ ! -x "$PLUGIN_EXECUTABLE" ]; then + echo "WARNING: Plugin executable is not executable: $PLUGIN_EXECUTABLE" + echo "Setting execute permissions..." + chmod +x "$PLUGIN_EXECUTABLE" + fi + + # 6. Export plugin environment variables + PLUGIN_ENV=$(echo "$plugin" | jq -r '.environment // {}') + + if [ "$PLUGIN_ENV" != "{}" ] && [ "$PLUGIN_ENV" != "null" ]; then + echo "Environment variables:" + + # Create subshell to export variables temporarily + ( + # Export each environment variable + while IFS= read -r export_line; do + if [ -n "$export_line" ]; then + echo " $export_line" + eval "$export_line" + fi + done < <(echo "$PLUGIN_ENV" | jq -r 'to_entries[] | "export \(.key)=\(.value | @sh)"') + + # 7. Execute plugin + echo "" + echo "Executing plugin..." + echo "----------------------------------------" + + if bash "$PLUGIN_EXECUTABLE"; then + EXIT_CODE=$? + echo "----------------------------------------" + echo "Plugin '$PLUGIN_NAME' completed successfully (exit code: $EXIT_CODE)" + else + EXIT_CODE=$? + echo "----------------------------------------" + echo "ERROR: Plugin '$PLUGIN_NAME' failed with exit code: $EXIT_CODE" + echo "WARNING: Continuing with remaining plugins despite failure" + fi + ) + + # Capture subshell exit code + SUBSHELL_EXIT=$? + if [ $SUBSHELL_EXIT -ne 0 ]; then + PLUGINS_FAILED=$((PLUGINS_FAILED + 1)) + else + PLUGINS_EXECUTED=$((PLUGINS_EXECUTED + 1)) + fi + else + echo "Environment variables: (none)" + echo "" + echo "Executing plugin..." + echo "----------------------------------------" + + if bash "$PLUGIN_EXECUTABLE"; then + EXIT_CODE=$? + echo "----------------------------------------" + echo "Plugin '$PLUGIN_NAME' completed successfully (exit code: $EXIT_CODE)" + PLUGINS_EXECUTED=$((PLUGINS_EXECUTED + 1)) + else + EXIT_CODE=$? + echo "----------------------------------------" + echo "ERROR: Plugin '$PLUGIN_NAME' failed with exit code: $EXIT_CODE" + echo "WARNING: Continuing with remaining plugins despite failure" + PLUGINS_FAILED=$((PLUGINS_FAILED + 1)) + fi + fi + + echo "" +done < <(echo "$PLUGINS" | jq -c '.[]') + +# 8. Final summary +echo "========================================" +echo "Plugin Execution Summary" +echo "========================================" +echo "Total plugins: $PLUGIN_COUNT" +echo "Executed successfully: $PLUGINS_EXECUTED" +echo "Skipped: $PLUGINS_SKIPPED" +echo "Failed: $PLUGINS_FAILED" +echo "" +echo "Plugin execution completed" + +exit 0 diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 8d879b3..39d09b0 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -45,16 +45,9 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "generate authorization policy" + - name: "execute plugins" type: script - file: "$SERVICE_PATH/plugins/authorization-policy/generate" - output: - - name: AUTHZ_POLICY_PUBLIC_FILE - type: file - file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" - - name: AUTHZ_POLICY_PRIVATE_FILE - type: file - file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" + file: "$SERVICE_PATH/plugins/entrypoint" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From c6b55b1f9eb4b6b2b15c0b93931dd9dfb4fdcd25 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 19 Jan 2026 15:47:45 -0300 Subject: [PATCH 035/101] feat: add echos and remove auth policy from workflows --- endpoint-exposer/entrypoint/service | 3 +- endpoint-exposer/plugins/README.md | 438 ------------------ .../plugins/authorization-policy/README.md | 241 ---------- .../plugins/authorization-policy/generate | 181 -------- .../authorization-policy/template.yaml.tpl | 38 -- endpoint-exposer/plugins/entrypoint | 215 --------- .../specs/notification-channel.json.tpl | 34 ++ endpoint-exposer/test/CONTRIBUTING.md | 9 +- .../test/test_authorization_policy.bats | 127 ----- endpoint-exposer/test/test_integration.bats | 33 -- endpoint-exposer/workflows/istio/create.yaml | 3 - endpoint-exposer/workflows/istio/update.yaml | 10 - 12 files changed, 39 insertions(+), 1293 deletions(-) delete mode 100644 endpoint-exposer/plugins/README.md delete mode 100644 endpoint-exposer/plugins/authorization-policy/README.md delete mode 100755 endpoint-exposer/plugins/authorization-policy/generate delete mode 100644 endpoint-exposer/plugins/authorization-policy/template.yaml.tpl delete mode 100755 endpoint-exposer/plugins/entrypoint create mode 100644 endpoint-exposer/specs/notification-channel.json.tpl delete mode 100644 endpoint-exposer/test/test_authorization_policy.bats diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service index ad77e32..aac8270 100755 --- a/endpoint-exposer/entrypoint/service +++ b/endpoint-exposer/entrypoint/service @@ -13,9 +13,10 @@ esac INGRESS_TYPE="${INGRESS_TYPE:-alb}" echo "INGRESS_TYPE is set to '$INGRESS_TYPE'" +echo "OVERRIDES_PATH is set to '$OVERRIDES_PATH'" WORKFLOW_PATH="$SERVICE_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" -OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" +OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$ACTION_TO_EXECUTE.yaml" VALUES_PATH="$SERVICE_PATH/values.yaml" CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH" diff --git a/endpoint-exposer/plugins/README.md b/endpoint-exposer/plugins/README.md deleted file mode 100644 index a3ea00a..0000000 --- a/endpoint-exposer/plugins/README.md +++ /dev/null @@ -1,438 +0,0 @@ -# Endpoint-Exposer Plugin System - -Sistema generalizado de plugins para el servicio endpoint-exposer que permite configurar y ejecutar plugins dinámicamente desde la service specification. - -## Descripción - -El sistema de plugins permite extender la funcionalidad de endpoint-exposer sin modificar el código del servicio. Los plugins se configuran en la service specification del platform y se ejecutan automáticamente durante los workflows. - -## Arquitectura - -### Componentes - -1. **Plugin Entrypoint** (`plugins/entrypoint`): Script bash que: - - Lee la service specification desde el platform - - Itera sobre los plugins configurados - - Ejecuta cada plugin con sus variables de entorno - - Maneja errores gracefully sin romper el workflow - -2. **Service Specification**: Configuración en el platform que define: - - Lista de plugins a ejecutar - - Configuración por plugin (path, ejecutable, env vars) - - Estado de habilitación de cada plugin - -3. **Plugins Externos**: Scripts ejecutables instalados en el sistema que: - - Implementan funcionalidad específica - - Reciben contexto via variables de entorno - - Retornan exit codes para indicar éxito/fallo - -## Configuración de Plugins - -### Service Specification Format - -Los plugins se configuran en la service specification del platform bajo `attributes.schema.plugins`: - -```json -{ - "id": "61e26a5e-ba14-4e76-856d-25929c90f0f0", - "slug": "endpoint-exposer", - "name": "Endpoint Exposer", - "type": "dependency", - "attributes": { - "schema": { - "plugins": [ - { - "name": "authorization-avp", - "exec": "/entrypoint", - "plugin_path": "/root/.np/nullplatform/services", - "enabled": true, - "environment": { - "JWT_ISSUER": "https://custom.issuer.com", - "JWT_ENABLED": "true", - "LOG_LEVEL": "debug" - } - }, - { - "name": "custom-monitoring", - "exec": "/run.sh", - "plugin_path": "/opt/plugins/monitoring", - "enabled": false, - "environment": { - "METRICS_ENDPOINT": "https://metrics.example.com", - "INTERVAL": "60" - } - } - ], - "properties": { - ... - } - } - } -} -``` - -### Campos de Configuración del Plugin - -| Campo | Tipo | Requerido | Descripción | -|-------|------|-----------|-------------| -| `name` | string | Sí | Identificador único del plugin. Se usa para construir el path completo. | -| `exec` | string | Sí | Script ejecutable dentro del directorio del plugin (ej: `/entrypoint`, `/run.sh`). | -| `plugin_path` | string | Sí | Directorio base donde está instalado el plugin. | -| `enabled` | boolean | No (default: true) | Flag para habilitar/deshabilitar el plugin. | -| `environment` | object | No | Variables de entorno específicas del plugin (key-value pairs). | - -### Path Resolution - -El path completo del ejecutable se construye como: - -```bash -PLUGIN_EXECUTABLE="$plugin_path/$name$exec" -``` - -**Ejemplo:** -- `plugin_path`: `/root/.np/nullplatform/services` -- `name`: `authorization-avp` -- `exec`: `/entrypoint` -- **Resultado**: `/root/.np/nullplatform/services/authorization-avp/entrypoint` - -## Integración en Workflows - -El plugin entrypoint se ejecuta como un step en los workflows: - -```yaml -steps: - - name: "build context" - type: script - file: "$SERVICE_PATH/scripts/istio/build_context" - - - name: "build httproutes" - type: script - file: "$SERVICE_PATH/scripts/istio/build_httproute" - - - name: "execute plugins" - type: script - file: "$SERVICE_PATH/plugins/entrypoint" # Plugin execution - - - name: apply - type: script - file: "$SERVICE_PATH/scripts/common/apply" -``` - -**Nota:** El step de plugins debe ejecutarse **antes** del `apply` para que los recursos generados por plugins se apliquen correctamente. - -## Flujo de Ejecución - -1. **Context Validation**: Verifica que existe la variable `CONTEXT` - -2. **Extract Service Spec ID**: Obtiene el ID desde: - - `.service.specification.id` - - `.service_specification_id` - - `.specification.id` - -3. **Fetch Service Specification**: Llama a `np service specification read --id $ID --format json` - -4. **Extract Plugins Array**: Lee `.attributes.schema.plugins[]` - -5. **Iterate Plugins**: Para cada plugin: - - Verifica si está habilitado (`enabled: true`) - - Valida que existan `plugin_path` y `exec` - - Construye path completo del ejecutable - - Verifica que el archivo existe y es ejecutable - - Exporta variables de entorno del plugin - - Ejecuta el plugin - - Captura exit code y logs - -6. **Summary Report**: Muestra resumen de plugins ejecutados/saltados/fallidos - -## Comportamiento y Casos de Uso - -### Caso 1: Plugin Habilitado y Exitoso - -**Input:** -```json -{ - "name": "auth-plugin", - "plugin_path": "/opt/plugins", - "exec": "/entrypoint", - "enabled": true, - "environment": { - "JWT_ISSUER": "https://issuer.com" - } -} -``` - -**Output:** -``` -======================================== -Plugin 1/1: auth-plugin -======================================== -Status: ENABLED -Executable: /opt/plugins/auth-plugin/entrypoint -Environment variables: - export JWT_ISSUER='https://issuer.com' - -Executing plugin... ----------------------------------------- -[plugin output here] ----------------------------------------- -Plugin 'auth-plugin' completed successfully (exit code: 0) -``` - -### Caso 2: Plugin Deshabilitado - -**Input:** -```json -{ - "name": "monitoring-plugin", - "enabled": false, - ... -} -``` - -**Output:** -``` -======================================== -Plugin 1/1: monitoring-plugin -======================================== -Status: DISABLED -Skipping plugin 'monitoring-plugin' -``` - -### Caso 3: Plugin No Encontrado - -**Input:** -```json -{ - "name": "missing-plugin", - "plugin_path": "/opt/does-not-exist", - "exec": "/run.sh", - "enabled": true -} -``` - -**Output:** -``` -======================================== -Plugin 1/1: missing-plugin -======================================== -Status: ENABLED -Executable: /opt/does-not-exist/missing-plugin/run.sh -WARNING: Plugin executable not found at path: /opt/does-not-exist/missing-plugin/run.sh -Skipping plugin 'missing-plugin' -``` - -**Nota:** El workflow continúa sin fallar. - -### Caso 4: Plugin Falla con Error - -**Input:** Plugin retorna exit code != 0 - -**Output:** -``` ----------------------------------------- -ERROR: Plugin 'failing-plugin' failed with exit code: 1 -WARNING: Continuing with remaining plugins despite failure -``` - -**Nota:** Por diseño, los plugins fallidos no rompen el workflow completo. Esto puede configurarse en el futuro con un flag `fail_on_error`. - -### Caso 5: Sin Plugins Configurados - -**Input:** Service specification sin `attributes.schema.plugins` - -**Output:** -``` -No plugins configured in service specification -Path checked: .attributes.schema.plugins - -Plugin execution completed (no plugins configured) -``` - -**Exit Code:** 0 (éxito) - -## Variables de Entorno Disponibles - -Los plugins tienen acceso a todas las variables de entorno del workflow, incluyendo: - -### Variables del Context -- `CONTEXT`: JSON completo del contexto de ejecución -- `SERVICE_ID`, `SERVICE_SLUG`: Información del servicio -- `SCOPE_ID`, `SCOPE_DOMAIN`: Información del scope -- `K8S_NAMESPACE`: Namespace de Kubernetes -- `GATEWAY_NAME`: Nombre del gateway de Istio - -### Variables del Workflow -- `ACTION`: Acción del workflow (create/update/delete/apply) -- `OUTPUT_DIR`: Directorio para archivos generados -- `SERVICE_PATH`: Path del servicio - -### Variables del Plugin -- Variables definidas en `environment` del plugin en la service specification - -## Desarrollo de Plugins - -### Estructura Básica de un Plugin - -```bash -#!/bin/bash -set -euo pipefail - -# 1. Validar variables de entorno requeridas -if [ -z "${REQUIRED_VAR:-}" ]; then - echo "ERROR: REQUIRED_VAR is not set" - exit 1 -fi - -# 2. Leer configuración del CONTEXT -SCOPE_ID=$(echo "$CONTEXT" | jq -r '.scope.id') - -# 3. Ejecutar lógica del plugin -echo "Executing plugin logic for scope: $SCOPE_ID" - -# 4. Generar outputs (opcional) -OUTPUT_FILE="$OUTPUT_DIR/plugin-output-$SCOPE_ID.yaml" -cat > "$OUTPUT_FILE" < "$PLUGIN_CONTEXT_PATH" - -# Renderizar template -PLUGIN_OUTPUT="$OUTPUT_DIR/plugin-resource-$SCOPE_ID.yaml" -gomplate -c .="$PLUGIN_CONTEXT_PATH" \ - --file "$PLUGIN_PATH/templates/resource.yaml.tmpl" \ - --out "$PLUGIN_OUTPUT" - -echo "Generated resource: $PLUGIN_OUTPUT" -``` - -## Testing - -### Test Local - -```bash -# Setup -export CONTEXT='{"service":{"specification":{"id":"test-spec-id"}}}' -export OUTPUT_DIR="/tmp/test-output" -mkdir -p "$OUTPUT_DIR" - -# Mock np CLI para testing -np() { - cat < RegularExpression - # Convert /api/users/:id to /api/users/[^/]+ - local converted_path="${path//:[^\/]*/[^/]+}" - # Extract prefix for fallback - local prefix="${path%%:*}" - echo "RegularExpression:$converted_path:$prefix" - elif [[ "$path" == *"*" ]]; then - # Contains wildcard -> PathPrefix - # Remove trailing /* or * - local clean_path="${path%/\*}" - clean_path="${clean_path%\*}" - echo "PathPrefix:$clean_path" - else - # No special characters -> Exact - echo "Exact:$path" - fi -} - -# Function to process routes and build rules -build_authz_rules() { - local routes_json="$1" - local rules="[]" - - local num_routes=$(echo "$routes_json" | jq 'length') - - for ((i=0; i "$context_path" - - # Generate AuthorizationPolicy YAML - local template="$SERVICE_PATH/plugins/authorization-policy/template.yaml.tpl" - - gomplate -c .="$context_path" \ - -f "$template" \ - -o "$authz_file" - - rm "$context_path" - echo "✅ $visibility AuthorizationPolicy generated: $authz_file" - fi -} - -# Generate AuthorizationPolicies for both visibilities -generate_authz_policy "public" -generate_authz_policy "private" - -echo "=== Authorization Policy generation complete ===" diff --git a/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl b/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl deleted file mode 100644 index dfac449..0000000 --- a/endpoint-exposer/plugins/authorization-policy/template.yaml.tpl +++ /dev/null @@ -1,38 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: {{ .service_slug }}-{{ .service_id }}-authz-{{ .suffix }} - namespace: {{ .gateway_namespace }} - labels: - app.kubernetes.io/name: {{ .service_slug }} - nullplatform.com/service-id: "{{ .service_id }}" - nullplatform.com/managed-by: endpoint-exposer -spec: - # Apply to the Gateway workload - selector: - matchLabels: - gateway.networking.k8s.io/gateway-name: {{ .gateway_name }} - action: CUSTOM - provider: - name: {{ .opa_provider }} - rules: - {{- range .rules }} - # Rule for: {{ .method }} {{ .path }} (scope: {{ .scope_slug }}) - - to: - - operation: - hosts: ["{{ $.domain }}"] - {{- if ne .method "" }} - methods: ["{{ .method }}"] - {{- end }} - paths: - {{- if eq .path_type "Exact" }} - - "{{ .path }}" - {{- else if eq .path_type "PathPrefix" }} - - "{{ .path }}*" - {{- else if eq .path_type "RegularExpression" }} - # Regex: {{ .path_regex }} - # Note: Istio AuthorizationPolicy doesn't support regex in paths - # Using prefix matching as fallback for regex patterns - - "{{ .path_prefix }}*" - {{- end }} - {{- end }} diff --git a/endpoint-exposer/plugins/entrypoint b/endpoint-exposer/plugins/entrypoint deleted file mode 100755 index 9352a38..0000000 --- a/endpoint-exposer/plugins/entrypoint +++ /dev/null @@ -1,215 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -# This script executes plugins configured in the service specification -# It reads the service spec, iterates over plugins, and executes them with their environment variables - -echo "========================================" -echo "Plugin Entrypoint Execution Started" -echo "========================================" -echo "" - -# 1. Validate that CONTEXT exists -if [ -z "${CONTEXT:-}" ]; then - echo "ERROR: CONTEXT environment variable is not set" - echo "Plugin execution cannot proceed without CONTEXT" - exit 1 -fi - -# 2. Extract SERVICE_SPECIFICATION_ID from CONTEXT -# Try multiple paths where the ID could be located -SERVICE_SPEC_ID=$(echo "$CONTEXT" | jq -r ' - .service.specification.id // - .service_specification_id // - .specification.id // - empty -') - -if [ -z "$SERVICE_SPEC_ID" ] || [ "$SERVICE_SPEC_ID" = "null" ]; then - echo "No service specification ID found in CONTEXT" - echo "Checked paths: .service.specification.id, .service_specification_id, .specification.id" - echo "Skipping plugin execution" - echo "" - echo "Plugin execution completed (no plugins configured)" - exit 0 -fi - -echo "Service Specification ID: $SERVICE_SPEC_ID" -echo "" - -# 3. Fetch service specification from platform -echo "Fetching service specification from platform..." - -if ! SERVICE_SPEC=$(np service specification read --id "$SERVICE_SPEC_ID" --format json 2>&1); then - echo "ERROR: Failed to read service specification" - echo "Command: np service specification read --id $SERVICE_SPEC_ID --format json" - echo "Output: $SERVICE_SPEC" - exit 1 -fi - -echo "Service specification retrieved successfully" -echo "" - -# 4. Extract plugins array from service specification -PLUGINS=$(echo "$SERVICE_SPEC" | jq -c '.attributes.schema.plugins // []') - -if [ "$PLUGINS" = "[]" ] || [ "$PLUGINS" = "null" ]; then - echo "No plugins configured in service specification" - echo "Path checked: .attributes.schema.plugins" - echo "" - echo "Plugin execution completed (no plugins configured)" - exit 0 -fi - -# Count plugins -PLUGIN_COUNT=$(echo "$PLUGINS" | jq 'length') -echo "Found $PLUGIN_COUNT plugin(s) in service specification" -echo "" - -# 5. Iterate over each plugin and execute it -# Use process substitution to avoid subshell and maintain counters -PLUGIN_INDEX=0 -PLUGINS_EXECUTED=0 -PLUGINS_SKIPPED=0 -PLUGINS_FAILED=0 - -while IFS= read -r plugin; do - PLUGIN_INDEX=$((PLUGIN_INDEX + 1)) - - # Extract plugin configuration - PLUGIN_NAME=$(echo "$plugin" | jq -r '.name // "unnamed-plugin"') - # Use 'if has("enabled") then .enabled else true end' to handle false correctly - PLUGIN_ENABLED=$(echo "$plugin" | jq -r 'if has("enabled") then .enabled else true end') - - echo "========================================" - echo "Plugin $PLUGIN_INDEX/$PLUGIN_COUNT: $PLUGIN_NAME" - echo "========================================" - - # Check if plugin is enabled - if [ "$PLUGIN_ENABLED" != "true" ]; then - echo "Status: DISABLED" - echo "Skipping plugin '$PLUGIN_NAME'" - echo "" - PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) - continue - fi - - echo "Status: ENABLED" - - # Extract paths and executable - PLUGIN_PATH=$(echo "$plugin" | jq -r '.plugin_path // ""') - PLUGIN_EXEC=$(echo "$plugin" | jq -r '.exec // ""') - - if [ -z "$PLUGIN_PATH" ] || [ "$PLUGIN_PATH" = "null" ]; then - echo "ERROR: Plugin '$PLUGIN_NAME' has no plugin_path configured" - echo "Skipping plugin" - echo "" - PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) - continue - fi - - if [ -z "$PLUGIN_EXEC" ] || [ "$PLUGIN_EXEC" = "null" ]; then - echo "ERROR: Plugin '$PLUGIN_NAME' has no exec configured" - echo "Skipping plugin" - echo "" - PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) - continue - fi - - # Build full executable path - PLUGIN_EXECUTABLE="$PLUGIN_PATH/$PLUGIN_NAME$PLUGIN_EXEC" - - echo "Executable: $PLUGIN_EXECUTABLE" - - # Check that executable exists - if [ ! -f "$PLUGIN_EXECUTABLE" ]; then - echo "WARNING: Plugin executable not found at path: $PLUGIN_EXECUTABLE" - echo "Skipping plugin '$PLUGIN_NAME'" - echo "" - PLUGINS_SKIPPED=$((PLUGINS_SKIPPED + 1)) - continue - fi - - # Check that executable has execution permissions - if [ ! -x "$PLUGIN_EXECUTABLE" ]; then - echo "WARNING: Plugin executable is not executable: $PLUGIN_EXECUTABLE" - echo "Setting execute permissions..." - chmod +x "$PLUGIN_EXECUTABLE" - fi - - # 6. Export plugin environment variables - PLUGIN_ENV=$(echo "$plugin" | jq -r '.environment // {}') - - if [ "$PLUGIN_ENV" != "{}" ] && [ "$PLUGIN_ENV" != "null" ]; then - echo "Environment variables:" - - # Create subshell to export variables temporarily - ( - # Export each environment variable - while IFS= read -r export_line; do - if [ -n "$export_line" ]; then - echo " $export_line" - eval "$export_line" - fi - done < <(echo "$PLUGIN_ENV" | jq -r 'to_entries[] | "export \(.key)=\(.value | @sh)"') - - # 7. Execute plugin - echo "" - echo "Executing plugin..." - echo "----------------------------------------" - - if bash "$PLUGIN_EXECUTABLE"; then - EXIT_CODE=$? - echo "----------------------------------------" - echo "Plugin '$PLUGIN_NAME' completed successfully (exit code: $EXIT_CODE)" - else - EXIT_CODE=$? - echo "----------------------------------------" - echo "ERROR: Plugin '$PLUGIN_NAME' failed with exit code: $EXIT_CODE" - echo "WARNING: Continuing with remaining plugins despite failure" - fi - ) - - # Capture subshell exit code - SUBSHELL_EXIT=$? - if [ $SUBSHELL_EXIT -ne 0 ]; then - PLUGINS_FAILED=$((PLUGINS_FAILED + 1)) - else - PLUGINS_EXECUTED=$((PLUGINS_EXECUTED + 1)) - fi - else - echo "Environment variables: (none)" - echo "" - echo "Executing plugin..." - echo "----------------------------------------" - - if bash "$PLUGIN_EXECUTABLE"; then - EXIT_CODE=$? - echo "----------------------------------------" - echo "Plugin '$PLUGIN_NAME' completed successfully (exit code: $EXIT_CODE)" - PLUGINS_EXECUTED=$((PLUGINS_EXECUTED + 1)) - else - EXIT_CODE=$? - echo "----------------------------------------" - echo "ERROR: Plugin '$PLUGIN_NAME' failed with exit code: $EXIT_CODE" - echo "WARNING: Continuing with remaining plugins despite failure" - PLUGINS_FAILED=$((PLUGINS_FAILED + 1)) - fi - fi - - echo "" -done < <(echo "$PLUGINS" | jq -c '.[]') - -# 8. Final summary -echo "========================================" -echo "Plugin Execution Summary" -echo "========================================" -echo "Total plugins: $PLUGIN_COUNT" -echo "Executed successfully: $PLUGINS_EXECUTED" -echo "Skipped: $PLUGINS_SKIPPED" -echo "Failed: $PLUGINS_FAILED" -echo "" -echo "Plugin execution completed" - -exit 0 diff --git a/endpoint-exposer/specs/notification-channel.json.tpl b/endpoint-exposer/specs/notification-channel.json.tpl new file mode 100644 index 0000000..ee3c798 --- /dev/null +++ b/endpoint-exposer/specs/notification-channel.json.tpl @@ -0,0 +1,34 @@ +{ + "nrn": "{{ env.Getenv "NRN" }}", + "status": "active", + "type": "agent", + "source": [ + "telemetry", + "service" + ], + "configuration": { + "api_key": "{{ env.Getenv "NP_API_KEY" }}", + "command": { + "data": { + "cmdline": "{{ env.Getenv "REPO_PATH" }}/entrypoint --service-path={{ env.Getenv "REPO_PATH" }}/{{ env.Getenv "SERVICE_PATH" }}", + "environment": { + "NP_ACTION_CONTEXT": "'${NOTIFICATION_CONTEXT}'" + } + }, + "type": "exec" + }, + "selector": { + "environment": "{{ env.Getenv "ENVIRONMENT" }}" + } + }, + "filters": { + "$or": [ + { + "service.specification.slug": "{{ env.Getenv "SERVICE_SLUG" }}" + }, + { + "arguments.scope_provider": "{{ env.Getenv "SERVICE_SPECIFICATION_ID" }}" + } + ] + } +} \ No newline at end of file diff --git a/endpoint-exposer/test/CONTRIBUTING.md b/endpoint-exposer/test/CONTRIBUTING.md index 9b2b888..7da1759 100644 --- a/endpoint-exposer/test/CONTRIBUTING.md +++ b/endpoint-exposer/test/CONTRIBUTING.md @@ -124,8 +124,8 @@ Each test should verify one specific behavior: Use the format: `: ` ```bash -@test "authorization_policy: generates policy when enabled and routes exist" -@test "authorization_policy: creates marker when disabled" +@test "build_httproute: creates HTTPRoute when routes exist" +@test "build_httproute: creates marker when no routes exist" @test "build_httproute: fails with invalid visibility parameter" ``` @@ -187,10 +187,7 @@ For end-to-end workflow tests: export VISIBILITY="private" bash "$SERVICE_PATH/scripts/istio/build_httproute" - # Step 3: Generate authorization - bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - # Step 4: Apply + # Step 3: Apply run bash "$SERVICE_PATH/scripts/common/apply" # Assert complete workflow diff --git a/endpoint-exposer/test/test_authorization_policy.bats b/endpoint-exposer/test/test_authorization_policy.bats deleted file mode 100644 index 1c73b13..0000000 --- a/endpoint-exposer/test/test_authorization_policy.bats +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env bats - -load helpers - -setup() { - export TEST_TEMP_DIR="$(mktemp -d)" - export OUTPUT_DIR="$TEST_TEMP_DIR/output" - mkdir -p "$OUTPUT_DIR" - export SERVICE_PATH="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)" - - # Load assert functions - load_bats_support_libraries - - export K8S_NAMESPACE="test-namespace" - - # Mock gomplate - cat > "$TEST_TEMP_DIR/gomplate" << 'EOF' -#!/bin/bash -TEMPLATE_FILE="" -OUTPUT_FILE="" - -while [[ $# -gt 0 ]]; do - case $1 in - -f) TEMPLATE_FILE="$2"; shift 2 ;; - -o) OUTPUT_FILE="$2"; shift 2 ;; - -c) shift 2 ;; - *) shift ;; - esac -done - -if [[ -n "$TEMPLATE_FILE" ]] && [[ -n "$OUTPUT_FILE" ]]; then - cat > "$OUTPUT_FILE" << YAML -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: ${SERVICE_SLUG}-${SERVICE_ID}-authz-test - namespace: gateways -spec: - action: CUSTOM -YAML -fi -EOF - chmod +x "$TEST_TEMP_DIR/gomplate" - export PATH="$TEST_TEMP_DIR:$PATH" -} - -@test "authorization_policy: generates policies when enabled" { - export CONTEXT=$(load_fixture "public-and-private-routes") - source "$SERVICE_PATH/scripts/istio/build_context" - - run bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - assert_success - assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" - assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" -} - -@test "authorization_policy: creates marker files when disabled" { - export CONTEXT=$(load_fixture "authorization-disabled") - source "$SERVICE_PATH/scripts/istio/build_context" - - run bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - assert_success - assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" - assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" - assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" - assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" -} - -@test "authorization_policy: creates marker for public when no public routes" { - export CONTEXT=$(load_fixture "no-public-routes") - # Enable authorization - export CONTEXT=$(echo "$CONTEXT" | jq '.parameters.authorization.enabled = true') - source "$SERVICE_PATH/scripts/istio/build_context" - - run bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - assert_success - assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" - assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" -} - -@test "authorization_policy: creates marker for private when no private routes" { - export CONTEXT=$(load_fixture "simple-public-routes") - # Enable authorization - export CONTEXT=$(echo "$CONTEXT" | jq '.parameters.authorization.enabled = true') - source "$SERVICE_PATH/scripts/istio/build_context" - - run bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - assert_success - assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" - assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" -} - -@test "authorization_policy: creates marker when no domain" { - export CONTEXT='{ - "service": {"id": "test-id", "slug": "test"}, - "parameters": { - "publicDomain": "", - "privateDomain": "", - "authorization": {"enabled": true} - }, - "routes": [{"path": "/test", "method": "GET", "scope": "test", "visibility": "public"}] - }' - source "$SERVICE_PATH/scripts/istio/build_context" - - run bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - assert_success - assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" - assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" -} - -@test "authorization_policy: builds rules correctly" { - export CONTEXT=$(load_fixture "public-and-private-routes") - source "$SERVICE_PATH/scripts/istio/build_context" - - run bash "$SERVICE_PATH/plugins/authorization-policy/generate" - - assert_success - - # Both files should be created - assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" - assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" -} diff --git a/endpoint-exposer/test/test_integration.bats b/endpoint-exposer/test/test_integration.bats index 879348d..d094fe5 100644 --- a/endpoint-exposer/test/test_integration.bats +++ b/endpoint-exposer/test/test_integration.bats @@ -128,15 +128,10 @@ teardown() { export VISIBILITY="private" bash "$SERVICE_PATH/scripts/istio/build_httproute" - # Step 4: Generate authorization policies (should create markers when disabled) - bash "$SERVICE_PATH/plugins/authorization-policy/generate" - # Verify outputs assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" assert_file_not_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" assert_file_exists "$OUTPUT_DIR/.httproute-private-deleted" - assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" - assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" # Verify public HTTPRoute content assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "HTTPRoute" @@ -156,26 +151,17 @@ teardown() { export VISIBILITY="private" bash "$SERVICE_PATH/scripts/istio/build_httproute" - # Generate authorization policies - bash "$SERVICE_PATH/plugins/authorization-policy/generate" - # Verify all resources created assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" - assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" - assert_file_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" # Verify no marker files (all resources should be created) assert_file_not_exists "$OUTPUT_DIR/.httproute-public-deleted" assert_file_not_exists "$OUTPUT_DIR/.httproute-private-deleted" - assert_file_not_exists "$OUTPUT_DIR/.authz-public-deleted" - assert_file_not_exists "$OUTPUT_DIR/.authz-private-deleted" # Verify content assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "api.edenred.nullimplementation.com" assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "api-private.edenred.nullimplementation.com" - assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "AuthorizationPolicy" - assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "AuthorizationPolicy" } @test "integration: workflow with authorization disabled creates cleanup markers" { @@ -191,18 +177,9 @@ teardown() { export VISIBILITY="private" bash "$SERVICE_PATH/scripts/istio/build_httproute" - # Generate authorization policies (should create markers) - bash "$SERVICE_PATH/plugins/authorization-policy/generate" - # Verify httproutes created assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" assert_file_exists "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" - - # Verify authz markers created (authorization disabled) - assert_file_exists "$OUTPUT_DIR/.authz-public-deleted" - assert_file_exists "$OUTPUT_DIR/.authz-private-deleted" - assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" - assert_file_not_exists "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" } @test "integration: apply step handles markers and resources correctly" { @@ -218,9 +195,6 @@ teardown() { export VISIBILITY="private" bash "$SERVICE_PATH/scripts/istio/build_httproute" - # Generate authorization policies - bash "$SERVICE_PATH/plugins/authorization-policy/generate" - # Run apply run bash "$SERVICE_PATH/scripts/common/apply" @@ -228,8 +202,6 @@ teardown() { # Should detect and process markers assert_output --partial "Private HTTPRoute marked for deletion" - assert_output --partial "Public AuthorizationPolicy marked for deletion" - assert_output --partial "Private AuthorizationPolicy marked for deletion" # Should apply the public httproute assert_output --partial "Applying 1 resources" @@ -248,14 +220,9 @@ teardown() { export VISIBILITY="private" bash "$SERVICE_PATH/scripts/istio/build_httproute" - # Generate authorization policies - bash "$SERVICE_PATH/plugins/authorization-policy/generate" - # Verify all resources have required labels assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "nullplatform.com/managed-by: endpoint-exposer" assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "nullplatform.com/managed-by: endpoint-exposer" - assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "nullplatform.com/managed-by: endpoint-exposer" - assert_file_contains "$OUTPUT_DIR/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "nullplatform.com/managed-by: endpoint-exposer" assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml" "nullplatform.com/service-id:" assert_file_contains "$OUTPUT_DIR/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml" "nullplatform.com/service-id:" diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 39d09b0..6c633bc 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -45,9 +45,6 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "execute plugins" - type: script - file: "$SERVICE_PATH/plugins/entrypoint" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 42f559d..85067b5 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -45,16 +45,6 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "generate authorization policy" - type: script - file: "$SERVICE_PATH/plugins/authorization-policy/generate" - output: - - name: AUTHZ_POLICY_PUBLIC_FILE - type: file - file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-public.yaml" - - name: AUTHZ_POLICY_PRIVATE_FILE - type: file - file: "$OUTPUT_DIR/authorization-policy-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From f6cbf48a301103bd625fb1647d01c22aa663acdd Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 19 Jan 2026 15:55:16 -0300 Subject: [PATCH 036/101] feat: change output dir read --- endpoint-exposer/scripts/istio/build_context | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index 080403a..f414861 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -26,7 +26,7 @@ CONTEXT=$(echo "$CONTEXT" | jq \ '. + {k8s_namespace: $k8s_namespace}') # Only set OUTPUT_DIR if not already set (allows tests to override) -if [[ -z "$OUTPUT_DIR" ]]; then +if [[ -z "${OUTPUT_DIR:-}" ]]; then export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_SLUG-$SERVICE_ID/$ACTION_NAME-$ACTION_ID" fi From 60e967b32616f1cbb9838d9d987f5b97e7a37b94 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 20 Jan 2026 09:26:28 -0300 Subject: [PATCH 037/101] feat: adapt overrides path --- endpoint-exposer/entrypoint/service | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service index aac8270..9b78a5f 100755 --- a/endpoint-exposer/entrypoint/service +++ b/endpoint-exposer/entrypoint/service @@ -26,4 +26,12 @@ if [[ -f "$OVERRIDES_WORKFLOW_PATH" ]]; then fi echo "Executing command: $CMD" + +# Note: The 'np service workflow exec' CLI internally modifies OVERRIDES_PATH +# It sets it to dirname(dirname(--overrides file path)) +# Example: --overrides /path/to/plugin/workflows/create.yaml +# Results in: OVERRIDES_PATH=/path/to (loses the 'plugin' directory) +# This is why plugin workflows must use: $OVERRIDES_PATH/plugin-name/scripts/... +export OVERRIDES_PATH + eval "$CMD" From b9431f51f4f7a4d636480ba1ea2f2bd9b56a8216 Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 20 Jan 2026 09:47:38 -0300 Subject: [PATCH 038/101] feat: adapt overrides path --- endpoint-exposer/entrypoint/service | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service index 9b78a5f..c758c75 100755 --- a/endpoint-exposer/entrypoint/service +++ b/endpoint-exposer/entrypoint/service @@ -19,7 +19,7 @@ WORKFLOW_PATH="$SERVICE_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$ACTION_TO_EXECUTE.yaml" VALUES_PATH="$SERVICE_PATH/values.yaml" -CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH" +CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH --build-context --include-secrets" if [[ -f "$OVERRIDES_WORKFLOW_PATH" ]]; then CMD="$CMD --overrides $OVERRIDES_WORKFLOW_PATH" @@ -27,11 +27,12 @@ fi echo "Executing command: $CMD" -# Note: The 'np service workflow exec' CLI internally modifies OVERRIDES_PATH -# It sets it to dirname(dirname(--overrides file path)) -# Example: --overrides /path/to/plugin/workflows/create.yaml -# Results in: OVERRIDES_PATH=/path/to (loses the 'plugin' directory) -# This is why plugin workflows must use: $OVERRIDES_PATH/plugin-name/scripts/... +# Note: The 'np service workflow exec' CLI automatically extracts OVERRIDES_PATH +# It uses regex /[^/]+/workflows/.*\.yaml$ to strip the workflows part +# Example: --overrides /root/.np/plugin/workflows/create.yaml +# Results in: OVERRIDES_PATH=/root/.np/plugin (correct) +# Workflow files should use: $OVERRIDES_PATH/scripts/... (no double nesting needed) +# See: cli/cmd/service/workflow/exec/service_workflow_exec.go getOverridesBasePath() export OVERRIDES_PATH eval "$CMD" From 442e5dc2e0eee3a4a4c8e361311eefdfa18239ca Mon Sep 17 00:00:00 2001 From: Javi Date: Tue, 20 Jan 2026 09:59:11 -0300 Subject: [PATCH 039/101] feat: add subfolder to avoid cli regex issue --- endpoint-exposer/entrypoint/service | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service index c758c75..2a59294 100755 --- a/endpoint-exposer/entrypoint/service +++ b/endpoint-exposer/entrypoint/service @@ -16,7 +16,7 @@ echo "INGRESS_TYPE is set to '$INGRESS_TYPE'" echo "OVERRIDES_PATH is set to '$OVERRIDES_PATH'" WORKFLOW_PATH="$SERVICE_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" -OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$ACTION_TO_EXECUTE.yaml" +OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/service/workflows/$ACTION_TO_EXECUTE.yaml" VALUES_PATH="$SERVICE_PATH/values.yaml" CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH --build-context --include-secrets" From 0e6f985160e41618a8ab2dbd856334a0a59c96d8 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 23 Jan 2026 16:39:26 -0300 Subject: [PATCH 040/101] feat: add change to support plugin --- endpoint-exposer/README.md | 233 ++++++++++++++++++ endpoint-exposer/entrypoint/service | 5 +- .../.authz-private-deleted | 0 .../.authz-public-deleted | 0 .../.httproute-private-deleted | 0 .../.httproute-public-deleted | 0 ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 11 - ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 11 - ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 12 - ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 12 - ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 7 - ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 7 - ...0-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml | 8 - ...60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml | 8 - .../null-null/.authz-private-deleted | 0 .../null-null/.authz-public-deleted | 0 .../null-null/.httproute-public-deleted | 0 17 files changed, 236 insertions(+), 78 deletions(-) create mode 100644 endpoint-exposer/README.md delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-private-deleted delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-public-deleted delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-private-deleted delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-public-deleted delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml delete mode 100644 endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml delete mode 100644 endpoint-exposer/output/test-test-id/null-null/.authz-private-deleted delete mode 100644 endpoint-exposer/output/test-test-id/null-null/.authz-public-deleted delete mode 100644 endpoint-exposer/output/test-test-id/null-null/.httproute-public-deleted diff --git a/endpoint-exposer/README.md b/endpoint-exposer/README.md new file mode 100644 index 0000000..db45d7d --- /dev/null +++ b/endpoint-exposer/README.md @@ -0,0 +1,233 @@ +# Endpoint Exposer Service + +## Overview + +The **endpoint-exposer** service is a critical infrastructure component of Nullplatform that manages dynamic exposure of application endpoints through public and private domains. It functions as a route orchestrator that translates high-level specifications into native Kubernetes configurations using Istio Service Mesh. + +## Core Responsibilities + +### 1. Dynamic Endpoint Management +- Expose application endpoints declaratively +- Configure separate public and private domains for different access levels +- Update route configurations with zero downtime +- Maintain configuration synchronized with desired state + +### 2. Route Configuration +- Define route patterns (exact, regex, wildcards) +- Specify allowed HTTP methods (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS) +- Associate routes with Nullplatform scopes for access control +- Control route visibility (public vs. private) + +### 3. Kubernetes and Istio Integration +- Generate HTTPRoute resources (Kubernetes Gateway API v1) +- Create Istio AuthorizationPolicies for access control +- Configure RequestAuthentication for JWT validation +- Apply routing policies in the service mesh + +### 4. Scope-Based Access Control +- Map endpoints to specific Nullplatform scopes +- Integrate with Open Policy Agent (OPA) for external authorization +- Implement granular authorization policies per route +- Validate JWT tokens according to scope configuration + +## Key Features + +### Route Management +```yaml +routes: + - method: GET + path: /api/users + scope: user-management + visible_on: public + enable_authorization: true +``` + +- **Path Types**: + - Exact: `/api/users` + - Regex with parameters: `/api/users/{id}` + - Wildcard: `/api/users/*` + +- **HTTP Methods**: Supports all standard HTTP methods +- **Visibility**: Public or private routes on separate domains +- **Authorization**: Optional per-route with JWT and OPA validation + +### Domain Separation + +**Public Domain:** +- Endpoints accessible from the internet +- Typically for public APIs and unauthenticated resources +- Connected to `gateway-public` gateway + +**Private Domain:** +- Internal organization endpoints +- Requires private network access +- Connected to `gateway-private` gateway + +### Authorization and Security + +- **JWT Validation**: Verifies authentication tokens on each request +- **Authorization Policies**: Istio AuthorizationPolicies per route +- **OPA Integration**: Authorization decisions delegated to Open Policy Agent +- **Scope Control**: Route-to-scope mapping for granular permissions + +## Architecture + +### Workflow + +1. **Build Context** + - Extracts service action parameters + - Retrieves Kubernetes namespace information + - Classifies routes by visibility (public/private) + +2. **Build HTTPRoutes** + - Generates base HTTPRoute templates per domain + - Queries scopes associated with each route + - Constructs Istio routing rules + +3. **Process Routes** + - Sorts routes by specificity (exact > regex > prefix) + - Generates AuthorizationPolicies if authorization is enabled + - Maps scope IDs to backend services + +4. **Apply Configuration** + - Applies generated YAML manifests to the cluster + - Manages cleanup of obsolete resources + - Maintains tracking of applied resources + +### Technologies + +- **Kubernetes**: Orchestration platform (Gateway API v1) +- **Istio**: Service mesh for traffic management and security +- **Bash**: Workflow scripting and automation +- **jq**: JSON processing and manipulation +- **gomplate**: Resource template generation +- **kubectl**: Kubernetes resource management +- **OPA**: Open Policy Agent for external authorization + +### Nullplatform Integration + +- **Scopes Service**: Scope queries for authorization (`np scope list/read`) +- **Notification Channel**: Receives events to trigger actions +- **Service Specification**: Service specification registration +- **Deployment Service**: Coordination with deployment workflows + +## File Structure + +``` +/endpoint-exposer +├── configure # Service configuration script +├── entrypoint/ # Entry points for actions +│ ├── service-action # Service action handler +│ └── link-action # Dependency handler +├── specs/ # Service specifications +│ ├── service-specification.json +│ └── link-specification.json +├── workflows/istio/ # Workflow definitions +│ ├── service-action.json +│ └── link-action.json +├── scripts/istio/ # Core routing logic +│ ├── build_context +│ ├── build_httproute +│ ├── process_routes +│ ├── build_rule +│ └── build_ingress_with_rule +├── scripts/common/ # Shared utilities +│ ├── apply +│ └── delete +├── templates/istio/ # K8s resource templates +│ ├── httproute.yaml.tmpl +│ ├── authz-policy.yaml.tmpl +│ └── request-authn.yaml.tmpl +├── test/ # BATS test suite +├── examples/ # Example configurations +└── container-scope-override/ # Custom deployment support +``` + +## Configuration + +### Environment Variables + +- `K8S_NAMESPACE`: Kubernetes namespace for resources (default: `nullplatform`) +- `PUBLIC_GATEWAY_NAME`: Public gateway name (default: `gateway-public`) +- `PRIVATE_GATEWAY_NAME`: Private gateway name (default: `gateway-private`) +- `GATEWAY_NAMESPACE`: Gateway namespace (default: `gateways`) +- `OPA_PROVIDER_NAME`: OPA authorization provider (default: `opa-ext-authz`) + +### Route Configuration Example + +```json +{ + "routes": [ + { + "method": "GET", + "path": "/api/v1/resource/{id}", + "scope": "resource-read", + "visible_on": "public", + "enable_authorization": true + }, + { + "method": "POST", + "path": "/api/v1/resource", + "scope": "resource-write", + "visible_on": "private", + "enable_authorization": true + } + ], + "public_domain": "api.example.com", + "private_domain": "internal-api.example.com" +} +``` + +## Testing + +The service uses BATS (Bash Automated Testing System) for testing: + +```bash +# Run all tests +./test/run-tests.sh + +# Run specific tests +bats test/istio/ +``` + +Tests cover: +- Simple routes +- Public and private routes +- Authorization scenarios +- JWT configurations +- Manifest generation + +## Operations + +### Create/Update Endpoints + +The service responds to Nullplatform actions: +- `create`: Generates and applies initial configuration +- `update`: Modifies existing configuration +- `delete`: Cleans up Kubernetes resources + +### Monitoring + +Generated resources can be monitored with: + +```bash +# View HTTPRoutes +kubectl get httproutes -n + +# View AuthorizationPolicies +kubectl get authorizationpolicies -n + +# View gateway status +kubectl get gateway -n gateways +``` + +## Use Cases + +1. **Public API**: Expose public REST endpoints with rate limiting and optional authentication +2. **Admin API**: Private endpoints with JWT authentication and scope-based authorization +3. **Internal Microservices**: Private routes between services with authorization policies +4. **Multi-Version APIs**: Path-based routing for API versioning + +## Contributing + +See [test/README.md](test/README.md) for information about testing and development. diff --git a/endpoint-exposer/entrypoint/service b/endpoint-exposer/entrypoint/service index 2a59294..877b2d2 100755 --- a/endpoint-exposer/entrypoint/service +++ b/endpoint-exposer/entrypoint/service @@ -28,8 +28,9 @@ fi echo "Executing command: $CMD" # Note: The 'np service workflow exec' CLI automatically extracts OVERRIDES_PATH -# It uses regex /[^/]+/workflows/.*\.yaml$ to strip the workflows part -# Example: --overrides /root/.np/plugin/workflows/create.yaml +# It uses regex /[^/]+/workflows/[^/]+\.yaml$ to strip the /folder/workflows/file.yaml part +# Example: --overrides /root/.np/plugin/service/workflows/create.yaml +# Regex matches: /service/workflows/create.yaml # Results in: OVERRIDES_PATH=/root/.np/plugin (correct) # Workflow files should use: $OVERRIDES_PATH/scripts/... (no double nesting needed) # See: cli/cmd/service/workflow/exec/service_workflow_exec.go getOverridesBasePath() diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-private-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-private-deleted deleted file mode 100644 index e69de29..0000000 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-public-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.authz-public-deleted deleted file mode 100644 index e69de29..0000000 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-private-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-private-deleted deleted file mode 100644 index e69de29..0000000 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-public-deleted b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/.httproute-public-deleted deleted file mode 100644 index e69de29..0000000 diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml deleted file mode 100644 index 0901ea7..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-private - namespace: nullplatform - labels: - nullplatform.com/managed-by: endpoint-exposer - nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" - app.kubernetes.io/name: api -spec: - action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml deleted file mode 100644 index d6aa398..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-public - namespace: nullplatform - labels: - nullplatform.com/managed-by: endpoint-exposer - nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" - app.kubernetes.io/name: api -spec: - action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml deleted file mode 100644 index 7a0b160..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private - namespace: nullplatform - labels: - nullplatform.com/managed-by: endpoint-exposer - nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" - app.kubernetes.io/name: api -spec: - hostnames: - - api-private.edenred.nullimplementation.com diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml deleted file mode 100644 index bfe1e9d..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/apply/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public - namespace: nullplatform - labels: - nullplatform.com/managed-by: endpoint-exposer - nullplatform.com/service-id: "fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd" - app.kubernetes.io/name: api -spec: - hostnames: - - api.edenred.nullimplementation.com diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml deleted file mode 100644 index d3d3ae3..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-test - namespace: gateways -spec: - action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml deleted file mode 100644 index d3d3ae3..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/authorization-policy-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-authz-test - namespace: gateways -spec: - action: CUSTOM diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml deleted file mode 100644 index 2a72402..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-private.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public - namespace: nullplatform -spec: - hostnames: - - diff --git a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml b/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml deleted file mode 100644 index 2a72402..0000000 --- a/endpoint-exposer/output/api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd/update-api-5b7636e1-304b-4ef9-92a9-2a0b102686f4/httproute-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: api-fbcf7a60-8ca8-4bf2-b1b5-5c59bb5bc4fd-public - namespace: nullplatform -spec: - hostnames: - - diff --git a/endpoint-exposer/output/test-test-id/null-null/.authz-private-deleted b/endpoint-exposer/output/test-test-id/null-null/.authz-private-deleted deleted file mode 100644 index e69de29..0000000 diff --git a/endpoint-exposer/output/test-test-id/null-null/.authz-public-deleted b/endpoint-exposer/output/test-test-id/null-null/.authz-public-deleted deleted file mode 100644 index e69de29..0000000 diff --git a/endpoint-exposer/output/test-test-id/null-null/.httproute-public-deleted b/endpoint-exposer/output/test-test-id/null-null/.httproute-public-deleted deleted file mode 100644 index e69de29..0000000 From 73b2f7c550d38ddc0ff4675a5bd3704764da1582 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 23 Jan 2026 16:53:46 -0300 Subject: [PATCH 041/101] feat: new service spec for avp --- endpoint-exposer/specs/service-spec.json.tpl | 229 +++++++++++++------ 1 file changed, 159 insertions(+), 70 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 2ac6916..9533545 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -4,9 +4,62 @@ "schema": { "type": "object", "$schema": "http://json-schema.org/draft-07/schema#", + "required": [ + "publicDomain" + ], "uiSchema": { "type": "VerticalLayout", "elements": [ + { + "type": "Categorization", + "options": { + "collapsable": { + "label": "Documentation", + "collapsed": true + } + }, + "elements": [ + { + "type": "Category", + "label": "Domains", + "elements": [ + { + "text": "### Public Domain\nBase domain for routes exposed to external traffic. Requests matching routes with `visibility: public` will be served through this domain.\n\n### Private Domain\nBase domain for routes accessible only within the internal network. Use this for service-to-service communication.", + "type": "Label", + "options": { + "format": "markdown" + } + } + ] + }, + { + "type": "Category", + "label": "Routes", + "elements": [ + { + "text": "### Route Configuration\nDefine how incoming requests are matched and forwarded to backend services.\n\n| Field | Description |\n|-------|-------------|\n| **Verb** | HTTP method to match (GET, POST, PUT, etc.) |\n| **Path** | URL path pattern (e.g., `/api/v1/users`) |\n| **Scope** | Target service that will handle the request |\n| **Visibility** | `public` (external) or `private` (internal network only) |\n| **Groups** | Security groups allowed to access this route. Leave empty for unrestricted access |", + "type": "Label", + "options": { + "format": "markdown" + } + } + ] + }, + { + "type": "Category", + "label": "Examples", + "elements": [ + { + "text": "### Public API Route\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/api/v1/wells\",\n \"scope\": \"wells-service\",\n \"visibility\": \"public\",\n \"groups\": []\n}\n```\n\n### Protected Internal Route\n```json\n{\n \"method\": \"POST\",\n \"path\": \"/internal/sync\",\n \"scope\": \"sync-service\",\n \"visibility\": \"private\",\n \"groups\": [\"AWS_PlataformaUpstream_Administrador_Desa\"]\n}\n```", + "type": "Label", + "options": { + "format": "markdown" + } + } + ] + } + ] + }, { "type": "Group", "label": "Domains", @@ -30,30 +83,39 @@ "scope": "#/properties/routes", "options": { "detail": { - "type": "GridLayout", - "columns": 4, - "elements": [ - { - "type": "Control", - "label": "Verb", - "scope": "#/items/properties/method" - }, - { - "type": "Control", - "label": "Path", - "scope": "#/items/properties/path" - }, - { - "type": "Control", - "label": "Scope", - "scope": "#/items/properties/scope" - }, - { - "type": "Control", - "label": "Visibility", - "scope": "#/items/properties/visibility" - } - ] + "type": "VerticalLayout", + "elements": [ + { + "type": "Control", + "label": "Verb", + "scope": "#/properties/method" + }, + { + "type": "HorizontalLayout", + "elements": [ + { + "type": "Control", + "label": "Path", + "scope": "#/properties/path" + }, + { + "type": "Control", + "label": "Scope", + "scope": "#/properties/scope" + }, + { + "type": "Control", + "label": "Visibility", + "scope": "#/properties/visibility" + } + ] + }, + { + "type": "Control", + "label": "Groups", + "scope": "#/properties/groups" + } + ] }, "showSortButtons": true } @@ -63,34 +125,18 @@ ] }, "properties": { - "publicDomain": { - "type": "string", - "title": "Public Domain", - "description": "Domain for public routes", - "enum": [ - "birds.edenred.nullimplementation.com", - "api.edenred.nullimplementation.com" - ], - "editableOn": ["create", "update"] - }, - "privateDomain": { - "type": "string", - "title": "Private Domain", - "description": "Domain for private routes", - "enum": [ - "birds-private.edenred.nullimplementation.com", - "api-private.edenred.nullimplementation.com" - ], - "editableOn": ["create", "update"] - }, "routes": { + "type": "array", + "title": "Routes", "items": { + "type": "object", + "required": [ + "method", + "path", + "scope", + "visibility" + ], "properties": { - "method": { - "type": "string", - "title": "Verb", - "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] - }, "path": { "type": "string", "title": "Path" @@ -98,46 +144,89 @@ "scope": { "type": "string", "title": "Scope", - "description": "The scope slug", "additionalKeywords": { "enum": "[.scopes[]?.slug]" } }, + "groups": { + "type": "array", + "title": "Authorized Groups", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "AWS_PlataformaUpstream_Gestor_Desa", + "AWS_PlataformaUpstream_Programador_Desa", + "AWS_PlataformaUpstream_Pulling_Desa", + "AWS_PlataformaUpstream_Workover_Desa", + "AWS_PlataformaUpstream_Visita_Desa", + "AWS_PlataformaUpstream_Administrador_Desa" + ] + } + }, + "method": { + "type": "string", + "title": "Verb", + "enum": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + "HEAD", + "OPTIONS" + ] + }, "visibility": { "type": "string", "title": "Visibility", - "description": "Route visibility level", - "enum": ["public", "private"], - "default": "public" + "default": "public", + "enum": [ + "public", + "private" + ] } - }, - "required": [ - "method", - "path", - "scope", - "visibility" - ], - "type": "object" - }, - "type": "array" + } + } + }, + "publicDomain": { + "type": "string", + "title": "Public Domain", + "editableOn": [ + "create", + "update" + ], + "enum": [ + "api.pae-infra.nullapps.io", + "services.pae-infra.nullapps.io" + ] + }, + "privateDomain": { + "type": "string", + "title": "Private Domain", + "editableOn": [ + "create", + "update" + ], + "enum": [ + "api-internal.pae-infra.nullapps.io", + "services-internal.pae-infra.nullapps.io" + ] } - }, - "required": [ - "publicDomain" - ], - "type": "object" + } }, "values": {} }, "dimensions": {}, - "name": "Service exposer V2", + "scopes": {}, + "name": "Endpoint exposer", "selectors": { "category": "any", "imported": false, "provider": "any", "sub_category": "any" }, - "slug": "service-exposer", + "slug": "endpoint-exposer", "type": "dependency", "use_default_actions": true, "available_actions": [ From 33824d51e94948d30704737478f01fe01b387c11 Mon Sep 17 00:00:00 2001 From: Javi Date: Mon, 26 Jan 2026 11:37:47 -0300 Subject: [PATCH 042/101] chore: change list of domains for PAE --- endpoint-exposer/specs/service-spec.json.tpl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index bb9deea..2fa89bc 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -229,8 +229,7 @@ "update" ], "enum": [ - "api.pae-infra.nullapps.io", - "services.pae-infra.nullapps.io" + "idp.poc.nullapps.io" ] }, "privateDomain": { @@ -241,8 +240,7 @@ "update" ], "enum": [ - "api-internal.pae-infra.nullapps.io", - "services-internal.pae-infra.nullapps.io" + "idp.poc.nullapps.io" ] } } From 4971a66fa14343ec14dd146634b3b5a6f19d833c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 13:08:21 -0300 Subject: [PATCH 043/101] fix: escape quotes in visible_to gomplate expression to produce valid JSON --- endpoint-exposer/specs/service-spec.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 2fa89bc..302f4bf 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -265,6 +265,6 @@ "available_links": [ ], "visible_to": [ - "{{ env.Getenv "NRN" }}" + "{{ env.Getenv \"NRN\" }}" ] } From b8497311e4d0ddb29af8590b363737d1d58c6221 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 14:45:14 -0300 Subject: [PATCH 044/101] fix(endpoint-exposer): update domain enum to api-private.playground.nullapps.io Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 302f4bf..b71d028 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -229,7 +229,7 @@ "update" ], "enum": [ - "idp.poc.nullapps.io" + "api-private.playground.nullapps.io" ] }, "privateDomain": { @@ -240,7 +240,7 @@ "update" ], "enum": [ - "idp.poc.nullapps.io" + "api-private.playground.nullapps.io" ] } } From 853fd63294407a17a61141e244477d83165aa599 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 15:15:58 -0300 Subject: [PATCH 045/101] feat(endpoint-exposer): generate AuthorizationPolicy per service from routes Each service create/update now generates an Istio AuthorizationPolicy in the gateways namespace that applies AVP ext_authz (avp-ext-authz provider) to exactly the routes and methods the service exposes. The static protected_paths config is no longer needed. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/istio/build_authz_policy | 77 +++++++++++++++++++ .../templates/istio/authz-policy.yaml.tpl | 26 +++++++ endpoint-exposer/workflows/istio/create.yaml | 20 ++++- endpoint-exposer/workflows/istio/update.yaml | 18 +++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100755 endpoint-exposer/scripts/istio/build_authz_policy create mode 100644 endpoint-exposer/templates/istio/authz-policy.yaml.tpl diff --git a/endpoint-exposer/scripts/istio/build_authz_policy b/endpoint-exposer/scripts/istio/build_authz_policy new file mode 100755 index 0000000..296d247 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_authz_policy @@ -0,0 +1,77 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/istio/config" + +VISIBILITY="${VISIBILITY:-public}" +AVP_PROVIDER_NAME="${AVP_PROVIDER_NAME:-avp-ext-authz}" + +echo "=== Building ${VISIBILITY} AuthorizationPolicy ===" + +if [[ "$VISIBILITY" == "public" ]]; then + ROUTES_JSON="$PUBLIC_ROUTES_JSON" + DOMAIN="$PUBLIC_DOMAIN" + SUFFIX="public" +elif [[ "$VISIBILITY" == "private" ]]; then + ROUTES_JSON="$PRIVATE_ROUTES_JSON" + DOMAIN="$PRIVATE_DOMAIN" + SUFFIX="private" +else + echo "ERROR: Invalid VISIBILITY value: $VISIBILITY" + exit 1 +fi + +NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') +DOMAIN_EMPTY=false +if [[ -z "$DOMAIN" ]] || [[ "$DOMAIN" == "null" ]]; then + DOMAIN_EMPTY=true +fi + +if [[ "$NUM_ROUTES" -eq 0 ]] || [[ "$DOMAIN_EMPTY" == "true" ]]; then + echo "No $VISIBILITY routes or domain, marking authz policy for cleanup..." + touch "$OUTPUT_DIR/.authz-$SUFFIX-deleted" + exit 0 +fi + +# Deduplicate methods and collect paths from routes +PATHS_JSON=$(echo "$ROUTES_JSON" | jq '[.[].path] | unique') +METHODS_JSON=$(echo "$ROUTES_JSON" | jq '[.[].method] | unique') + +echo "$VISIBILITY Domain: $DOMAIN" +echo "Paths: $PATHS_JSON" +echo "Methods: $METHODS_JSON" + +AUTHZ_CONTEXT=$(jq -n \ + --arg service_slug "$SERVICE_SLUG" \ + --arg service_id "$SERVICE_ID" \ + --arg domain "$DOMAIN" \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg suffix "$SUFFIX" \ + --arg provider_name "$AVP_PROVIDER_NAME" \ + --argjson paths "$PATHS_JSON" \ + --argjson methods "$METHODS_JSON" \ + '{ + service_slug: $service_slug, + service_id: $service_id, + domain: $domain, + gateway_namespace: $gateway_namespace, + suffix: $suffix, + provider_name: $provider_name, + paths: $paths, + methods: $methods + }') + +CONTEXT_PATH="$OUTPUT_DIR/authz-$SUFFIX-context-$SERVICE_ID.json" +AUTHZ_FILE="$OUTPUT_DIR/authz-$SERVICE_ID-$SUFFIX.yaml" + +echo "$AUTHZ_CONTEXT" > "$CONTEXT_PATH" + +gomplate -c .="$CONTEXT_PATH" \ + -f "$SERVICE_PATH/templates/istio/authz-policy.yaml.tpl" \ + -o "$AUTHZ_FILE" + +rm "$CONTEXT_PATH" + +echo "✅ $VISIBILITY AuthorizationPolicy created: $AUTHZ_FILE" +export AUTHZ_FILE diff --git a/endpoint-exposer/templates/istio/authz-policy.yaml.tpl b/endpoint-exposer/templates/istio/authz-policy.yaml.tpl new file mode 100644 index 0000000..5b09fe5 --- /dev/null +++ b/endpoint-exposer/templates/istio/authz-policy.yaml.tpl @@ -0,0 +1,26 @@ +apiVersion: security.istio.io/v1 +kind: AuthorizationPolicy +metadata: + name: {{ .service_slug }}-{{ .service_id }}-authz-{{ .suffix }} + namespace: {{ .gateway_namespace }} + labels: + app.kubernetes.io/name: {{ .service_slug }} + nullplatform.com/service-id: "{{ .service_id }}" + nullplatform.com/managed-by: endpoint-exposer +spec: + selector: + matchLabels: + gateway.networking.k8s.io/gateway-name: gateway-{{ .suffix }} + action: CUSTOM + provider: + name: {{ .provider_name }} + rules: + - to: + - operation: + hosts: + - {{ .domain }} + methods: +{{ range .methods }} - {{ . }} +{{ end }} paths: +{{ range .paths }} - {{ . }} +{{ end }} diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 6c633bc..ba16ee8 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -45,9 +45,27 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + - name: "build public authz policy" + type: script + file: "$SERVICE_PATH/scripts/istio/build_authz_policy" + configuration: + VISIBILITY: "public" + output: + - name: AUTHZ_PUBLIC_FILE + type: file + file: "$OUTPUT_DIR/authz-$SERVICE_ID-public.yaml" + - name: "build private authz policy" + type: script + file: "$SERVICE_PATH/scripts/istio/build_authz_policy" + configuration: + VISIBILITY: "private" + output: + - name: AUTHZ_PRIVATE_FILE + type: file + file: "$OUTPUT_DIR/authz-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" configuration: ACTION: apply - DRY_RUN: false \ No newline at end of file + DRY_RUN: false diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 85067b5..ba16ee8 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -45,6 +45,24 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" + - name: "build public authz policy" + type: script + file: "$SERVICE_PATH/scripts/istio/build_authz_policy" + configuration: + VISIBILITY: "public" + output: + - name: AUTHZ_PUBLIC_FILE + type: file + file: "$OUTPUT_DIR/authz-$SERVICE_ID-public.yaml" + - name: "build private authz policy" + type: script + file: "$SERVICE_PATH/scripts/istio/build_authz_policy" + configuration: + VISIBILITY: "private" + output: + - name: AUTHZ_PRIVATE_FILE + type: file + file: "$OUTPUT_DIR/authz-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From f153141f0fbc2d4ef0d83d47b92e2c2b1a751900 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 15:28:54 -0300 Subject: [PATCH 046/101] fix(endpoint-exposer): re-add idp.poc.nullapps.io to enum for backward compat Allows existing service instances with the old domain to pass validation during delete/update actions. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index b71d028..0d5486b 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -229,7 +229,8 @@ "update" ], "enum": [ - "api-private.playground.nullapps.io" + "api-private.playground.nullapps.io", + "idp.poc.nullapps.io" ] }, "privateDomain": { @@ -240,7 +241,8 @@ "update" ], "enum": [ - "api-private.playground.nullapps.io" + "api-private.playground.nullapps.io", + "idp.poc.nullapps.io" ] } } From 982dcc7d1bb0dd7d862cd1647e96bd9ef8b04cc0 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 15:32:23 -0300 Subject: [PATCH 047/101] revert(endpoint-exposer): remove idp.poc.nullapps.io from domain enum Old service deleted, backward compat no longer needed. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 0d5486b..b71d028 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -229,8 +229,7 @@ "update" ], "enum": [ - "api-private.playground.nullapps.io", - "idp.poc.nullapps.io" + "api-private.playground.nullapps.io" ] }, "privateDomain": { @@ -241,8 +240,7 @@ "update" ], "enum": [ - "api-private.playground.nullapps.io", - "idp.poc.nullapps.io" + "api-private.playground.nullapps.io" ] } } From 34196f1cf939b8710700e6e228a65ce76ddde03d Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 16:54:09 -0300 Subject: [PATCH 048/101] fix: remove output declarations from authz policy steps to unblock apply The workflow executor stops execution when a declared output file doesn't exist. The authz steps create a cleanup marker (.authz-*-deleted) instead of the YAML file when there are no routes, causing {} output and halting the workflow before the apply step runs. The apply script finds YAML files in OUTPUT_DIR directly, so these output declarations are unnecessary. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/workflows/istio/create.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index ba16ee8..cd72638 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -50,19 +50,11 @@ steps: file: "$SERVICE_PATH/scripts/istio/build_authz_policy" configuration: VISIBILITY: "public" - output: - - name: AUTHZ_PUBLIC_FILE - type: file - file: "$OUTPUT_DIR/authz-$SERVICE_ID-public.yaml" - name: "build private authz policy" type: script file: "$SERVICE_PATH/scripts/istio/build_authz_policy" configuration: VISIBILITY: "private" - output: - - name: AUTHZ_PRIVATE_FILE - type: file - file: "$OUTPUT_DIR/authz-$SERVICE_ID-private.yaml" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From 730ea9551066ae249dc164d09404c33e589d577c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 17:07:04 -0300 Subject: [PATCH 049/101] fix: export K8S_NAMESPACE from build_context so apply step can use it The apply script calls delete_if_exists with $K8S_NAMESPACE but the variable was missing from the workflow output declarations, making it unbound when the apply step ran. Without it, the step fails on the cleanup marker processing for private routes. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/workflows/istio/create.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index cd72638..ec7a006 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -27,6 +27,8 @@ steps: type: environment - name: PRIVATE_ROUTES_JSON type: environment + - name: K8S_NAMESPACE + type: environment - name: "build public httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_httproute" From 09af2988f6b24b3cafa0bfa34f3e2abcc4792018 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 17:23:08 -0300 Subject: [PATCH 050/101] feat: create AuthorizationPolicy per scope DNS instead of publicDomain The exposer now resolves each route's scope to its actual DNS hostname (scope.domain) and creates one AuthorizationPolicy per unique scope. This ensures the real endpoint the app is served on gets protected, not just the gateway entry point configured in publicDomain. - build_context: export APPLICATION_ID for scope resolution - build_authz_policy: fetch scopes via np CLI, group routes by scope, generate one policy per scope using scope.domain as the protected host - create.yaml: merge two visibility-based steps into one build_authz_policies step - apply: keep legacy authz-public/private cleanup for backward compat Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/common/apply | 4 +- .../scripts/istio/build_authz_policy | 133 +++++++++--------- endpoint-exposer/scripts/istio/build_context | 2 + endpoint-exposer/workflows/istio/create.yaml | 11 +- 4 files changed, 77 insertions(+), 73 deletions(-) diff --git a/endpoint-exposer/scripts/common/apply b/endpoint-exposer/scripts/common/apply index 1b092ff..e07acd1 100644 --- a/endpoint-exposer/scripts/common/apply +++ b/endpoint-exposer/scripts/common/apply @@ -37,13 +37,13 @@ if [[ -f "$OUTPUT_DIR/.httproute-private-deleted" ]]; then fi if [[ -f "$OUTPUT_DIR/.authz-public-deleted" ]]; then - echo "Public AuthorizationPolicy marked for deletion" + echo "Public AuthorizationPolicy marked for deletion (legacy)" delete_if_exists "authorizationpolicy" "$SERVICE_SLUG-$SERVICE_ID-authz-public" "$GATEWAY_NAMESPACE" rm "$OUTPUT_DIR/.authz-public-deleted" fi if [[ -f "$OUTPUT_DIR/.authz-private-deleted" ]]; then - echo "Private AuthorizationPolicy marked for deletion" + echo "Private AuthorizationPolicy marked for deletion (legacy)" delete_if_exists "authorizationpolicy" "$SERVICE_SLUG-$SERVICE_ID-authz-private" "$GATEWAY_NAMESPACE" rm "$OUTPUT_DIR/.authz-private-deleted" fi diff --git a/endpoint-exposer/scripts/istio/build_authz_policy b/endpoint-exposer/scripts/istio/build_authz_policy index 296d247..6b7ac99 100755 --- a/endpoint-exposer/scripts/istio/build_authz_policy +++ b/endpoint-exposer/scripts/istio/build_authz_policy @@ -4,74 +4,81 @@ set -euo pipefail source "$SERVICE_PATH/scripts/istio/config" -VISIBILITY="${VISIBILITY:-public}" AVP_PROVIDER_NAME="${AVP_PROVIDER_NAME:-avp-ext-authz}" -echo "=== Building ${VISIBILITY} AuthorizationPolicy ===" - -if [[ "$VISIBILITY" == "public" ]]; then - ROUTES_JSON="$PUBLIC_ROUTES_JSON" - DOMAIN="$PUBLIC_DOMAIN" - SUFFIX="public" -elif [[ "$VISIBILITY" == "private" ]]; then - ROUTES_JSON="$PRIVATE_ROUTES_JSON" - DOMAIN="$PRIVATE_DOMAIN" - SUFFIX="private" -else - echo "ERROR: Invalid VISIBILITY value: $VISIBILITY" - exit 1 -fi +echo "=== Building AuthorizationPolicies by scope ===" NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') -DOMAIN_EMPTY=false -if [[ -z "$DOMAIN" ]] || [[ "$DOMAIN" == "null" ]]; then - DOMAIN_EMPTY=true +if [[ "$NUM_ROUTES" -eq 0 ]]; then + echo "No routes configured, skipping AuthorizationPolicy generation" + exit 0 fi -if [[ "$NUM_ROUTES" -eq 0 ]] || [[ "$DOMAIN_EMPTY" == "true" ]]; then - echo "No $VISIBILITY routes or domain, marking authz policy for cleanup..." - touch "$OUTPUT_DIR/.authz-$SUFFIX-deleted" - exit 0 +if [[ -z "${APPLICATION_ID:-}" ]] || [[ "$APPLICATION_ID" == "null" ]]; then + echo "ERROR: APPLICATION_ID is not set" + exit 1 fi -# Deduplicate methods and collect paths from routes -PATHS_JSON=$(echo "$ROUTES_JSON" | jq '[.[].path] | unique') -METHODS_JSON=$(echo "$ROUTES_JSON" | jq '[.[].method] | unique') - -echo "$VISIBILITY Domain: $DOMAIN" -echo "Paths: $PATHS_JSON" -echo "Methods: $METHODS_JSON" - -AUTHZ_CONTEXT=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg domain "$DOMAIN" \ - --arg gateway_namespace "$GATEWAY_NAMESPACE" \ - --arg suffix "$SUFFIX" \ - --arg provider_name "$AVP_PROVIDER_NAME" \ - --argjson paths "$PATHS_JSON" \ - --argjson methods "$METHODS_JSON" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - domain: $domain, - gateway_namespace: $gateway_namespace, - suffix: $suffix, - provider_name: $provider_name, - paths: $paths, - methods: $methods - }') - -CONTEXT_PATH="$OUTPUT_DIR/authz-$SUFFIX-context-$SERVICE_ID.json" -AUTHZ_FILE="$OUTPUT_DIR/authz-$SERVICE_ID-$SUFFIX.yaml" - -echo "$AUTHZ_CONTEXT" > "$CONTEXT_PATH" - -gomplate -c .="$CONTEXT_PATH" \ - -f "$SERVICE_PATH/templates/istio/authz-policy.yaml.tpl" \ - -o "$AUTHZ_FILE" - -rm "$CONTEXT_PATH" - -echo "✅ $VISIBILITY AuthorizationPolicy created: $AUTHZ_FILE" -export AUTHZ_FILE +echo "Fetching scopes for application $APPLICATION_ID..." +SCOPES_JSON=$(np scope list --application-id "$APPLICATION_ID" --format json | jq '.results // []') +echo "Scopes fetched: $(echo "$SCOPES_JSON" | jq 'length')" + +SCOPE_SLUGS=$(echo "$ROUTES_JSON" | jq -r '[.[].scope] | unique | .[]') + +while IFS= read -r SCOPE_SLUG; do + echo "--- Processing scope: $SCOPE_SLUG ---" + + SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE_SLUG" '.[] | select(.name == $name) | .id') + if [[ -z "$SCOPE_ID" ]] || [[ "$SCOPE_ID" == "null" ]]; then + echo "WARNING: Could not resolve scope '$SCOPE_SLUG', skipping" + continue + fi + + SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') + if [[ -z "$SCOPE_DOMAIN" ]] || [[ "$SCOPE_DOMAIN" == "null" ]]; then + echo "WARNING: Scope '$SCOPE_SLUG' (id: $SCOPE_ID) has no domain, skipping" + continue + fi + + echo "Scope domain: $SCOPE_DOMAIN" + + SCOPE_ROUTES=$(echo "$ROUTES_JSON" | jq -c --arg slug "$SCOPE_SLUG" '[.[] | select(.scope == $slug)]') + PATHS_JSON=$(echo "$SCOPE_ROUTES" | jq '[.[].path] | unique') + METHODS_JSON=$(echo "$SCOPE_ROUTES" | jq '[.[].method] | unique') + + echo "Paths: $PATHS_JSON" + echo "Methods: $METHODS_JSON" + + AUTHZ_CONTEXT=$(jq -n \ + --arg service_slug "$SERVICE_SLUG" \ + --arg service_id "$SERVICE_ID" \ + --arg domain "$SCOPE_DOMAIN" \ + --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg suffix "scope-$SCOPE_ID" \ + --arg provider_name "$AVP_PROVIDER_NAME" \ + --argjson paths "$PATHS_JSON" \ + --argjson methods "$METHODS_JSON" \ + '{ + service_slug: $service_slug, + service_id: $service_id, + domain: $domain, + gateway_namespace: $gateway_namespace, + suffix: $suffix, + provider_name: $provider_name, + paths: $paths, + methods: $methods + }') + + AUTHZ_FILE="$OUTPUT_DIR/authz-$SERVICE_ID-scope-$SCOPE_ID.yaml" + CONTEXT_PATH="$OUTPUT_DIR/authz-scope-$SCOPE_ID-context.json" + + echo "$AUTHZ_CONTEXT" > "$CONTEXT_PATH" + + gomplate -c .="$CONTEXT_PATH" \ + -f "$SERVICE_PATH/templates/istio/authz-policy.yaml.tpl" \ + -o "$AUTHZ_FILE" + + rm "$CONTEXT_PATH" + + echo "✅ AuthorizationPolicy created for scope '$SCOPE_SLUG' (domain: $SCOPE_DOMAIN): $AUTHZ_FILE" +done <<< "$SCOPE_SLUGS" diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index f414861..f2f2607 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -13,6 +13,7 @@ PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.publicDomain // .parameters PRIVATE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.privateDomain // .parameters.private_domain // ""') K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" +APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') # Extract routes array from parameters ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') @@ -36,6 +37,7 @@ export SERVICE_ID export SERVICE_SLUG export ACTION_ID export ACTION_NAME +export APPLICATION_ID export PUBLIC_DOMAIN export PRIVATE_DOMAIN export ROUTES_JSON diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index ec7a006..9eb34d7 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -29,6 +29,8 @@ steps: type: environment - name: K8S_NAMESPACE type: environment + - name: APPLICATION_ID + type: environment - name: "build public httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_httproute" @@ -47,16 +49,9 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "build public authz policy" + - name: "build authz policies" type: script file: "$SERVICE_PATH/scripts/istio/build_authz_policy" - configuration: - VISIBILITY: "public" - - name: "build private authz policy" - type: script - file: "$SERVICE_PATH/scripts/istio/build_authz_policy" - configuration: - VISIBILITY: "private" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" From 59b8c170a6a2ed0096a19283bd969b8f2f6be6b5 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 17:28:12 -0300 Subject: [PATCH 051/101] fix: read routes and application_id from CONTEXT directly in build_authz_policy Complex JSON env vars (ROUTES_JSON) may not survive workflow step boundaries intact. Re-reading from \$CONTEXT (always available as the raw action context) ensures the data is correct regardless of how the workflow executor serialises environment outputs between steps. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/istio/build_authz_policy | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_authz_policy b/endpoint-exposer/scripts/istio/build_authz_policy index 6b7ac99..aef44f5 100755 --- a/endpoint-exposer/scripts/istio/build_authz_policy +++ b/endpoint-exposer/scripts/istio/build_authz_policy @@ -8,14 +8,18 @@ AVP_PROVIDER_NAME="${AVP_PROVIDER_NAME:-avp-ext-authz}" echo "=== Building AuthorizationPolicies by scope ===" +# Re-read from context directly — complex JSON env vars may not survive workflow step boundaries +ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') +APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') + NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') if [[ "$NUM_ROUTES" -eq 0 ]]; then echo "No routes configured, skipping AuthorizationPolicy generation" exit 0 fi -if [[ -z "${APPLICATION_ID:-}" ]] || [[ "$APPLICATION_ID" == "null" ]]; then - echo "ERROR: APPLICATION_ID is not set" +if [[ -z "$APPLICATION_ID" ]] || [[ "$APPLICATION_ID" == "null" ]]; then + echo "ERROR: APPLICATION_ID not found in context" exit 1 fi From b5732acf5006275e7700d3073ef5a01d2cd459a1 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 17:30:32 -0300 Subject: [PATCH 052/101] fix: use --application_id (underscore) flag for np scope list The np CLI uses underscore-style flags for this command, not hyphen. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/istio/build_authz_policy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/istio/build_authz_policy b/endpoint-exposer/scripts/istio/build_authz_policy index aef44f5..b71554d 100755 --- a/endpoint-exposer/scripts/istio/build_authz_policy +++ b/endpoint-exposer/scripts/istio/build_authz_policy @@ -24,7 +24,7 @@ if [[ -z "$APPLICATION_ID" ]] || [[ "$APPLICATION_ID" == "null" ]]; then fi echo "Fetching scopes for application $APPLICATION_ID..." -SCOPES_JSON=$(np scope list --application-id "$APPLICATION_ID" --format json | jq '.results // []') +SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') echo "Scopes fetched: $(echo "$SCOPES_JSON" | jq 'length')" SCOPE_SLUGS=$(echo "$ROUTES_JSON" | jq -r '[.[].scope] | unique | .[]') From b3c9d77615e246d24382f86cde5444fbaeabac66 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 17:35:51 -0300 Subject: [PATCH 053/101] fix: use correct gateway-name selector in AuthorizationPolicy Gateway selector was using the suffix value ('gateway-scope-') instead of the actual gateway name ('gateway-public'), causing the policy to match no workloads. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/istio/build_authz_policy | 2 ++ endpoint-exposer/templates/istio/authz-policy.yaml.tpl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/istio/build_authz_policy b/endpoint-exposer/scripts/istio/build_authz_policy index b71554d..572367a 100755 --- a/endpoint-exposer/scripts/istio/build_authz_policy +++ b/endpoint-exposer/scripts/istio/build_authz_policy @@ -58,6 +58,7 @@ while IFS= read -r SCOPE_SLUG; do --arg service_id "$SERVICE_ID" \ --arg domain "$SCOPE_DOMAIN" \ --arg gateway_namespace "$GATEWAY_NAMESPACE" \ + --arg gateway_name "$PUBLIC_GATEWAY_NAME" \ --arg suffix "scope-$SCOPE_ID" \ --arg provider_name "$AVP_PROVIDER_NAME" \ --argjson paths "$PATHS_JSON" \ @@ -67,6 +68,7 @@ while IFS= read -r SCOPE_SLUG; do service_id: $service_id, domain: $domain, gateway_namespace: $gateway_namespace, + gateway_name: $gateway_name, suffix: $suffix, provider_name: $provider_name, paths: $paths, diff --git a/endpoint-exposer/templates/istio/authz-policy.yaml.tpl b/endpoint-exposer/templates/istio/authz-policy.yaml.tpl index 5b09fe5..97f0990 100644 --- a/endpoint-exposer/templates/istio/authz-policy.yaml.tpl +++ b/endpoint-exposer/templates/istio/authz-policy.yaml.tpl @@ -10,7 +10,7 @@ metadata: spec: selector: matchLabels: - gateway.networking.k8s.io/gateway-name: gateway-{{ .suffix }} + gateway.networking.k8s.io/gateway-name: {{ .gateway_name }} action: CUSTOM provider: name: {{ .provider_name }} From 4a75a1ab87a7894f51179c46ae32f9a389c95531 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 17:55:46 -0300 Subject: [PATCH 054/101] feat: manage AVP Cedar policies from endpoint-exposer The exposer is now the single source of truth for Cedar policies in AVP: - create: generates one policy per route based on path/method/groups/scope-domain - delete: removes all policies tagged with the service-id in their description - update: sync (delete + recreate) so stale policies never remain Routes with empty groups allow any authenticated user; routes with groups restrict access to those principals via Cedar `when` conditions. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/config | 4 + endpoint-exposer/scripts/avp/create_policies | 84 ++++++++++++++++++++ endpoint-exposer/scripts/avp/delete_policies | 29 +++++++ endpoint-exposer/scripts/avp/sync_policies | 7 ++ endpoint-exposer/workflows/istio/create.yaml | 3 + endpoint-exposer/workflows/istio/delete.yaml | 3 + endpoint-exposer/workflows/istio/update.yaml | 69 +--------------- 7 files changed, 134 insertions(+), 65 deletions(-) create mode 100755 endpoint-exposer/scripts/avp/config create mode 100755 endpoint-exposer/scripts/avp/create_policies create mode 100755 endpoint-exposer/scripts/avp/delete_policies create mode 100755 endpoint-exposer/scripts/avp/sync_policies diff --git a/endpoint-exposer/scripts/avp/config b/endpoint-exposer/scripts/avp/config new file mode 100755 index 0000000..adba3f7 --- /dev/null +++ b/endpoint-exposer/scripts/avp/config @@ -0,0 +1,4 @@ +#!/bin/bash + +export AVP_POLICY_STORE_ID="${AVP_POLICY_STORE_ID:-RPkfnZxHxnWktjL9KF2qz5}" +export AVP_AWS_REGION="${AVP_AWS_REGION:-us-east-1}" diff --git a/endpoint-exposer/scripts/avp/create_policies b/endpoint-exposer/scripts/avp/create_policies new file mode 100755 index 0000000..45077dc --- /dev/null +++ b/endpoint-exposer/scripts/avp/create_policies @@ -0,0 +1,84 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +echo "=== Creating AVP Cedar Policies ===" + +ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') +APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') + +NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') +if [[ "$NUM_ROUTES" -eq 0 ]]; then + echo "No routes configured, skipping AVP policy creation" + exit 0 +fi + +if [[ -z "$APPLICATION_ID" ]] || [[ "$APPLICATION_ID" == "null" ]]; then + echo "ERROR: APPLICATION_ID not found in context" + exit 1 +fi + +SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') + +for i in $(seq 0 $((NUM_ROUTES - 1))); do + ROUTE=$(echo "$ROUTES_JSON" | jq -c ".[$i]") + METHOD=$(echo "$ROUTE" | jq -r '.method') + ROUTE_PATH=$(echo "$ROUTE" | jq -r '.path') + SCOPE_SLUG=$(echo "$ROUTE" | jq -r '.scope') + GROUPS_JSON=$(echo "$ROUTE" | jq -c '.groups // []') + NUM_GROUPS=$(echo "$GROUPS_JSON" | jq 'length') + + SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE_SLUG" '.[] | select(.name == $name) | .id') + if [[ -z "$SCOPE_ID" ]] || [[ "$SCOPE_ID" == "null" ]]; then + echo "WARNING: Could not resolve scope '$SCOPE_SLUG', skipping route $METHOD $ROUTE_PATH" + continue + fi + + SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') + if [[ -z "$SCOPE_DOMAIN" ]] || [[ "$SCOPE_DOMAIN" == "null" ]]; then + echo "WARNING: Scope '$SCOPE_SLUG' (id: $SCOPE_ID) has no domain, skipping route $METHOD $ROUTE_PATH" + continue + fi + + if [[ "$NUM_GROUPS" -gt 0 ]]; then + GROUP_CONDITIONS=$(echo "$GROUPS_JSON" | jq -r \ + 'map("principal in ApiAccess::Group::\"" + . + "\"") | join(" ||\n ")') + CEDAR_STATEMENT="permit ( + principal, + action == ApiAccess::Action::\"$METHOD\", + resource +) +when { + ($GROUP_CONDITIONS) && + resource.host == \"$SCOPE_DOMAIN\" && + resource.path == \"$ROUTE_PATH\" +};" + else + CEDAR_STATEMENT="permit ( + principal, + action == ApiAccess::Action::\"$METHOD\", + resource +) +when { + resource.host == \"$SCOPE_DOMAIN\" && + resource.path == \"$ROUTE_PATH\" +};" + fi + + DESCRIPTION="endpoint-exposer|service-id=$SERVICE_ID|route=$METHOD:$ROUTE_PATH|scope=$SCOPE_ID" + + DEFINITION=$(jq -n \ + --arg desc "$DESCRIPTION" \ + --arg stmt "$CEDAR_STATEMENT" \ + '{"static": {"description": $desc, "statement": $stmt}}') + + POLICY_ID=$(aws verifiedpermissions create-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --definition "$DEFINITION" \ + --query 'policyId' --output text) + + echo "✅ Cedar policy created: $METHOD $ROUTE_PATH @ $SCOPE_DOMAIN (groups: $NUM_GROUPS, policy-id: $POLICY_ID)" +done diff --git a/endpoint-exposer/scripts/avp/delete_policies b/endpoint-exposer/scripts/avp/delete_policies new file mode 100755 index 0000000..b70c9ce --- /dev/null +++ b/endpoint-exposer/scripts/avp/delete_policies @@ -0,0 +1,29 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +echo "=== Deleting AVP Cedar Policies for service $SERVICE_ID ===" + +DESCRIPTION_PREFIX="endpoint-exposer|service-id=$SERVICE_ID" + +POLICY_IDS=$(aws verifiedpermissions list-policies \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --output json | jq -r --arg prefix "$DESCRIPTION_PREFIX" \ + '[.policies[] | select((.definition.static.description // "") | startswith($prefix)) | .policyId] | .[]') + +if [[ -z "$POLICY_IDS" ]]; then + echo "No AVP policies found for service $SERVICE_ID" + exit 0 +fi + +while IFS= read -r POLICY_ID; do + echo "Deleting policy: $POLICY_ID" + aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" + echo "✅ Deleted policy $POLICY_ID" +done <<< "$POLICY_IDS" diff --git a/endpoint-exposer/scripts/avp/sync_policies b/endpoint-exposer/scripts/avp/sync_policies new file mode 100755 index 0000000..9f47b0c --- /dev/null +++ b/endpoint-exposer/scripts/avp/sync_policies @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +# Delete existing policies then recreate from current routes +"$SERVICE_PATH/scripts/avp/delete_policies" +"$SERVICE_PATH/scripts/avp/create_policies" diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 9eb34d7..20709b4 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -52,6 +52,9 @@ steps: - name: "build authz policies" type: script file: "$SERVICE_PATH/scripts/istio/build_authz_policy" + - name: "manage avp policies" + type: script + file: "$SERVICE_PATH/scripts/avp/create_policies" - name: apply type: script file: "$SERVICE_PATH/scripts/common/apply" diff --git a/endpoint-exposer/workflows/istio/delete.yaml b/endpoint-exposer/workflows/istio/delete.yaml index 1f4d320..31a1348 100644 --- a/endpoint-exposer/workflows/istio/delete.yaml +++ b/endpoint-exposer/workflows/istio/delete.yaml @@ -1,6 +1,9 @@ include: - "$SERVICE_PATH/workflows/istio/create.yaml" steps: + - name: "manage avp policies" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_policies" - name: apply configuration: ACTION: delete \ No newline at end of file diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index ba16ee8..68e0dde 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -1,71 +1,10 @@ +include: + - "$SERVICE_PATH/workflows/istio/create.yaml" steps: - - name: "find k8s namespace" + - name: "manage avp policies" type: script - file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" - output: - - name: K8S_NAMESPACE - type: environment - - name: "build context" - type: script - file: "$SERVICE_PATH/scripts/istio/build_context" - output: - - name: SERVICE_ID - type: environment - - name: SERVICE_SLUG - type: environment - - name: ACTION_ID - type: environment - - name: ACTION_NAME - type: environment - - name: PUBLIC_DOMAIN - type: environment - - name: PRIVATE_DOMAIN - type: environment - - name: ROUTES_JSON - type: environment - - name: PUBLIC_ROUTES_JSON - type: environment - - name: PRIVATE_ROUTES_JSON - type: environment - - name: "build public httproute" - type: script - file: "$SERVICE_PATH/scripts/istio/build_httproute" - configuration: - VISIBILITY: "public" - output: - - name: HTTPROUTE_PUBLIC_FILE - type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" - - name: "build private httproute" - type: script - file: "$SERVICE_PATH/scripts/istio/build_httproute" - configuration: - VISIBILITY: "private" - output: - - name: HTTPROUTE_PRIVATE_FILE - type: file - file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "build public authz policy" - type: script - file: "$SERVICE_PATH/scripts/istio/build_authz_policy" - configuration: - VISIBILITY: "public" - output: - - name: AUTHZ_PUBLIC_FILE - type: file - file: "$OUTPUT_DIR/authz-$SERVICE_ID-public.yaml" - - name: "build private authz policy" - type: script - file: "$SERVICE_PATH/scripts/istio/build_authz_policy" - configuration: - VISIBILITY: "private" - output: - - name: AUTHZ_PRIVATE_FILE - type: file - file: "$OUTPUT_DIR/authz-$SERVICE_ID-private.yaml" + file: "$SERVICE_PATH/scripts/avp/sync_policies" - name: apply - type: script - file: "$SERVICE_PATH/scripts/common/apply" configuration: ACTION: apply DRY_RUN: false From 4f1b38773aaa88212894d1f839a02260ecddc84c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Thu, 21 May 2026 18:12:00 -0300 Subject: [PATCH 055/101] fix: add has operator for optional host attr and explicit error checking In STRICT mode, AVP rejects direct access to optional schema attributes. `host` is marked required:false so Cedar must use `resource has host &&` before `resource.host == "..."`. Also fail loudly if the policy creation returns no policyId instead of silently continuing with an empty ID. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/create_policies | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/avp/create_policies b/endpoint-exposer/scripts/avp/create_policies index 45077dc..120c15c 100755 --- a/endpoint-exposer/scripts/avp/create_policies +++ b/endpoint-exposer/scripts/avp/create_policies @@ -52,6 +52,7 @@ for i in $(seq 0 $((NUM_ROUTES - 1))); do ) when { ($GROUP_CONDITIONS) && + resource has host && resource.host == \"$SCOPE_DOMAIN\" && resource.path == \"$ROUTE_PATH\" };" @@ -62,6 +63,7 @@ when { resource ) when { + resource has host && resource.host == \"$SCOPE_DOMAIN\" && resource.path == \"$ROUTE_PATH\" };" @@ -78,7 +80,12 @@ when { --policy-store-id "$AVP_POLICY_STORE_ID" \ --region "$AVP_AWS_REGION" \ --definition "$DEFINITION" \ - --query 'policyId' --output text) + --query 'policyId' --output text) || { echo "ERROR: Failed to create Cedar policy for $METHOD $ROUTE_PATH"; exit 1; } + + if [[ -z "$POLICY_ID" ]] || [[ "$POLICY_ID" == "None" ]]; then + echo "ERROR: Policy creation returned no policyId for $METHOD $ROUTE_PATH" + exit 1 + fi echo "✅ Cedar policy created: $METHOD $ROUTE_PATH @ $SCOPE_DOMAIN (groups: $NUM_GROUPS, policy-id: $POLICY_ID)" done From 62f3c7683e2b76562f90ca14eb6ae7291d8cc894 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 10:45:46 -0300 Subject: [PATCH 056/101] feat(endpoint-exposer): switch to global catch-all AuthorizationPolicy Exposer now only manages Cedar policies in AVP. Per-service Istio AuthorizationPolicies are replaced by a single Terraform-managed CUSTOM policy on gateway-public that sends all traffic through AVP. - Remove build_authz_policy script and authz-policy.yaml.tpl template - Remove 'build authz policies' step from create workflow - delete_policies now also cleans up legacy per-service AuthorizationPolicies by service-id label (migration path for existing deployments) Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/delete_policies | 11 ++- .../scripts/istio/build_authz_policy | 90 ------------------- .../templates/istio/authz-policy.yaml.tpl | 26 ------ endpoint-exposer/workflows/istio/create.yaml | 3 - 4 files changed, 9 insertions(+), 121 deletions(-) delete mode 100755 endpoint-exposer/scripts/istio/build_authz_policy delete mode 100644 endpoint-exposer/templates/istio/authz-policy.yaml.tpl diff --git a/endpoint-exposer/scripts/avp/delete_policies b/endpoint-exposer/scripts/avp/delete_policies index b70c9ce..64cd76f 100755 --- a/endpoint-exposer/scripts/avp/delete_policies +++ b/endpoint-exposer/scripts/avp/delete_policies @@ -3,6 +3,7 @@ set -euo pipefail source "$SERVICE_PATH/scripts/avp/config" +source "$SERVICE_PATH/scripts/istio/config" echo "=== Deleting AVP Cedar Policies for service $SERVICE_ID ===" @@ -20,10 +21,16 @@ if [[ -z "$POLICY_IDS" ]]; then fi while IFS= read -r POLICY_ID; do - echo "Deleting policy: $POLICY_ID" + echo "Deleting Cedar policy: $POLICY_ID" aws verifiedpermissions delete-policy \ --policy-store-id "$AVP_POLICY_STORE_ID" \ --region "$AVP_AWS_REGION" \ --policy-id "$POLICY_ID" - echo "✅ Deleted policy $POLICY_ID" + echo "✅ Deleted Cedar policy $POLICY_ID" done <<< "$POLICY_IDS" + +echo "Cleaning up per-service AuthorizationPolicies for service $SERVICE_ID..." +kubectl delete authorizationpolicy \ + -n "$GATEWAY_NAMESPACE" \ + -l "nullplatform.com/service-id=$SERVICE_ID" \ + --ignore-not-found 2>/dev/null && echo "✅ AuthorizationPolicies cleaned up" diff --git a/endpoint-exposer/scripts/istio/build_authz_policy b/endpoint-exposer/scripts/istio/build_authz_policy deleted file mode 100755 index 572367a..0000000 --- a/endpoint-exposer/scripts/istio/build_authz_policy +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -source "$SERVICE_PATH/scripts/istio/config" - -AVP_PROVIDER_NAME="${AVP_PROVIDER_NAME:-avp-ext-authz}" - -echo "=== Building AuthorizationPolicies by scope ===" - -# Re-read from context directly — complex JSON env vars may not survive workflow step boundaries -ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') -APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') - -NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') -if [[ "$NUM_ROUTES" -eq 0 ]]; then - echo "No routes configured, skipping AuthorizationPolicy generation" - exit 0 -fi - -if [[ -z "$APPLICATION_ID" ]] || [[ "$APPLICATION_ID" == "null" ]]; then - echo "ERROR: APPLICATION_ID not found in context" - exit 1 -fi - -echo "Fetching scopes for application $APPLICATION_ID..." -SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') -echo "Scopes fetched: $(echo "$SCOPES_JSON" | jq 'length')" - -SCOPE_SLUGS=$(echo "$ROUTES_JSON" | jq -r '[.[].scope] | unique | .[]') - -while IFS= read -r SCOPE_SLUG; do - echo "--- Processing scope: $SCOPE_SLUG ---" - - SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE_SLUG" '.[] | select(.name == $name) | .id') - if [[ -z "$SCOPE_ID" ]] || [[ "$SCOPE_ID" == "null" ]]; then - echo "WARNING: Could not resolve scope '$SCOPE_SLUG', skipping" - continue - fi - - SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') - if [[ -z "$SCOPE_DOMAIN" ]] || [[ "$SCOPE_DOMAIN" == "null" ]]; then - echo "WARNING: Scope '$SCOPE_SLUG' (id: $SCOPE_ID) has no domain, skipping" - continue - fi - - echo "Scope domain: $SCOPE_DOMAIN" - - SCOPE_ROUTES=$(echo "$ROUTES_JSON" | jq -c --arg slug "$SCOPE_SLUG" '[.[] | select(.scope == $slug)]') - PATHS_JSON=$(echo "$SCOPE_ROUTES" | jq '[.[].path] | unique') - METHODS_JSON=$(echo "$SCOPE_ROUTES" | jq '[.[].method] | unique') - - echo "Paths: $PATHS_JSON" - echo "Methods: $METHODS_JSON" - - AUTHZ_CONTEXT=$(jq -n \ - --arg service_slug "$SERVICE_SLUG" \ - --arg service_id "$SERVICE_ID" \ - --arg domain "$SCOPE_DOMAIN" \ - --arg gateway_namespace "$GATEWAY_NAMESPACE" \ - --arg gateway_name "$PUBLIC_GATEWAY_NAME" \ - --arg suffix "scope-$SCOPE_ID" \ - --arg provider_name "$AVP_PROVIDER_NAME" \ - --argjson paths "$PATHS_JSON" \ - --argjson methods "$METHODS_JSON" \ - '{ - service_slug: $service_slug, - service_id: $service_id, - domain: $domain, - gateway_namespace: $gateway_namespace, - gateway_name: $gateway_name, - suffix: $suffix, - provider_name: $provider_name, - paths: $paths, - methods: $methods - }') - - AUTHZ_FILE="$OUTPUT_DIR/authz-$SERVICE_ID-scope-$SCOPE_ID.yaml" - CONTEXT_PATH="$OUTPUT_DIR/authz-scope-$SCOPE_ID-context.json" - - echo "$AUTHZ_CONTEXT" > "$CONTEXT_PATH" - - gomplate -c .="$CONTEXT_PATH" \ - -f "$SERVICE_PATH/templates/istio/authz-policy.yaml.tpl" \ - -o "$AUTHZ_FILE" - - rm "$CONTEXT_PATH" - - echo "✅ AuthorizationPolicy created for scope '$SCOPE_SLUG' (domain: $SCOPE_DOMAIN): $AUTHZ_FILE" -done <<< "$SCOPE_SLUGS" diff --git a/endpoint-exposer/templates/istio/authz-policy.yaml.tpl b/endpoint-exposer/templates/istio/authz-policy.yaml.tpl deleted file mode 100644 index 97f0990..0000000 --- a/endpoint-exposer/templates/istio/authz-policy.yaml.tpl +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: security.istio.io/v1 -kind: AuthorizationPolicy -metadata: - name: {{ .service_slug }}-{{ .service_id }}-authz-{{ .suffix }} - namespace: {{ .gateway_namespace }} - labels: - app.kubernetes.io/name: {{ .service_slug }} - nullplatform.com/service-id: "{{ .service_id }}" - nullplatform.com/managed-by: endpoint-exposer -spec: - selector: - matchLabels: - gateway.networking.k8s.io/gateway-name: {{ .gateway_name }} - action: CUSTOM - provider: - name: {{ .provider_name }} - rules: - - to: - - operation: - hosts: - - {{ .domain }} - methods: -{{ range .methods }} - {{ . }} -{{ end }} paths: -{{ range .paths }} - {{ . }} -{{ end }} diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 20709b4..0ea50a6 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -49,9 +49,6 @@ steps: - name: HTTPROUTE_PRIVATE_FILE type: file file: "$OUTPUT_DIR/httproute-$SERVICE_ID-private.yaml" - - name: "build authz policies" - type: script - file: "$SERVICE_PATH/scripts/istio/build_authz_policy" - name: "manage avp policies" type: script file: "$SERVICE_PATH/scripts/avp/create_policies" From 7567d6c4cdb84ba9d13c3920ed1befb4382275a6 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 12:08:20 -0300 Subject: [PATCH 057/101] refactor(endpoint-exposer): derive HTTPRoute hostnames from scope domains Remove publicDomain/privateDomain manual configuration fields. The HTTPRoute hostname(s) are now resolved automatically from the scope domain of each route, making scope the single source of truth for both Cedar policies and HTTPRoute routing. - Remove publicDomain/privateDomain from service spec (required + properties + UI) - Remove PUBLIC_DOMAIN/PRIVATE_DOMAIN from build_context - build_httproute now looks up scope domains via np scope API - HTTPRoute template supports multiple hostnames (one per unique scope) - Remove domain outputs from create workflow Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/istio/build_context | 6 --- .../scripts/istio/build_httproute | 41 ++++++++++++------- endpoint-exposer/specs/service-spec.json.tpl | 40 +----------------- .../templates/istio/httproute.yaml.tpl | 3 +- endpoint-exposer/workflows/istio/create.yaml | 4 -- 5 files changed, 29 insertions(+), 65 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_context b/endpoint-exposer/scripts/istio/build_context index f2f2607..883d070 100644 --- a/endpoint-exposer/scripts/istio/build_context +++ b/endpoint-exposer/scripts/istio/build_context @@ -8,10 +8,6 @@ SERVICE_SLUG=$(echo "$CONTEXT" | jq -r .service.slug) ACTION_ID=$(echo "$CONTEXT" | jq -r .id) ACTION_NAME=$(echo "$CONTEXT" | jq -r .slug) -# Extract domains from parameters -PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.publicDomain // .parameters.public_domain // ""') -PRIVATE_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.privateDomain // .parameters.private_domain // ""') - K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') @@ -38,8 +34,6 @@ export SERVICE_SLUG export ACTION_ID export ACTION_NAME export APPLICATION_ID -export PUBLIC_DOMAIN -export PRIVATE_DOMAIN export ROUTES_JSON export PUBLIC_ROUTES_JSON export PRIVATE_ROUTES_JSON \ No newline at end of file diff --git a/endpoint-exposer/scripts/istio/build_httproute b/endpoint-exposer/scripts/istio/build_httproute index 64162f4..e0709eb 100755 --- a/endpoint-exposer/scripts/istio/build_httproute +++ b/endpoint-exposer/scripts/istio/build_httproute @@ -13,12 +13,10 @@ echo "=== Building ${VISIBILITY} HTTPRoute ===" # Set variables based on visibility if [[ "$VISIBILITY" == "public" ]]; then ROUTES_JSON="$PUBLIC_ROUTES_JSON" - DOMAIN="$PUBLIC_DOMAIN" GATEWAY_NAME="$PUBLIC_GATEWAY_NAME" SUFFIX="public" elif [[ "$VISIBILITY" == "private" ]]; then ROUTES_JSON="$PRIVATE_ROUTES_JSON" - DOMAIN="$PRIVATE_DOMAIN" GATEWAY_NAME="$PRIVATE_GATEWAY_NAME" SUFFIX="private" else @@ -26,22 +24,35 @@ else exit 1 fi -# Check if we have routes and domain NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') echo "Number of $VISIBILITY routes: $NUM_ROUTES" -echo "$VISIBILITY Domain: '$DOMAIN'" -# Check if domain is empty/null -DOMAIN_EMPTY=false -if [[ -z "$DOMAIN" ]] || [[ "$DOMAIN" == "null" ]] || [[ "$DOMAIN" == "\"null\"" ]]; then - DOMAIN_EMPTY=true -fi - -# Determine if we should create an empty resource +# Derive unique hostnames from scope domains CREATE_EMPTY=false -if [[ "$NUM_ROUTES" -eq 0 ]] || [[ "$DOMAIN_EMPTY" == "true" ]]; then +HOSTNAMES_JSON="[]" + +if [[ "$NUM_ROUTES" -eq 0 ]]; then CREATE_EMPTY=true - echo "No $VISIBILITY routes or domain configured, creating empty resource for cleanup..." + echo "No $VISIBILITY routes configured, creating empty resource for cleanup..." +else + source "$SERVICE_PATH/scripts/avp/config" + SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') + + SCOPE_SLUGS=$(echo "$ROUTES_JSON" | jq -r '[.[].scope] | unique | .[]') + while IFS= read -r SCOPE_SLUG; do + SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE_SLUG" '.[] | select(.name == $name) | .id') + [[ -z "$SCOPE_ID" || "$SCOPE_ID" == "null" ]] && continue + SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') + [[ -z "$SCOPE_DOMAIN" || "$SCOPE_DOMAIN" == "null" ]] && continue + HOSTNAMES_JSON=$(echo "$HOSTNAMES_JSON" | jq --arg d "$SCOPE_DOMAIN" '. + [$d] | unique') + done <<< "$SCOPE_SLUGS" + + if [[ "$(echo "$HOSTNAMES_JSON" | jq 'length')" -eq 0 ]]; then + CREATE_EMPTY=true + echo "No scope domains resolved for $VISIBILITY routes, creating empty resource for cleanup..." + else + echo "$VISIBILITY domains: $(echo "$HOSTNAMES_JSON" | jq -r '.[]' | tr '\n' ' ')" + fi fi # Create HTTPRoute @@ -61,7 +72,7 @@ else --arg service_slug "$SERVICE_SLUG" \ --arg service_id "$SERVICE_ID" \ --arg k8s_namespace "$K8S_NAMESPACE" \ - --arg domain "$DOMAIN" \ + --argjson hostnames "$HOSTNAMES_JSON" \ --arg gateway_name "$GATEWAY_NAME" \ --arg gateway_namespace "$GATEWAY_NAMESPACE" \ --arg suffix "$SUFFIX" \ @@ -69,7 +80,7 @@ else service_slug: $service_slug, service_id: $service_id, k8s_namespace: $k8s_namespace, - domain: $domain, + hostnames: $hostnames, gateway_name: $gateway_name, gateway_namespace: $gateway_namespace, suffix: $suffix diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index b71d028..09b9d02 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -4,9 +4,7 @@ "schema": { "type": "object", "$schema": "http://json-schema.org/draft-07/schema#", - "required": [ - "publicDomain" - ], + "required": [], "uiSchema": { "type": "VerticalLayout", "elements": [ @@ -60,20 +58,6 @@ } ] }, - { - "type": "Group", - "label": "Domains", - "elements": [ - { - "type": "Control", - "scope": "#/properties/publicDomain" - }, - { - "type": "Control", - "scope": "#/properties/privateDomain" - } - ] - }, { "type": "Group", "label": "Authorization", @@ -221,28 +205,6 @@ } } }, - "publicDomain": { - "type": "string", - "title": "Public Domain", - "editableOn": [ - "create", - "update" - ], - "enum": [ - "api-private.playground.nullapps.io" - ] - }, - "privateDomain": { - "type": "string", - "title": "Private Domain", - "editableOn": [ - "create", - "update" - ], - "enum": [ - "api-private.playground.nullapps.io" - ] - } } }, "values": {} diff --git a/endpoint-exposer/templates/istio/httproute.yaml.tpl b/endpoint-exposer/templates/istio/httproute.yaml.tpl index 728c0aa..ce8b7f8 100644 --- a/endpoint-exposer/templates/istio/httproute.yaml.tpl +++ b/endpoint-exposer/templates/istio/httproute.yaml.tpl @@ -14,7 +14,8 @@ spec: group: gateway.networking.k8s.io kind: Gateway hostnames: - - {{ .domain }} +{{ range .hostnames }} - {{ . }} +{{ end }} rules: - matches: - path: diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 0ea50a6..e808eb5 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -17,10 +17,6 @@ steps: type: environment - name: ACTION_NAME type: environment - - name: PUBLIC_DOMAIN - type: environment - - name: PRIVATE_DOMAIN - type: environment - name: ROUTES_JSON type: environment - name: PUBLIC_ROUTES_JSON From bca8b369386ac228037379805c0cebff26850f78 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 12:10:45 -0300 Subject: [PATCH 058/101] fix(endpoint-exposer): remove trailing comma in service-spec after domain fields removal Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 09b9d02..78860ea 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -204,7 +204,7 @@ } } } - }, + } } }, "values": {} From e4113ee8342256a72ce77e6d2dd4463d47649943 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 12:37:19 -0300 Subject: [PATCH 059/101] docs(endpoint-exposer): remove visibility and domain references from documentation Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 78860ea..8cc04eb 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -17,25 +17,12 @@ } }, "elements": [ - { - "type": "Category", - "label": "Domains", - "elements": [ - { - "text": "### Public Domain\nBase domain for routes exposed to external traffic. Requests matching routes with `visibility: public` will be served through this domain.\n\n### Private Domain\nBase domain for routes accessible only within the internal network. Use this for service-to-service communication.", - "type": "Label", - "options": { - "format": "markdown" - } - } - ] - }, { "type": "Category", "label": "Routes", "elements": [ { - "text": "### Route Configuration\nDefine how incoming requests are matched and forwarded to backend services.\n\n| Field | Description |\n|-------|-------------|\n| **Verb** | HTTP method to match (GET, POST, PUT, etc.) |\n| **Path** | URL path pattern (e.g., `/api/v1/users`) |\n| **Scope** | Target service that will handle the request |\n| **Visibility** | `public` (external) or `private` (internal network only) |\n| **Groups** | Security groups allowed to access this route. Leave empty for unrestricted access |", + "text": "### Route Configuration\nDefine how incoming requests are matched and forwarded to backend services.\n\n| Field | Description |\n|-------|-------------|\n| **Verb** | HTTP method to match (GET, POST, PUT, etc.) |\n| **Path** | URL path pattern (e.g., `/api/v1/users`) |\n| **Scope** | Target service that will handle the request |\n| **Groups** | Security groups allowed to access this route. Leave empty for unrestricted access |", "type": "Label", "options": { "format": "markdown" @@ -48,7 +35,7 @@ "label": "Examples", "elements": [ { - "text": "### Public API Route\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/api/v1/wells\",\n \"scope\": \"wells-service\",\n \"visibility\": \"public\",\n \"groups\": []\n}\n```\n\n### Protected Internal Route\n```json\n{\n \"method\": \"POST\",\n \"path\": \"/internal/sync\",\n \"scope\": \"sync-service\",\n \"visibility\": \"private\",\n \"groups\": [\"AWS_PlataformaUpstream_Administrador_Desa\"]\n}\n```", + "text": "### API Route\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/api/v1/wells\",\n \"scope\": \"wells-service\",\n \"groups\": []\n}\n```\n\n### Protected Route\n```json\n{\n \"method\": \"POST\",\n \"path\": \"/internal/sync\",\n \"scope\": \"sync-service\",\n \"groups\": [\"AWS_PlataformaUpstream_Administrador_Desa\"]\n}\n```", "type": "Label", "options": { "format": "markdown" From 70735ff9244463526b5e8bfdfcf1d270719de0d7 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 14:00:25 -0300 Subject: [PATCH 060/101] feat(endpoint-exposer): implement read action to show live AVP Cedar policies Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/read_policies | 48 ++++++++++++++++++++++ endpoint-exposer/workflows/istio/read.yaml | 18 ++------ 2 files changed, 51 insertions(+), 15 deletions(-) create mode 100755 endpoint-exposer/scripts/avp/read_policies diff --git a/endpoint-exposer/scripts/avp/read_policies b/endpoint-exposer/scripts/avp/read_policies new file mode 100755 index 0000000..eb2086b --- /dev/null +++ b/endpoint-exposer/scripts/avp/read_policies @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +echo "=== AVP Cedar Policies for service $SERVICE_ID ===" +echo "" + +DESCRIPTION_PREFIX="endpoint-exposer|service-id=$SERVICE_ID" + +POLICIES_JSON=$(aws verifiedpermissions list-policies \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --output json | jq -c \ + --arg prefix "$DESCRIPTION_PREFIX" \ + '[.policies[] | select((.definition.static.description // "") | startswith($prefix))]') + +NUM_POLICIES=$(echo "$POLICIES_JSON" | jq 'length') + +if [[ "$NUM_POLICIES" -eq 0 ]]; then + echo "No AVP policies found for service $SERVICE_ID" + exit 0 +fi + +echo "Found $NUM_POLICIES Cedar policies:" +echo "" +printf "%-8s %-45s %-55s %s\n" "METHOD" "PATH" "DOMAIN" "GROUPS" +printf "%-8s %-45s %-55s %s\n" "------" "----" "------" "------" + +for i in $(seq 0 $((NUM_POLICIES - 1))); do + POLICY=$(echo "$POLICIES_JSON" | jq -c ".[$i]") + DESCRIPTION=$(echo "$POLICY" | jq -r '.definition.static.description // ""') + STATEMENT=$(echo "$POLICY" | jq -r '.definition.static.statement // ""') + + ROUTE=$(echo "$DESCRIPTION" | grep -oP 'route=\K[^|]+' || true) + METHOD=$(echo "$ROUTE" | cut -d: -f1) + ROUTE_PATH=$(echo "$ROUTE" | cut -d: -f2-) + + DOMAIN=$(echo "$STATEMENT" | grep -oP 'resource\.host == "\K[^"]+' || true) + + GROUPS=$(echo "$STATEMENT" | grep -oP 'ApiAccess::Group::"[^"]*"' \ + | sed 's/ApiAccess::Group::"//; s/"$//' \ + | tr '\n' ',' | sed 's/,$//' || true) + [[ -z "$GROUPS" ]] && GROUPS="(open access)" + + printf "%-8s %-45s %-55s %s\n" "$METHOD" "$ROUTE_PATH" "$DOMAIN" "$GROUPS" +done diff --git a/endpoint-exposer/workflows/istio/read.yaml b/endpoint-exposer/workflows/istio/read.yaml index 8de54ea..ab37b80 100644 --- a/endpoint-exposer/workflows/istio/read.yaml +++ b/endpoint-exposer/workflows/istio/read.yaml @@ -1,10 +1,4 @@ steps: - - name: "find k8s namespace" - type: script - file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" - output: - - name: K8S_NAMESPACE - type: environment - name: "build context" type: script file: "$SERVICE_PATH/scripts/istio/build_context" @@ -17,14 +11,8 @@ steps: type: environment - name: ACTION_NAME type: environment - - name: LINK_ID - type: environment - - name: LINK_NAME - type: environment - - name: SCOPE_ID - type: environment - - name: RULE_PATH + - name: APPLICATION_ID type: environment - - name: read + - name: "read avp policies" type: script - file: "$SERVICE_PATH/scripts/istio/read_ingress" \ No newline at end of file + file: "$SERVICE_PATH/scripts/avp/read_policies" From b478f06fb7c5b74d089ca060b38a4a7dfbaf2a76 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 14:12:51 -0300 Subject: [PATCH 061/101] fix(endpoint-exposer): enable read action button Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/actions/read.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/actions/read.json.tpl b/endpoint-exposer/specs/actions/read.json.tpl index f6a4c52..49d0264 100644 --- a/endpoint-exposer/specs/actions/read.json.tpl +++ b/endpoint-exposer/specs/actions/read.json.tpl @@ -3,7 +3,7 @@ "slug": "read", "type": "custom", "annotations": {}, - "enabled_when": "", + "enabled_when": "true", "retryable": false, "service_specification_id": "{{ env.Getenv "SERVICE_SPECIFICATION_ID" }}", "parameters": { From 392a51bd94d91ce1e5354eea35f12f9c2bf6c3c6 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 14:24:21 -0300 Subject: [PATCH 062/101] fix(endpoint-exposer): remove gomplate expression from read action spec for Terraform compatibility Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/actions/read.json.tpl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/endpoint-exposer/specs/actions/read.json.tpl b/endpoint-exposer/specs/actions/read.json.tpl index 49d0264..4b4ebdf 100644 --- a/endpoint-exposer/specs/actions/read.json.tpl +++ b/endpoint-exposer/specs/actions/read.json.tpl @@ -5,7 +5,6 @@ "annotations": {}, "enabled_when": "true", "retryable": false, - "service_specification_id": "{{ env.Getenv "SERVICE_SPECIFICATION_ID" }}", "parameters": { "schema": { "type": "object", @@ -22,4 +21,4 @@ }, "values": {} } -} \ No newline at end of file +} From a60f32ecb592184ddafc7a3c41a884dab2426564 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 14:47:18 -0300 Subject: [PATCH 063/101] fix(endpoint-exposer): replace grep -P with BusyBox-compatible sed/grep -E in read_policies Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/read_policies | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/endpoint-exposer/scripts/avp/read_policies b/endpoint-exposer/scripts/avp/read_policies index eb2086b..eecbf46 100755 --- a/endpoint-exposer/scripts/avp/read_policies +++ b/endpoint-exposer/scripts/avp/read_policies @@ -33,14 +33,17 @@ for i in $(seq 0 $((NUM_POLICIES - 1))); do DESCRIPTION=$(echo "$POLICY" | jq -r '.definition.static.description // ""') STATEMENT=$(echo "$POLICY" | jq -r '.definition.static.statement // ""') - ROUTE=$(echo "$DESCRIPTION" | grep -oP 'route=\K[^|]+' || true) + # Parse description using sed (BusyBox-compatible) + ROUTE=$(echo "$DESCRIPTION" | sed -n 's/.*route=\([^|]*\).*/\1/p') METHOD=$(echo "$ROUTE" | cut -d: -f1) ROUTE_PATH=$(echo "$ROUTE" | cut -d: -f2-) - DOMAIN=$(echo "$STATEMENT" | grep -oP 'resource\.host == "\K[^"]+' || true) + # Extract domain from Cedar statement using sed + DOMAIN=$(echo "$STATEMENT" | grep -oE 'resource\.host == "[^"]*"' | sed 's/resource\.host == "//;s/"//') - GROUPS=$(echo "$STATEMENT" | grep -oP 'ApiAccess::Group::"[^"]*"' \ - | sed 's/ApiAccess::Group::"//; s/"$//' \ + # Extract groups using grep -E and sed + GROUPS=$(echo "$STATEMENT" | grep -oE 'ApiAccess::Group::"[^"]*"' \ + | sed 's/ApiAccess::Group::"//;s/"$//' \ | tr '\n' ',' | sed 's/,$//' || true) [[ -z "$GROUPS" ]] && GROUPS="(open access)" From b2a2ab80fec69d821dbe1ab8c8b94df5ca5282a5 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 14:59:02 -0300 Subject: [PATCH 064/101] fix(endpoint-exposer): add || true to grep calls to avoid set -e failures on no-match Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/read_policies | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/endpoint-exposer/scripts/avp/read_policies b/endpoint-exposer/scripts/avp/read_policies index eecbf46..b9507a8 100755 --- a/endpoint-exposer/scripts/avp/read_policies +++ b/endpoint-exposer/scripts/avp/read_policies @@ -34,15 +34,15 @@ for i in $(seq 0 $((NUM_POLICIES - 1))); do STATEMENT=$(echo "$POLICY" | jq -r '.definition.static.statement // ""') # Parse description using sed (BusyBox-compatible) - ROUTE=$(echo "$DESCRIPTION" | sed -n 's/.*route=\([^|]*\).*/\1/p') + ROUTE=$(echo "$DESCRIPTION" | sed -n 's/.*route=\([^|]*\).*/\1/p' || true) METHOD=$(echo "$ROUTE" | cut -d: -f1) ROUTE_PATH=$(echo "$ROUTE" | cut -d: -f2-) # Extract domain from Cedar statement using sed - DOMAIN=$(echo "$STATEMENT" | grep -oE 'resource\.host == "[^"]*"' | sed 's/resource\.host == "//;s/"//') + DOMAIN=$(echo "$STATEMENT" | grep -oE 'resource\.host == "[^"]*"' 2>/dev/null | sed 's/resource\.host == "//;s/"//' || true) # Extract groups using grep -E and sed - GROUPS=$(echo "$STATEMENT" | grep -oE 'ApiAccess::Group::"[^"]*"' \ + GROUPS=$(echo "$STATEMENT" | grep -oE 'ApiAccess::Group::"[^"]*"' 2>/dev/null \ | sed 's/ApiAccess::Group::"//;s/"$//' \ | tr '\n' ',' | sed 's/,$//' || true) [[ -z "$GROUPS" ]] && GROUPS="(open access)" From 8add791865764b347bf0805ec71f59bdd7d073b8 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 15:03:38 -0300 Subject: [PATCH 065/101] fix(endpoint-exposer): use get-policy for full statement, simplify parsing Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/read_policies | 26 ++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/endpoint-exposer/scripts/avp/read_policies b/endpoint-exposer/scripts/avp/read_policies index b9507a8..4e3544c 100755 --- a/endpoint-exposer/scripts/avp/read_policies +++ b/endpoint-exposer/scripts/avp/read_policies @@ -14,7 +14,7 @@ POLICIES_JSON=$(aws verifiedpermissions list-policies \ --region "$AVP_AWS_REGION" \ --output json | jq -c \ --arg prefix "$DESCRIPTION_PREFIX" \ - '[.policies[] | select((.definition.static.description // "") | startswith($prefix))]') + '[.policies[] | select((.definition.static.description // "") | startswith($prefix)) | {policyId, description: .definition.static.description}]') NUM_POLICIES=$(echo "$POLICIES_JSON" | jq 'length') @@ -29,23 +29,31 @@ printf "%-8s %-45s %-55s %s\n" "METHOD" "PATH" "DOMAIN" "GROUPS" printf "%-8s %-45s %-55s %s\n" "------" "----" "------" "------" for i in $(seq 0 $((NUM_POLICIES - 1))); do - POLICY=$(echo "$POLICIES_JSON" | jq -c ".[$i]") - DESCRIPTION=$(echo "$POLICY" | jq -r '.definition.static.description // ""') - STATEMENT=$(echo "$POLICY" | jq -r '.definition.static.statement // ""') + ENTRY=$(echo "$POLICIES_JSON" | jq -r ".[$i]") + POLICY_ID=$(echo "$ENTRY" | jq -r '.policyId') + DESCRIPTION=$(echo "$ENTRY" | jq -r '.description') - # Parse description using sed (BusyBox-compatible) + # Parse description (BusyBox-compatible sed) ROUTE=$(echo "$DESCRIPTION" | sed -n 's/.*route=\([^|]*\).*/\1/p' || true) METHOD=$(echo "$ROUTE" | cut -d: -f1) ROUTE_PATH=$(echo "$ROUTE" | cut -d: -f2-) - # Extract domain from Cedar statement using sed - DOMAIN=$(echo "$STATEMENT" | grep -oE 'resource\.host == "[^"]*"' 2>/dev/null | sed 's/resource\.host == "//;s/"//' || true) + # Fetch full statement via get-policy + STATEMENT=$(aws verifiedpermissions get-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" \ + --output json | jq -r '.definition.static.statement // ""') - # Extract groups using grep -E and sed + # Extract domain (BusyBox-compatible) + DOMAIN=$(echo "$STATEMENT" | grep -oE 'resource\.host == "[^"]*"' 2>/dev/null \ + | sed 's/resource\.host == "//;s/".*//' || true) + + # Extract groups (BusyBox-compatible) GROUPS=$(echo "$STATEMENT" | grep -oE 'ApiAccess::Group::"[^"]*"' 2>/dev/null \ | sed 's/ApiAccess::Group::"//;s/"$//' \ | tr '\n' ',' | sed 's/,$//' || true) - [[ -z "$GROUPS" ]] && GROUPS="(open access)" + [ -z "$GROUPS" ] && GROUPS="(open access)" printf "%-8s %-45s %-55s %s\n" "$METHOD" "$ROUTE_PATH" "$DOMAIN" "$GROUPS" done From a48b939c0b1ab6b05216d8a5c6110661f154f134 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 15:24:38 -0300 Subject: [PATCH 066/101] fix(endpoint-exposer): use sed BRE for group extraction, more BusyBox-compatible Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/read_policies | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/endpoint-exposer/scripts/avp/read_policies b/endpoint-exposer/scripts/avp/read_policies index 4e3544c..97f3da1 100755 --- a/endpoint-exposer/scripts/avp/read_policies +++ b/endpoint-exposer/scripts/avp/read_policies @@ -49,9 +49,9 @@ for i in $(seq 0 $((NUM_POLICIES - 1))); do DOMAIN=$(echo "$STATEMENT" | grep -oE 'resource\.host == "[^"]*"' 2>/dev/null \ | sed 's/resource\.host == "//;s/".*//' || true) - # Extract groups (BusyBox-compatible) - GROUPS=$(echo "$STATEMENT" | grep -oE 'ApiAccess::Group::"[^"]*"' 2>/dev/null \ - | sed 's/ApiAccess::Group::"//;s/"$//' \ + # Extract groups (BusyBox sed BRE - one group per line in Cedar statement) + GROUPS=$(echo "$STATEMENT" | grep 'ApiAccess::Group::' 2>/dev/null \ + | sed 's/.*ApiAccess::Group::"\([^"]*\)".*/\1/' \ | tr '\n' ',' | sed 's/,$//' || true) [ -z "$GROUPS" ] && GROUPS="(open access)" From 4d6a7a57ef119fad5e831689c08c46b1f96fc2f3 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 15:29:30 -0300 Subject: [PATCH 067/101] Remove Authorization Group from uiSchema Properties were not defined in the schema, causing "No applicable renderer found." errors in the UI. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 32 -------------------- 1 file changed, 32 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 8cc04eb..fc5a2b3 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -45,38 +45,6 @@ } ] }, - { - "type": "Group", - "label": "Authorization", - "elements": [ - { - "type": "Control", - "scope": "#/properties/authorization/properties/enabled" - }, - { - "type": "Control", - "scope": "#/properties/authorization/properties/headerName", - "rule": { - "effect": "SHOW", - "condition": { - "scope": "#/properties/authorization/properties/enabled", - "schema": { "const": true } - } - } - }, - { - "type": "Control", - "scope": "#/properties/authorization/properties/allowedValues", - "rule": { - "effect": "SHOW", - "condition": { - "scope": "#/properties/authorization/properties/enabled", - "schema": { "const": true } - } - } - } - ] - }, { "type": "Group", "label": "Routes", From e4190aae52a52a3c476ecc51c212d788655d0f3f Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 20:11:30 -0300 Subject: [PATCH 068/101] Add link-per-route model for AVP Cedar policy management Each link represents one route (method + path + scope + visibility + groups). Creating a link creates a dedicated HTTPRoute (one rule, one DNS endpoint) and a Cedar policy in AVP. Deleting a link cleans up both resources. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/entrypoint/link | 4 +- .../scripts/avp/create_link_policy | 57 +++++++++++++ .../scripts/avp/delete_link_policy | 20 +++++ .../scripts/istio/build_link_context | 42 ++++++++++ .../scripts/istio/build_link_httproute | 81 ++++++++++++++++++ .../scripts/istio/delete_link_httproute | 19 +++++ .../scripts/np/update_link_results | 17 ++++ endpoint-exposer/specs/links/route.json.tpl | 82 +++++++++++++++++++ endpoint-exposer/specs/service-spec.json.tpl | 5 +- endpoint-exposer/workflows/istio/link.yaml | 56 +++++++++++++ endpoint-exposer/workflows/istio/unlink.yaml | 7 ++ 11 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 endpoint-exposer/scripts/avp/create_link_policy create mode 100644 endpoint-exposer/scripts/avp/delete_link_policy create mode 100644 endpoint-exposer/scripts/istio/build_link_context create mode 100644 endpoint-exposer/scripts/istio/build_link_httproute create mode 100644 endpoint-exposer/scripts/istio/delete_link_httproute create mode 100644 endpoint-exposer/scripts/np/update_link_results create mode 100644 endpoint-exposer/specs/links/route.json.tpl create mode 100644 endpoint-exposer/workflows/istio/link.yaml create mode 100644 endpoint-exposer/workflows/istio/unlink.yaml diff --git a/endpoint-exposer/entrypoint/link b/endpoint-exposer/entrypoint/link index 253ef01..1f40cf6 100755 --- a/endpoint-exposer/entrypoint/link +++ b/endpoint-exposer/entrypoint/link @@ -16,7 +16,7 @@ case "$SERVICE_ACTION_TYPE" in ;; esac -INGRESS_TYPE="${INGRESS_TYPE:-alb}" +INGRESS_TYPE="${INGRESS_TYPE:-istio}" echo "INGRESS_TYPE is set to '$INGRESS_TYPE'" @@ -24,7 +24,7 @@ WORKFLOW_PATH="$SERVICE_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" OVERRIDES_WORKFLOW_PATH="$OVERRIDES_PATH/workflows/$INGRESS_TYPE/$ACTION_TO_EXECUTE.yaml" VALUES_PATH="$SERVICE_PATH/values.yaml" -CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH" +CMD="np service workflow exec --workflow $WORKFLOW_PATH --values $VALUES_PATH --build-context --include-secrets" if [[ -f "$OVERRIDES_WORKFLOW_PATH" ]]; then CMD="$CMD --overrides $OVERRIDES_WORKFLOW_PATH" diff --git a/endpoint-exposer/scripts/avp/create_link_policy b/endpoint-exposer/scripts/avp/create_link_policy new file mode 100644 index 0000000..fc2ff79 --- /dev/null +++ b/endpoint-exposer/scripts/avp/create_link_policy @@ -0,0 +1,57 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +echo "=== Creating AVP Cedar Policy: $METHOD $ROUTE_PATH (scope: $SCOPE) ===" + +NUM_GROUPS=$(echo "$GROUPS_JSON" | jq 'length') + +if [[ "$NUM_GROUPS" -gt 0 ]]; then + GROUP_CONDITIONS=$(echo "$GROUPS_JSON" | jq -r \ + 'map("principal in ApiAccess::Group::\"" + . + "\"") | join(" ||\n ")') + CEDAR_STATEMENT="permit ( + principal, + action == ApiAccess::Action::\"$METHOD\", + resource +) +when { + ($GROUP_CONDITIONS) && + resource has host && + resource.host == \"$SCOPE_DOMAIN\" && + resource.path == \"$ROUTE_PATH\" +};" +else + CEDAR_STATEMENT="permit ( + principal, + action == ApiAccess::Action::\"$METHOD\", + resource +) +when { + resource has host && + resource.host == \"$SCOPE_DOMAIN\" && + resource.path == \"$ROUTE_PATH\" +};" +fi + +DESCRIPTION="endpoint-exposer|service-id=$SERVICE_ID|link=$HTTPROUTE_NAME|route=$METHOD:$ROUTE_PATH|scope=$SCOPE_ID" + +DEFINITION=$(jq -n \ + --arg desc "$DESCRIPTION" \ + --arg stmt "$CEDAR_STATEMENT" \ + '{"static": {"description": $desc, "statement": $stmt}}') + +POLICY_ID=$(aws verifiedpermissions create-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --definition "$DEFINITION" \ + --query 'policyId' --output text) || { echo "ERROR: Failed to create Cedar policy"; exit 1; } + +if [[ -z "$POLICY_ID" || "$POLICY_ID" == "None" ]]; then + echo "ERROR: Policy creation returned no policyId" + exit 1 +fi + +echo "✅ Cedar policy created: $METHOD $ROUTE_PATH @ $SCOPE_DOMAIN (groups: $NUM_GROUPS, policy-id: $POLICY_ID)" +export POLICY_ID diff --git a/endpoint-exposer/scripts/avp/delete_link_policy b/endpoint-exposer/scripts/avp/delete_link_policy new file mode 100644 index 0000000..22d150a --- /dev/null +++ b/endpoint-exposer/scripts/avp/delete_link_policy @@ -0,0 +1,20 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +POLICY_ID="${ACTION_LINK_ATTRIBUTES_POLICY_ID:-}" + +if [[ -z "$POLICY_ID" || "$POLICY_ID" == "null" ]]; then + echo "WARNING: No policy_id found in link attributes, skipping Cedar policy deletion" + exit 0 +fi + +echo "Deleting Cedar policy: $POLICY_ID" +aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" || { echo "WARNING: Failed to delete policy $POLICY_ID (may already be deleted)"; } + +echo "✅ Cedar policy $POLICY_ID deleted" diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context new file mode 100644 index 0000000..f90f613 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -0,0 +1,42 @@ +#!/bin/bash + +set -euo pipefail + +CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" +if [[ -z "$CONTEXT" ]]; then + echo "ERROR: No context available (CONTEXT or NP_ACTION_CONTEXT must be set)" + exit 1 +fi + +SERVICE_ID=$(echo "$CONTEXT" | jq -r '.service.id') +SERVICE_SLUG=$(echo "$CONTEXT" | jq -r '.service.slug') +ACTION_ID=$(echo "$CONTEXT" | jq -r '.id') +APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') + +METHOD=$(echo "$CONTEXT" | jq -r '.parameters.method') +ROUTE_PATH=$(echo "$CONTEXT" | jq -r '.parameters.path') +SCOPE=$(echo "$CONTEXT" | jq -r '.parameters.scope') +VISIBILITY=$(echo "$CONTEXT" | jq -r '.parameters.visibility // "public"') +GROUPS_JSON=$(echo "$CONTEXT" | jq -c '.parameters.groups // []') + +# Unique HTTPRoute name per link — stored in link attributes for use on delete +HTTPROUTE_NAME="ee-link-${ACTION_ID}" + +K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" + +if [[ -z "${OUTPUT_DIR:-}" ]]; then + export OUTPUT_DIR="$SERVICE_PATH/output/$SERVICE_SLUG-$SERVICE_ID/link-$ACTION_ID" +fi +mkdir -p "$OUTPUT_DIR" + +export SERVICE_ID +export SERVICE_SLUG +export ACTION_ID +export APPLICATION_ID +export METHOD +export ROUTE_PATH +export SCOPE +export VISIBILITY +export GROUPS_JSON +export HTTPROUTE_NAME +export K8S_NAMESPACE diff --git a/endpoint-exposer/scripts/istio/build_link_httproute b/endpoint-exposer/scripts/istio/build_link_httproute new file mode 100644 index 0000000..16a4d1a --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_link_httproute @@ -0,0 +1,81 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/istio/config" +source "$SERVICE_PATH/scripts/avp/config" + +echo "=== Building HTTPRoute for link: $METHOD $ROUTE_PATH (scope: $SCOPE, visibility: $VISIBILITY) ===" + +GATEWAY_NAME="$PUBLIC_GATEWAY_NAME" +if [[ "$VISIBILITY" == "private" ]]; then + GATEWAY_NAME="$PRIVATE_GATEWAY_NAME" +fi + +SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') + +SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE" '.[] | select(.name == $name) | .id') +if [[ -z "$SCOPE_ID" || "$SCOPE_ID" == "null" ]]; then + echo "ERROR: Could not resolve scope '$SCOPE'" + exit 1 +fi + +SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') +if [[ -z "$SCOPE_DOMAIN" || "$SCOPE_DOMAIN" == "null" ]]; then + echo "ERROR: Scope '$SCOPE' (id: $SCOPE_ID) has no domain" + exit 1 +fi + +echo "Resolved scope '$SCOPE' → domain: $SCOPE_DOMAIN" + +# Detect path match type +if echo "$ROUTE_PATH" | grep -q '\*'; then + PATH_TYPE="PathPrefix" + CONVERTED_PATH=$(echo "$ROUTE_PATH" | sed 's/\*.*//') +elif echo "$ROUTE_PATH" | grep -q ':'; then + PATH_TYPE="RegularExpression" + CONVERTED_PATH=$(echo "$ROUTE_PATH" | sed 's/:[^/]*/[^\/]+/g') +else + PATH_TYPE="Exact" + CONVERTED_PATH="$ROUTE_PATH" +fi + +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-${HTTPROUTE_NAME}.yaml" + +cat > "$HTTPROUTE_FILE" </dev/null; then + kubectl delete httproute "$HTTPROUTE_NAME" -n "$K8S_NAMESPACE" + echo "✅ HTTPRoute $HTTPROUTE_NAME deleted" +else + echo "HTTPRoute $HTTPROUTE_NAME not found (already deleted or never existed)" +fi diff --git a/endpoint-exposer/scripts/np/update_link_results b/endpoint-exposer/scripts/np/update_link_results new file mode 100644 index 0000000..3a9199e --- /dev/null +++ b/endpoint-exposer/scripts/np/update_link_results @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail + +CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" + +RESULTS=$(echo "$CONTEXT" | jq \ + --arg httproute_name "$HTTPROUTE_NAME" \ + --arg policy_id "$POLICY_ID" \ + '(.notification.parameters // .parameters // {}) + { + httproute_name: $httproute_name, + policy_id: $policy_id + }') + +echo "Storing link results: httproute_name=$HTTPROUTE_NAME, policy_id=$POLICY_ID" +np link action update --results "$RESULTS" +echo "✅ Link results stored" diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl new file mode 100644 index 0000000..46ace01 --- /dev/null +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -0,0 +1,82 @@ +{ + "name": "Route", + "slug": "route", + "unique": false, + "assignable_to": "any", + "use_default_actions": true, + "attributes": { + "schema": { + "type": "object", + "required": ["method", "path", "scope"], + "properties": { + "method": { + "type": "string", + "title": "HTTP Method", + "editableOn": ["create"], + "visibleOn": ["read"], + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] + }, + "path": { + "type": "string", + "title": "Path", + "editableOn": ["create"], + "visibleOn": ["read"] + }, + "scope": { + "type": "string", + "title": "Scope", + "editableOn": ["create"], + "visibleOn": ["read"], + "additionalKeywords": { + "enum": "[.scopes[]?.slug]" + } + }, + "visibility": { + "type": "string", + "title": "Visibility", + "default": "public", + "editableOn": ["create"], + "visibleOn": ["read"], + "enum": ["public", "private"] + }, + "groups": { + "type": "array", + "title": "Authorized Groups", + "uniqueItems": true, + "editableOn": ["create"], + "visibleOn": ["read"], + "items": { + "type": "string", + "enum": [ + "AWS_PlataformaUpstream_Gestor_Desa", + "AWS_PlataformaUpstream_Programador_Desa", + "AWS_PlataformaUpstream_Pulling_Desa", + "AWS_PlataformaUpstream_Workover_Desa", + "AWS_PlataformaUpstream_Visita_Desa", + "AWS_PlataformaUpstream_Administrador_Desa" + ] + } + }, + "httproute_name": { + "type": "string", + "title": "HTTPRoute Name", + "editableOn": [], + "visibleOn": ["read"] + }, + "policy_id": { + "type": "string", + "title": "AVP Policy ID", + "editableOn": [], + "visibleOn": ["read"] + } + } + }, + "values": {} + }, + "selectors": { + "category": "any", + "imported": false, + "provider": "any", + "sub_category": "any" + } +} diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index fc5a2b3..582ad8a 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -176,10 +176,9 @@ "slug": "endpoint-exposer", "type": "dependency", "use_default_actions": true, - "available_actions": [ - "read" - ], + "available_actions": [], "available_links": [ + "route" ], "visible_to": [ "{{ env.Getenv \"NRN\" }}" diff --git a/endpoint-exposer/workflows/istio/link.yaml b/endpoint-exposer/workflows/istio/link.yaml new file mode 100644 index 0000000..b8f525b --- /dev/null +++ b/endpoint-exposer/workflows/istio/link.yaml @@ -0,0 +1,56 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_link_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_SLUG + type: environment + - name: ACTION_ID + type: environment + - name: APPLICATION_ID + type: environment + - name: METHOD + type: environment + - name: ROUTE_PATH + type: environment + - name: SCOPE + type: environment + - name: VISIBILITY + type: environment + - name: GROUPS_JSON + type: environment + - name: HTTPROUTE_NAME + type: environment + - name: K8S_NAMESPACE + type: environment + - name: "build httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/build_link_httproute" + output: + - name: SCOPE_ID + type: environment + - name: SCOPE_DOMAIN + type: environment + - name: "create avp policy" + type: script + file: "$SERVICE_PATH/scripts/avp/create_link_policy" + output: + - name: POLICY_ID + type: environment + - name: "apply" + type: script + file: "$SERVICE_PATH/scripts/common/apply" + configuration: + ACTION: apply + DRY_RUN: false + - name: "store link results" + type: script + file: "$SERVICE_PATH/scripts/np/update_link_results" diff --git a/endpoint-exposer/workflows/istio/unlink.yaml b/endpoint-exposer/workflows/istio/unlink.yaml new file mode 100644 index 0000000..4401774 --- /dev/null +++ b/endpoint-exposer/workflows/istio/unlink.yaml @@ -0,0 +1,7 @@ +steps: + - name: "delete avp policy" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_link_policy" + - name: "delete httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/delete_link_httproute" From c380f3e48cebde10462760de34e3225ba4929f4b Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 20:19:50 -0300 Subject: [PATCH 069/101] Remove additionalKeywords from scope in route link spec additionalKeywords resolves to empty enum in link specs causing JSON Forms to fail rendering the schema. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 46ace01..06063fa 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -26,10 +26,7 @@ "type": "string", "title": "Scope", "editableOn": ["create"], - "visibleOn": ["read"], - "additionalKeywords": { - "enum": "[.scopes[]?.slug]" - } + "visibleOn": ["read"] }, "visibility": { "type": "string", From 61871686df6a7e6825c75dee749fadb0197c5b13 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 20:25:53 -0300 Subject: [PATCH 070/101] feat(endpoint-exposer): add uiSchema to route link spec, remove routes from service spec - Add uiSchema to route link spec so JSON Forms renders method, path, scope, visibility, and groups fields in the link creation form - Remove routes array and all related uiSchema elements from service spec since routes are now managed individually via links Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 15 ++ endpoint-exposer/specs/service-spec.json.tpl | 155 +------------------ 2 files changed, 17 insertions(+), 153 deletions(-) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 06063fa..385664b 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -8,6 +8,21 @@ "schema": { "type": "object", "required": ["method", "path", "scope"], + "uiSchema": { + "type": "VerticalLayout", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + {"type": "Control", "label": "HTTP Method", "scope": "#/properties/method"}, + {"type": "Control", "label": "Visibility", "scope": "#/properties/visibility"} + ] + }, + {"type": "Control", "label": "Path", "scope": "#/properties/path"}, + {"type": "Control", "label": "Scope", "scope": "#/properties/scope"}, + {"type": "Control", "label": "Authorized Groups", "scope": "#/properties/groups"} + ] + }, "properties": { "method": { "type": "string", diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 582ad8a..ca0229b 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -7,160 +7,9 @@ "required": [], "uiSchema": { "type": "VerticalLayout", - "elements": [ - { - "type": "Categorization", - "options": { - "collapsable": { - "label": "Documentation", - "collapsed": true - } - }, - "elements": [ - { - "type": "Category", - "label": "Routes", - "elements": [ - { - "text": "### Route Configuration\nDefine how incoming requests are matched and forwarded to backend services.\n\n| Field | Description |\n|-------|-------------|\n| **Verb** | HTTP method to match (GET, POST, PUT, etc.) |\n| **Path** | URL path pattern (e.g., `/api/v1/users`) |\n| **Scope** | Target service that will handle the request |\n| **Groups** | Security groups allowed to access this route. Leave empty for unrestricted access |", - "type": "Label", - "options": { - "format": "markdown" - } - } - ] - }, - { - "type": "Category", - "label": "Examples", - "elements": [ - { - "text": "### API Route\n```json\n{\n \"method\": \"GET\",\n \"path\": \"/api/v1/wells\",\n \"scope\": \"wells-service\",\n \"groups\": []\n}\n```\n\n### Protected Route\n```json\n{\n \"method\": \"POST\",\n \"path\": \"/internal/sync\",\n \"scope\": \"sync-service\",\n \"groups\": [\"AWS_PlataformaUpstream_Administrador_Desa\"]\n}\n```", - "type": "Label", - "options": { - "format": "markdown" - } - } - ] - } - ] - }, - { - "type": "Group", - "label": "Routes", - "elements": [ - { - "type": "Control", - "scope": "#/properties/routes", - "options": { - "detail": { - "type": "VerticalLayout", - "elements": [ - { - "type": "Control", - "label": "Verb", - "scope": "#/properties/method" - }, - { - "type": "HorizontalLayout", - "elements": [ - { - "type": "Control", - "label": "Path", - "scope": "#/properties/path" - }, - { - "type": "Control", - "label": "Scope", - "scope": "#/properties/scope" - }, - { - "type": "Control", - "label": "Visibility", - "scope": "#/properties/visibility" - } - ] - }, - { - "type": "Control", - "label": "Groups", - "scope": "#/properties/groups" - } - ] - }, - "showSortButtons": true - } - } - ] - } - ] + "elements": [] }, - "properties": { - "routes": { - "type": "array", - "title": "Routes", - "items": { - "type": "object", - "required": [ - "method", - "path", - "scope", - "visibility" - ], - "properties": { - "path": { - "type": "string", - "title": "Path" - }, - "scope": { - "type": "string", - "title": "Scope", - "additionalKeywords": { - "enum": "[.scopes[]?.slug]" - } - }, - "groups": { - "type": "array", - "title": "Authorized Groups", - "uniqueItems": true, - "items": { - "type": "string", - "enum": [ - "AWS_PlataformaUpstream_Gestor_Desa", - "AWS_PlataformaUpstream_Programador_Desa", - "AWS_PlataformaUpstream_Pulling_Desa", - "AWS_PlataformaUpstream_Workover_Desa", - "AWS_PlataformaUpstream_Visita_Desa", - "AWS_PlataformaUpstream_Administrador_Desa" - ] - } - }, - "method": { - "type": "string", - "title": "Verb", - "enum": [ - "GET", - "POST", - "PUT", - "PATCH", - "DELETE", - "HEAD", - "OPTIONS" - ] - }, - "visibility": { - "type": "string", - "title": "Visibility", - "default": "public", - "enum": [ - "public", - "private" - ] - } - } - } - } - } + "properties": {} }, "values": {} }, From e4680497be136e6fa49a3e11a0678d0e95c252f3 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 20:48:59 -0300 Subject: [PATCH 071/101] fix(endpoint-exposer): remove visibleOn from input fields in route link spec Fields with visibleOn: ["read"] are hidden from the create form. Input fields (method, path, scope, visibility, groups) should have no visibleOn restriction so they appear during link creation. Output-only fields (httproute_name, policy_id) keep visibleOn: ["read"]. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 385664b..abd318a 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -28,27 +28,23 @@ "type": "string", "title": "HTTP Method", "editableOn": ["create"], - "visibleOn": ["read"], "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "title": "Path", - "editableOn": ["create"], - "visibleOn": ["read"] + "editableOn": ["create"] }, "scope": { "type": "string", "title": "Scope", - "editableOn": ["create"], - "visibleOn": ["read"] + "editableOn": ["create"] }, "visibility": { "type": "string", "title": "Visibility", "default": "public", "editableOn": ["create"], - "visibleOn": ["read"], "enum": ["public", "private"] }, "groups": { @@ -56,7 +52,6 @@ "title": "Authorized Groups", "uniqueItems": true, "editableOn": ["create"], - "visibleOn": ["read"], "items": { "type": "string", "enum": [ From 8e73afe658eaa7fa67e85a7a3e42c87a305946d9 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 20:57:36 -0300 Subject: [PATCH 072/101] feat(endpoint-exposer): render Authorized Groups in vertical column layout Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index abd318a..7efa0af 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -20,7 +20,14 @@ }, {"type": "Control", "label": "Path", "scope": "#/properties/path"}, {"type": "Control", "label": "Scope", "scope": "#/properties/scope"}, - {"type": "Control", "label": "Authorized Groups", "scope": "#/properties/groups"} + { + "type": "Control", + "label": "Authorized Groups", + "scope": "#/properties/groups", + "options": { + "orientation": "vertical" + } + } ] }, "properties": { From 6fb368d420b74511cb73d489babfa141dff45b18 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 21:08:28 -0300 Subject: [PATCH 073/101] feat(endpoint-exposer): infer scope from link dimensions instead of form input Remove scope from the link creation form. The scope is now automatically derived by matching the link's service dimensions against available application scopes (excluding deleted ones). The resolved scope is stored in link results so it appears in the read view. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/istio/build_link_context | 14 +++++++++++++- endpoint-exposer/scripts/np/update_link_results | 6 ++++-- endpoint-exposer/specs/links/route.json.tpl | 6 +++--- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context index f90f613..6f57f82 100644 --- a/endpoint-exposer/scripts/istio/build_link_context +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -15,8 +15,20 @@ APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') METHOD=$(echo "$CONTEXT" | jq -r '.parameters.method') ROUTE_PATH=$(echo "$CONTEXT" | jq -r '.parameters.path') -SCOPE=$(echo "$CONTEXT" | jq -r '.parameters.scope') VISIBILITY=$(echo "$CONTEXT" | jq -r '.parameters.visibility // "public"') + +# Infer scope from link/service dimensions — avoids requiring user input +LINK_DIMENSIONS=$(echo "$CONTEXT" | jq -c '.service.dimensions // {}') +echo "Inferring scope from dimensions: $LINK_DIMENSIONS" +SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') +SCOPE=$(echo "$SCOPES_JSON" | jq -r \ + --argjson dims "$LINK_DIMENSIONS" \ + '[.[] | select(.name | startswith("deleted-") | not) | select(.dimensions == $dims)] | .[0].name // empty') +if [[ -z "$SCOPE" ]]; then + echo "ERROR: Could not infer scope from dimensions: $LINK_DIMENSIONS" + exit 1 +fi +echo "Inferred scope: $SCOPE" GROUPS_JSON=$(echo "$CONTEXT" | jq -c '.parameters.groups // []') # Unique HTTPRoute name per link — stored in link attributes for use on delete diff --git a/endpoint-exposer/scripts/np/update_link_results b/endpoint-exposer/scripts/np/update_link_results index 3a9199e..d10efce 100644 --- a/endpoint-exposer/scripts/np/update_link_results +++ b/endpoint-exposer/scripts/np/update_link_results @@ -7,11 +7,13 @@ CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" RESULTS=$(echo "$CONTEXT" | jq \ --arg httproute_name "$HTTPROUTE_NAME" \ --arg policy_id "$POLICY_ID" \ + --arg scope "$SCOPE" \ '(.notification.parameters // .parameters // {}) + { httproute_name: $httproute_name, - policy_id: $policy_id + policy_id: $policy_id, + scope: $scope }') -echo "Storing link results: httproute_name=$HTTPROUTE_NAME, policy_id=$POLICY_ID" +echo "Storing link results: httproute_name=$HTTPROUTE_NAME, policy_id=$POLICY_ID, scope=$SCOPE" np link action update --results "$RESULTS" echo "✅ Link results stored" diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 7efa0af..6fff34a 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -7,7 +7,7 @@ "attributes": { "schema": { "type": "object", - "required": ["method", "path", "scope"], + "required": ["method", "path"], "uiSchema": { "type": "VerticalLayout", "elements": [ @@ -19,7 +19,6 @@ ] }, {"type": "Control", "label": "Path", "scope": "#/properties/path"}, - {"type": "Control", "label": "Scope", "scope": "#/properties/scope"}, { "type": "Control", "label": "Authorized Groups", @@ -45,7 +44,8 @@ "scope": { "type": "string", "title": "Scope", - "editableOn": ["create"] + "editableOn": [], + "visibleOn": ["read"] }, "visibility": { "type": "string", From 611b01a7bf6e0fffac4e18afcf1c7564ded8c0a9 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 21:18:44 -0300 Subject: [PATCH 074/101] feat(endpoint-exposer): support multiple routes per link - Link form now uses a routes array (method, path, visibility, groups per route) matching the original service spec UI pattern - HTTPRoutes split by visibility: ee-link-{id}-public / -private - One Cedar policy created per route; all IDs stored as policy_ids array - Delete cleans up both HTTPRoute variants and all policy IDs - scope field removed from form (inferred from service dimensions) Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/avp/create_link_policy | 62 ++++++---- .../scripts/avp/delete_link_policy | 23 ++-- .../scripts/istio/build_link_context | 12 +- .../scripts/istio/build_link_httproute | 77 ++++++------ .../scripts/istio/delete_link_httproute | 21 ++-- .../scripts/np/update_link_results | 6 +- endpoint-exposer/specs/links/route.json.tpl | 111 ++++++++++-------- endpoint-exposer/workflows/istio/link.yaml | 10 +- 8 files changed, 171 insertions(+), 151 deletions(-) diff --git a/endpoint-exposer/scripts/avp/create_link_policy b/endpoint-exposer/scripts/avp/create_link_policy index fc2ff79..09398c1 100644 --- a/endpoint-exposer/scripts/avp/create_link_policy +++ b/endpoint-exposer/scripts/avp/create_link_policy @@ -4,14 +4,24 @@ set -euo pipefail source "$SERVICE_PATH/scripts/avp/config" -echo "=== Creating AVP Cedar Policy: $METHOD $ROUTE_PATH (scope: $SCOPE) ===" +TOTAL_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') +echo "=== Creating AVP Cedar Policies for $TOTAL_ROUTES routes ===" -NUM_GROUPS=$(echo "$GROUPS_JSON" | jq 'length') +POLICY_IDS_LIST=() -if [[ "$NUM_GROUPS" -gt 0 ]]; then - GROUP_CONDITIONS=$(echo "$GROUPS_JSON" | jq -r \ - 'map("principal in ApiAccess::Group::\"" + . + "\"") | join(" ||\n ")') - CEDAR_STATEMENT="permit ( +for i in $(seq 0 $((TOTAL_ROUTES - 1))); do + ROUTE=$(echo "$ROUTES_JSON" | jq ".[$i]") + METHOD=$(echo "$ROUTE" | jq -r '.method') + ROUTE_PATH=$(echo "$ROUTE" | jq -r '.path') + ROUTE_GROUPS=$(echo "$ROUTE" | jq -c '.groups // []') + NUM_GROUPS=$(echo "$ROUTE_GROUPS" | jq 'length') + + echo "Creating policy for: $METHOD $ROUTE_PATH (groups: $NUM_GROUPS)" + + if [[ "$NUM_GROUPS" -gt 0 ]]; then + GROUP_CONDITIONS=$(echo "$ROUTE_GROUPS" | jq -r \ + 'map("principal in ApiAccess::Group::\"" + . + "\"") | join(" ||\n ")') + CEDAR_STATEMENT="permit ( principal, action == ApiAccess::Action::\"$METHOD\", resource @@ -22,8 +32,8 @@ when { resource.host == \"$SCOPE_DOMAIN\" && resource.path == \"$ROUTE_PATH\" };" -else - CEDAR_STATEMENT="permit ( + else + CEDAR_STATEMENT="permit ( principal, action == ApiAccess::Action::\"$METHOD\", resource @@ -33,25 +43,27 @@ when { resource.host == \"$SCOPE_DOMAIN\" && resource.path == \"$ROUTE_PATH\" };" -fi + fi -DESCRIPTION="endpoint-exposer|service-id=$SERVICE_ID|link=$HTTPROUTE_NAME|route=$METHOD:$ROUTE_PATH|scope=$SCOPE_ID" + DESCRIPTION="endpoint-exposer|service-id=$SERVICE_ID|link=$HTTPROUTE_NAME|route=$METHOD:$ROUTE_PATH|scope=$SCOPE_ID" + DEFINITION=$(jq -n --arg desc "$DESCRIPTION" --arg stmt "$CEDAR_STATEMENT" \ + '{"static": {"description": $desc, "statement": $stmt}}') -DEFINITION=$(jq -n \ - --arg desc "$DESCRIPTION" \ - --arg stmt "$CEDAR_STATEMENT" \ - '{"static": {"description": $desc, "statement": $stmt}}') + POLICY_ID=$(aws verifiedpermissions create-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --definition "$DEFINITION" \ + --query 'policyId' --output text) || { echo "ERROR: Failed to create Cedar policy for $METHOD $ROUTE_PATH"; exit 1; } -POLICY_ID=$(aws verifiedpermissions create-policy \ - --policy-store-id "$AVP_POLICY_STORE_ID" \ - --region "$AVP_AWS_REGION" \ - --definition "$DEFINITION" \ - --query 'policyId' --output text) || { echo "ERROR: Failed to create Cedar policy"; exit 1; } + if [[ -z "$POLICY_ID" || "$POLICY_ID" == "None" ]]; then + echo "ERROR: Policy creation returned no policyId for $METHOD $ROUTE_PATH" + exit 1 + fi -if [[ -z "$POLICY_ID" || "$POLICY_ID" == "None" ]]; then - echo "ERROR: Policy creation returned no policyId" - exit 1 -fi + echo "✅ Cedar policy created: $METHOD $ROUTE_PATH @ $SCOPE_DOMAIN (policy-id: $POLICY_ID)" + POLICY_IDS_LIST+=("\"$POLICY_ID\"") +done -echo "✅ Cedar policy created: $METHOD $ROUTE_PATH @ $SCOPE_DOMAIN (groups: $NUM_GROUPS, policy-id: $POLICY_ID)" -export POLICY_ID +POLICY_IDS="[$(IFS=,; echo "${POLICY_IDS_LIST[*]}")]" +echo "✅ All Cedar policies created: $POLICY_IDS" +export POLICY_IDS diff --git a/endpoint-exposer/scripts/avp/delete_link_policy b/endpoint-exposer/scripts/avp/delete_link_policy index 22d150a..d7b63c2 100644 --- a/endpoint-exposer/scripts/avp/delete_link_policy +++ b/endpoint-exposer/scripts/avp/delete_link_policy @@ -4,17 +4,22 @@ set -euo pipefail source "$SERVICE_PATH/scripts/avp/config" -POLICY_ID="${ACTION_LINK_ATTRIBUTES_POLICY_ID:-}" +POLICY_IDS="${ACTION_LINK_ATTRIBUTES_POLICY_IDS:-}" -if [[ -z "$POLICY_ID" || "$POLICY_ID" == "null" ]]; then - echo "WARNING: No policy_id found in link attributes, skipping Cedar policy deletion" +if [[ -z "$POLICY_IDS" || "$POLICY_IDS" == "null" || "$POLICY_IDS" == "[]" ]]; then + echo "WARNING: No policy_ids found in link attributes, skipping Cedar policy deletion" exit 0 fi -echo "Deleting Cedar policy: $POLICY_ID" -aws verifiedpermissions delete-policy \ - --policy-store-id "$AVP_POLICY_STORE_ID" \ - --region "$AVP_AWS_REGION" \ - --policy-id "$POLICY_ID" || { echo "WARNING: Failed to delete policy $POLICY_ID (may already be deleted)"; } +POLICY_COUNT=$(echo "$POLICY_IDS" | jq 'length') +echo "=== Deleting $POLICY_COUNT AVP Cedar Policies ===" -echo "✅ Cedar policy $POLICY_ID deleted" +for i in $(seq 0 $((POLICY_COUNT - 1))); do + POLICY_ID=$(echo "$POLICY_IDS" | jq -r ".[$i]") + echo "Deleting Cedar policy: $POLICY_ID" + aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete policy $POLICY_ID (may already be deleted)" + echo "✅ Cedar policy $POLICY_ID deleted" +done diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context index 6f57f82..f619b9c 100644 --- a/endpoint-exposer/scripts/istio/build_link_context +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -13,9 +13,7 @@ SERVICE_SLUG=$(echo "$CONTEXT" | jq -r '.service.slug') ACTION_ID=$(echo "$CONTEXT" | jq -r '.id') APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') -METHOD=$(echo "$CONTEXT" | jq -r '.parameters.method') -ROUTE_PATH=$(echo "$CONTEXT" | jq -r '.parameters.path') -VISIBILITY=$(echo "$CONTEXT" | jq -r '.parameters.visibility // "public"') +ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') # Infer scope from link/service dimensions — avoids requiring user input LINK_DIMENSIONS=$(echo "$CONTEXT" | jq -c '.service.dimensions // {}') @@ -29,9 +27,8 @@ if [[ -z "$SCOPE" ]]; then exit 1 fi echo "Inferred scope: $SCOPE" -GROUPS_JSON=$(echo "$CONTEXT" | jq -c '.parameters.groups // []') -# Unique HTTPRoute name per link — stored in link attributes for use on delete +# Unique HTTPRoute base name per link — stored in link attributes for use on delete HTTPROUTE_NAME="ee-link-${ACTION_ID}" K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" @@ -45,10 +42,7 @@ export SERVICE_ID export SERVICE_SLUG export ACTION_ID export APPLICATION_ID -export METHOD -export ROUTE_PATH +export ROUTES_JSON export SCOPE -export VISIBILITY -export GROUPS_JSON export HTTPROUTE_NAME export K8S_NAMESPACE diff --git a/endpoint-exposer/scripts/istio/build_link_httproute b/endpoint-exposer/scripts/istio/build_link_httproute index 16a4d1a..996a686 100644 --- a/endpoint-exposer/scripts/istio/build_link_httproute +++ b/endpoint-exposer/scripts/istio/build_link_httproute @@ -5,48 +5,43 @@ set -euo pipefail source "$SERVICE_PATH/scripts/istio/config" source "$SERVICE_PATH/scripts/avp/config" -echo "=== Building HTTPRoute for link: $METHOD $ROUTE_PATH (scope: $SCOPE, visibility: $VISIBILITY) ===" - -GATEWAY_NAME="$PUBLIC_GATEWAY_NAME" -if [[ "$VISIBILITY" == "private" ]]; then - GATEWAY_NAME="$PRIVATE_GATEWAY_NAME" -fi +echo "=== Building HTTPRoutes for link (scope: $SCOPE) ===" +# Resolve scope → ID and domain (shared by all routes in this link) SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') - SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE" '.[] | select(.name == $name) | .id') if [[ -z "$SCOPE_ID" || "$SCOPE_ID" == "null" ]]; then echo "ERROR: Could not resolve scope '$SCOPE'" exit 1 fi - SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') -if [[ -z "$SCOPE_DOMAIN" || "$SCOPE_DOMAIN" == "null" ]]; then - echo "ERROR: Scope '$SCOPE' (id: $SCOPE_ID) has no domain" +if [[ -z "$SCOPE_DOMAIN" ]]; then + echo "ERROR: Scope '$SCOPE' has no domain" exit 1 fi - echo "Resolved scope '$SCOPE' → domain: $SCOPE_DOMAIN" -# Detect path match type -if echo "$ROUTE_PATH" | grep -q '\*'; then - PATH_TYPE="PathPrefix" - CONVERTED_PATH=$(echo "$ROUTE_PATH" | sed 's/\*.*//') -elif echo "$ROUTE_PATH" | grep -q ':'; then - PATH_TYPE="RegularExpression" - CONVERTED_PATH=$(echo "$ROUTE_PATH" | sed 's/:[^/]*/[^\/]+/g') -else - PATH_TYPE="Exact" - CONVERTED_PATH="$ROUTE_PATH" -fi +# Split routes by visibility and inject scope for process_routes +PUBLIC_ROUTES=$(echo "$ROUTES_JSON" | jq -c --arg s "$SCOPE" \ + '[.[] | select((.visibility // "public") == "public") | . + {scope: $s}]') +PRIVATE_ROUTES=$(echo "$ROUTES_JSON" | jq -c --arg s "$SCOPE" \ + '[.[] | select(.visibility == "private") | . + {scope: $s}]') +NUM_PUBLIC=$(echo "$PUBLIC_ROUTES" | jq 'length') +NUM_PRIVATE=$(echo "$PRIVATE_ROUTES" | jq 'length') +echo "Routes: $NUM_PUBLIC public, $NUM_PRIVATE private" -HTTPROUTE_FILE="$OUTPUT_DIR/httproute-${HTTPROUTE_NAME}.yaml" +# Creates an empty HTTPRoute YAML file and processes all routes into it +build_httproute() { + local name=$1 + local gateway=$2 + local routes=$3 + local file="$OUTPUT_DIR/httproute-${name}.yaml" -cat > "$HTTPROUTE_FILE" < "$file" </dev/null; then - kubectl delete httproute "$HTTPROUTE_NAME" -n "$K8S_NAMESPACE" - echo "✅ HTTPRoute $HTTPROUTE_NAME deleted" -else - echo "HTTPRoute $HTTPROUTE_NAME not found (already deleted or never existed)" -fi +echo "=== Deleting HTTPRoutes for link: $BASE_NAME ===" +for suffix in public private; do + NAME="${BASE_NAME}-${suffix}" + if kubectl get httproute "$NAME" -n "$K8S_NAMESPACE" &>/dev/null; then + kubectl delete httproute "$NAME" -n "$K8S_NAMESPACE" + echo "✅ HTTPRoute $NAME deleted" + else + echo "HTTPRoute $NAME not found (skipping)" + fi +done diff --git a/endpoint-exposer/scripts/np/update_link_results b/endpoint-exposer/scripts/np/update_link_results index d10efce..3d67bec 100644 --- a/endpoint-exposer/scripts/np/update_link_results +++ b/endpoint-exposer/scripts/np/update_link_results @@ -6,14 +6,14 @@ CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" RESULTS=$(echo "$CONTEXT" | jq \ --arg httproute_name "$HTTPROUTE_NAME" \ - --arg policy_id "$POLICY_ID" \ + --argjson policy_ids "$POLICY_IDS" \ --arg scope "$SCOPE" \ '(.notification.parameters // .parameters // {}) + { httproute_name: $httproute_name, - policy_id: $policy_id, + policy_ids: $policy_ids, scope: $scope }') -echo "Storing link results: httproute_name=$HTTPROUTE_NAME, policy_id=$POLICY_ID, scope=$SCOPE" +echo "Storing link results: httproute_name=$HTTPROUTE_NAME, scope=$SCOPE, policy_ids=$POLICY_IDS" np link action update --results "$RESULTS" echo "✅ Link results stored" diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 6fff34a..86f79e2 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -7,39 +7,80 @@ "attributes": { "schema": { "type": "object", - "required": ["method", "path"], + "required": ["routes"], "uiSchema": { "type": "VerticalLayout", "elements": [ - { - "type": "HorizontalLayout", - "elements": [ - {"type": "Control", "label": "HTTP Method", "scope": "#/properties/method"}, - {"type": "Control", "label": "Visibility", "scope": "#/properties/visibility"} - ] - }, - {"type": "Control", "label": "Path", "scope": "#/properties/path"}, { "type": "Control", - "label": "Authorized Groups", - "scope": "#/properties/groups", + "scope": "#/properties/routes", "options": { - "orientation": "vertical" + "showSortButtons": true, + "detail": { + "type": "VerticalLayout", + "elements": [ + { + "type": "HorizontalLayout", + "elements": [ + {"type": "Control", "label": "HTTP Method", "scope": "#/properties/method"}, + {"type": "Control", "label": "Visibility", "scope": "#/properties/visibility"} + ] + }, + {"type": "Control", "label": "Path", "scope": "#/properties/path"}, + { + "type": "Control", + "label": "Authorized Groups", + "scope": "#/properties/groups", + "options": {"orientation": "vertical"} + } + ] + } } } ] }, "properties": { - "method": { - "type": "string", - "title": "HTTP Method", + "routes": { + "type": "array", + "title": "Routes", "editableOn": ["create"], - "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] - }, - "path": { - "type": "string", - "title": "Path", - "editableOn": ["create"] + "items": { + "type": "object", + "required": ["method", "path"], + "properties": { + "method": { + "type": "string", + "title": "HTTP Method", + "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] + }, + "path": { + "type": "string", + "title": "Path" + }, + "visibility": { + "type": "string", + "title": "Visibility", + "default": "public", + "enum": ["public", "private"] + }, + "groups": { + "type": "array", + "title": "Authorized Groups", + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "AWS_PlataformaUpstream_Gestor_Desa", + "AWS_PlataformaUpstream_Programador_Desa", + "AWS_PlataformaUpstream_Pulling_Desa", + "AWS_PlataformaUpstream_Workover_Desa", + "AWS_PlataformaUpstream_Visita_Desa", + "AWS_PlataformaUpstream_Administrador_Desa" + ] + } + } + } + } }, "scope": { "type": "string", @@ -47,39 +88,15 @@ "editableOn": [], "visibleOn": ["read"] }, - "visibility": { - "type": "string", - "title": "Visibility", - "default": "public", - "editableOn": ["create"], - "enum": ["public", "private"] - }, - "groups": { - "type": "array", - "title": "Authorized Groups", - "uniqueItems": true, - "editableOn": ["create"], - "items": { - "type": "string", - "enum": [ - "AWS_PlataformaUpstream_Gestor_Desa", - "AWS_PlataformaUpstream_Programador_Desa", - "AWS_PlataformaUpstream_Pulling_Desa", - "AWS_PlataformaUpstream_Workover_Desa", - "AWS_PlataformaUpstream_Visita_Desa", - "AWS_PlataformaUpstream_Administrador_Desa" - ] - } - }, "httproute_name": { "type": "string", "title": "HTTPRoute Name", "editableOn": [], "visibleOn": ["read"] }, - "policy_id": { + "policy_ids": { "type": "string", - "title": "AVP Policy ID", + "title": "AVP Policy IDs", "editableOn": [], "visibleOn": ["read"] } diff --git a/endpoint-exposer/workflows/istio/link.yaml b/endpoint-exposer/workflows/istio/link.yaml index b8f525b..96a14ed 100644 --- a/endpoint-exposer/workflows/istio/link.yaml +++ b/endpoint-exposer/workflows/istio/link.yaml @@ -17,16 +17,10 @@ steps: type: environment - name: APPLICATION_ID type: environment - - name: METHOD - type: environment - - name: ROUTE_PATH + - name: ROUTES_JSON type: environment - name: SCOPE type: environment - - name: VISIBILITY - type: environment - - name: GROUPS_JSON - type: environment - name: HTTPROUTE_NAME type: environment - name: K8S_NAMESPACE @@ -43,7 +37,7 @@ steps: type: script file: "$SERVICE_PATH/scripts/avp/create_link_policy" output: - - name: POLICY_ID + - name: POLICY_IDS type: environment - name: "apply" type: script From 6d485dc26c56f18d1d34cc1464212906b2188e82 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 21:24:18 -0300 Subject: [PATCH 075/101] fix(endpoint-exposer): shorten AVP policy description to fit 150-char limit Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/create_link_policy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/avp/create_link_policy b/endpoint-exposer/scripts/avp/create_link_policy index 09398c1..e8eac93 100644 --- a/endpoint-exposer/scripts/avp/create_link_policy +++ b/endpoint-exposer/scripts/avp/create_link_policy @@ -45,7 +45,7 @@ when { };" fi - DESCRIPTION="endpoint-exposer|service-id=$SERVICE_ID|link=$HTTPROUTE_NAME|route=$METHOD:$ROUTE_PATH|scope=$SCOPE_ID" + DESCRIPTION="ee|link=$HTTPROUTE_NAME|route=$METHOD:$ROUTE_PATH|scope=$SCOPE_ID" DEFINITION=$(jq -n --arg desc "$DESCRIPTION" --arg stmt "$CEDAR_STATEMENT" \ '{"static": {"description": $desc, "statement": $stmt}}') From 55a2450bf21c29895889e2cb323cb147596c3ba2 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 21:52:39 -0300 Subject: [PATCH 076/101] feat(link-spec): allow editing routes after link creation Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 86f79e2..9f69a3e 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -43,7 +43,7 @@ "routes": { "type": "array", "title": "Routes", - "editableOn": ["create"], + "editableOn": ["create", "edit"], "items": { "type": "object", "required": ["method", "path"], From 6ca2d8370c2b7f2a8d94c749e54cbf1f4332d739 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 21:57:41 -0300 Subject: [PATCH 077/101] feat(link-spec): make routes editable after link creation Add description field to routes to force provider diff detection. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 1 + 1 file changed, 1 insertion(+) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index 9f69a3e..e624e18 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -43,6 +43,7 @@ "routes": { "type": "array", "title": "Routes", + "description": "HTTP routes to expose", "editableOn": ["create", "edit"], "items": { "type": "object", From 4c63f83d7d28009f7edb8eb54ee0dcea54ae92b8 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 22:03:41 -0300 Subject: [PATCH 078/101] fix(link-spec): use "update" instead of "edit" for editableOn Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/links/route.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/links/route.json.tpl b/endpoint-exposer/specs/links/route.json.tpl index e624e18..496cc51 100644 --- a/endpoint-exposer/specs/links/route.json.tpl +++ b/endpoint-exposer/specs/links/route.json.tpl @@ -44,7 +44,7 @@ "type": "array", "title": "Routes", "description": "HTTP routes to expose", - "editableOn": ["create", "edit"], + "editableOn": ["create", "update"], "items": { "type": "object", "required": ["method", "path"], From ec44daac633f1aab82b0322118092b946f3b7b3b Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 22:10:15 -0300 Subject: [PATCH 079/101] feat(link): add link-update workflow with proper scope and HTTPRoute reuse - Entrypoint: map update action type to link-update workflow - build_link_context: reuse stored HTTPROUTE_NAME and SCOPE on update - link-update.yaml: delete old policies, rebuild HTTPRoute, create new policies Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/entrypoint/link | 3 ++ .../scripts/istio/build_link_context | 33 +++++++----- .../workflows/istio/link-update.yaml | 53 +++++++++++++++++++ 3 files changed, 75 insertions(+), 14 deletions(-) create mode 100644 endpoint-exposer/workflows/istio/link-update.yaml diff --git a/endpoint-exposer/entrypoint/link b/endpoint-exposer/entrypoint/link index 1f40cf6..ad323c1 100755 --- a/endpoint-exposer/entrypoint/link +++ b/endpoint-exposer/entrypoint/link @@ -11,6 +11,9 @@ case "$SERVICE_ACTION_TYPE" in "create") ACTION_TO_EXECUTE="link" ;; + "update") + ACTION_TO_EXECUTE="link-update" + ;; "delete") ACTION_TO_EXECUTE="unlink" ;; diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context index f619b9c..10f277e 100644 --- a/endpoint-exposer/scripts/istio/build_link_context +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -15,21 +15,26 @@ APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') -# Infer scope from link/service dimensions — avoids requiring user input -LINK_DIMENSIONS=$(echo "$CONTEXT" | jq -c '.service.dimensions // {}') -echo "Inferring scope from dimensions: $LINK_DIMENSIONS" -SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') -SCOPE=$(echo "$SCOPES_JSON" | jq -r \ - --argjson dims "$LINK_DIMENSIONS" \ - '[.[] | select(.name | startswith("deleted-") | not) | select(.dimensions == $dims)] | .[0].name // empty') -if [[ -z "$SCOPE" ]]; then - echo "ERROR: Could not infer scope from dimensions: $LINK_DIMENSIONS" - exit 1 +# Reuse stored HTTPROUTE_NAME on update, generate new one on create +HTTPROUTE_NAME="${ACTION_LINK_ATTRIBUTES_HTTPROUTE_NAME:-ee-link-${ACTION_ID}}" + +# Reuse stored scope on update, infer from dimensions on create +SCOPE="${ACTION_LINK_ATTRIBUTES_SCOPE:-}" +if [[ -z "$SCOPE" || "$SCOPE" == "null" ]]; then + LINK_DIMENSIONS=$(echo "$CONTEXT" | jq -c '.service.dimensions // {}') + echo "Inferring scope from dimensions: $LINK_DIMENSIONS" + SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') + SCOPE=$(echo "$SCOPES_JSON" | jq -r \ + --argjson dims "$LINK_DIMENSIONS" \ + '[.[] | select(.name | startswith("deleted-") | not) | select(.dimensions == $dims)] | .[0].name // empty') + if [[ -z "$SCOPE" ]]; then + echo "ERROR: Could not infer scope from dimensions: $LINK_DIMENSIONS" + exit 1 + fi + echo "Inferred scope: $SCOPE" +else + echo "Reusing stored scope: $SCOPE" fi -echo "Inferred scope: $SCOPE" - -# Unique HTTPRoute base name per link — stored in link attributes for use on delete -HTTPROUTE_NAME="ee-link-${ACTION_ID}" K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" diff --git a/endpoint-exposer/workflows/istio/link-update.yaml b/endpoint-exposer/workflows/istio/link-update.yaml new file mode 100644 index 0000000..73eb508 --- /dev/null +++ b/endpoint-exposer/workflows/istio/link-update.yaml @@ -0,0 +1,53 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_link_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_SLUG + type: environment + - name: ACTION_ID + type: environment + - name: APPLICATION_ID + type: environment + - name: ROUTES_JSON + type: environment + - name: SCOPE + type: environment + - name: HTTPROUTE_NAME + type: environment + - name: K8S_NAMESPACE + type: environment + - name: "delete avp policies" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_link_policy" + - name: "build httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/build_link_httproute" + output: + - name: SCOPE_ID + type: environment + - name: SCOPE_DOMAIN + type: environment + - name: "create avp policy" + type: script + file: "$SERVICE_PATH/scripts/avp/create_link_policy" + output: + - name: POLICY_IDS + type: environment + - name: "apply" + type: script + file: "$SERVICE_PATH/scripts/common/apply" + configuration: + ACTION: apply + DRY_RUN: false + - name: "store link results" + type: script + file: "$SERVICE_PATH/scripts/np/update_link_results" From 87df4292f22bc886d0823c6edd6654dc6c9983da Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 22:16:01 -0300 Subject: [PATCH 080/101] fix(link-update): use LINK_ID for stable HTTPRoute name across updates - build_link_context: derive HTTPROUTE_NAME from link.id (stable) instead of action.id (changes on every update) - delete_link_policies_by_name: search AVP by HTTPROUTE_NAME in description instead of relying on stored policy_ids - link-update.yaml: use delete_link_policies_by_name for cleanup Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/avp/delete_link_policies_by_name | 33 ++++++++++++++++++ .../scripts/istio/build_link_context | 34 ++++++++----------- .../workflows/istio/link-update.yaml | 2 +- 3 files changed, 49 insertions(+), 20 deletions(-) create mode 100755 endpoint-exposer/scripts/avp/delete_link_policies_by_name diff --git a/endpoint-exposer/scripts/avp/delete_link_policies_by_name b/endpoint-exposer/scripts/avp/delete_link_policies_by_name new file mode 100755 index 0000000..1fe6b33 --- /dev/null +++ b/endpoint-exposer/scripts/avp/delete_link_policies_by_name @@ -0,0 +1,33 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +if [[ -z "${HTTPROUTE_NAME:-}" ]]; then + echo "WARNING: HTTPROUTE_NAME not set, skipping Cedar policy cleanup" + exit 0 +fi + +echo "=== Deleting AVP Cedar Policies for HTTPRoute: $HTTPROUTE_NAME ===" + +POLICY_IDS=$(aws verifiedpermissions list-policies \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --output json | jq -r \ + --arg name "$HTTPROUTE_NAME" \ + '[.policies[] | select(.definition.static.description | test("link=" + $name)) | .policyId] | .[]') + +if [[ -z "$POLICY_IDS" ]]; then + echo "No existing Cedar policies found for $HTTPROUTE_NAME" + exit 0 +fi + +for POLICY_ID in $POLICY_IDS; do + echo "Deleting Cedar policy: $POLICY_ID" + aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete $POLICY_ID (may already be deleted)" + echo "✅ Cedar policy $POLICY_ID deleted" +done diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context index 10f277e..f242d46 100644 --- a/endpoint-exposer/scripts/istio/build_link_context +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -11,30 +11,26 @@ fi SERVICE_ID=$(echo "$CONTEXT" | jq -r '.service.id') SERVICE_SLUG=$(echo "$CONTEXT" | jq -r '.service.slug') ACTION_ID=$(echo "$CONTEXT" | jq -r '.id') +LINK_ID=$(echo "$CONTEXT" | jq -r '.link.id') APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') -# Reuse stored HTTPROUTE_NAME on update, generate new one on create -HTTPROUTE_NAME="${ACTION_LINK_ATTRIBUTES_HTTPROUTE_NAME:-ee-link-${ACTION_ID}}" - -# Reuse stored scope on update, infer from dimensions on create -SCOPE="${ACTION_LINK_ATTRIBUTES_SCOPE:-}" -if [[ -z "$SCOPE" || "$SCOPE" == "null" ]]; then - LINK_DIMENSIONS=$(echo "$CONTEXT" | jq -c '.service.dimensions // {}') - echo "Inferring scope from dimensions: $LINK_DIMENSIONS" - SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') - SCOPE=$(echo "$SCOPES_JSON" | jq -r \ - --argjson dims "$LINK_DIMENSIONS" \ - '[.[] | select(.name | startswith("deleted-") | not) | select(.dimensions == $dims)] | .[0].name // empty') - if [[ -z "$SCOPE" ]]; then - echo "ERROR: Could not infer scope from dimensions: $LINK_DIMENSIONS" - exit 1 - fi - echo "Inferred scope: $SCOPE" -else - echo "Reusing stored scope: $SCOPE" +# Use LINK_ID (stable across create/update) so HTTPRoute name never changes +HTTPROUTE_NAME="ee-link-${LINK_ID}" + +# Infer scope from dimensions — available in both create and update contexts +LINK_DIMENSIONS=$(echo "$CONTEXT" | jq -c '.service.dimensions // {}') +echo "Inferring scope from dimensions: $LINK_DIMENSIONS" +SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') +SCOPE=$(echo "$SCOPES_JSON" | jq -r \ + --argjson dims "$LINK_DIMENSIONS" \ + '[.[] | select(.name | startswith("deleted-") | not) | select(.dimensions == $dims)] | .[0].name // empty') +if [[ -z "$SCOPE" ]]; then + echo "ERROR: Could not infer scope from dimensions: $LINK_DIMENSIONS" + exit 1 fi +echo "Inferred scope: $SCOPE" K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" diff --git a/endpoint-exposer/workflows/istio/link-update.yaml b/endpoint-exposer/workflows/istio/link-update.yaml index 73eb508..7cc061a 100644 --- a/endpoint-exposer/workflows/istio/link-update.yaml +++ b/endpoint-exposer/workflows/istio/link-update.yaml @@ -27,7 +27,7 @@ steps: type: environment - name: "delete avp policies" type: script - file: "$SERVICE_PATH/scripts/avp/delete_link_policy" + file: "$SERVICE_PATH/scripts/avp/delete_link_policies_by_name" - name: "build httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_link_httproute" From 876c5ad86c72b80b2b6786de45a810f6280afc3f Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 22:18:31 -0300 Subject: [PATCH 081/101] fix(link-update): add output to delete step so workflow runner continues np service workflow exec treats intermediate steps without output or configuration as terminal. Adding POLICIES_DELETED output keeps the pipeline flowing to build httproute and create avp policy. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/delete_link_policies_by_name | 2 ++ endpoint-exposer/workflows/istio/link-update.yaml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/endpoint-exposer/scripts/avp/delete_link_policies_by_name b/endpoint-exposer/scripts/avp/delete_link_policies_by_name index 1fe6b33..35f78bf 100755 --- a/endpoint-exposer/scripts/avp/delete_link_policies_by_name +++ b/endpoint-exposer/scripts/avp/delete_link_policies_by_name @@ -31,3 +31,5 @@ for POLICY_ID in $POLICY_IDS; do --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete $POLICY_ID (may already be deleted)" echo "✅ Cedar policy $POLICY_ID deleted" done + +export POLICIES_DELETED="true" diff --git a/endpoint-exposer/workflows/istio/link-update.yaml b/endpoint-exposer/workflows/istio/link-update.yaml index 7cc061a..28b3e02 100644 --- a/endpoint-exposer/workflows/istio/link-update.yaml +++ b/endpoint-exposer/workflows/istio/link-update.yaml @@ -28,6 +28,9 @@ steps: - name: "delete avp policies" type: script file: "$SERVICE_PATH/scripts/avp/delete_link_policies_by_name" + output: + - name: POLICIES_DELETED + type: environment - name: "build httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_link_httproute" From da397b4ffe868ac632b172d49a5cdb28aa9cf334 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 22:21:38 -0300 Subject: [PATCH 082/101] fix(avp): replace exit 0 with conditional flow in delete scripts When np service workflow exec sources scripts, exit 0 kills the parent workflow process and skips remaining steps. Replace early exit 0 paths with if/else blocks so the workflow continues normally. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/avp/delete_link_policies_by_name | 47 ++++++++++--------- .../scripts/avp/delete_link_policy | 27 +++++------ 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/endpoint-exposer/scripts/avp/delete_link_policies_by_name b/endpoint-exposer/scripts/avp/delete_link_policies_by_name index 35f78bf..5d8e724 100755 --- a/endpoint-exposer/scripts/avp/delete_link_policies_by_name +++ b/endpoint-exposer/scripts/avp/delete_link_policies_by_name @@ -4,32 +4,33 @@ set -euo pipefail source "$SERVICE_PATH/scripts/avp/config" +export POLICIES_DELETED="false" + if [[ -z "${HTTPROUTE_NAME:-}" ]]; then echo "WARNING: HTTPROUTE_NAME not set, skipping Cedar policy cleanup" - exit 0 -fi - -echo "=== Deleting AVP Cedar Policies for HTTPRoute: $HTTPROUTE_NAME ===" - -POLICY_IDS=$(aws verifiedpermissions list-policies \ - --policy-store-id "$AVP_POLICY_STORE_ID" \ - --region "$AVP_AWS_REGION" \ - --output json | jq -r \ - --arg name "$HTTPROUTE_NAME" \ - '[.policies[] | select(.definition.static.description | test("link=" + $name)) | .policyId] | .[]') +else + echo "=== Deleting AVP Cedar Policies for HTTPRoute: $HTTPROUTE_NAME ===" -if [[ -z "$POLICY_IDS" ]]; then - echo "No existing Cedar policies found for $HTTPROUTE_NAME" - exit 0 -fi - -for POLICY_ID in $POLICY_IDS; do - echo "Deleting Cedar policy: $POLICY_ID" - aws verifiedpermissions delete-policy \ + POLICY_IDS=$(aws verifiedpermissions list-policies \ --policy-store-id "$AVP_POLICY_STORE_ID" \ --region "$AVP_AWS_REGION" \ - --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete $POLICY_ID (may already be deleted)" - echo "✅ Cedar policy $POLICY_ID deleted" -done + --output json | jq -r \ + --arg name "$HTTPROUTE_NAME" \ + '[.policies[] | select(.definition.static.description | test("link=" + $name)) | .policyId] | .[]') + + if [[ -z "$POLICY_IDS" ]]; then + echo "No existing Cedar policies found for $HTTPROUTE_NAME" + else + for POLICY_ID in $POLICY_IDS; do + echo "Deleting Cedar policy: $POLICY_ID" + aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete $POLICY_ID (may already be deleted)" + echo "✅ Cedar policy $POLICY_ID deleted" + done + POLICIES_DELETED="true" + fi +fi -export POLICIES_DELETED="true" +export POLICIES_DELETED diff --git a/endpoint-exposer/scripts/avp/delete_link_policy b/endpoint-exposer/scripts/avp/delete_link_policy index d7b63c2..530917c 100644 --- a/endpoint-exposer/scripts/avp/delete_link_policy +++ b/endpoint-exposer/scripts/avp/delete_link_policy @@ -8,18 +8,17 @@ POLICY_IDS="${ACTION_LINK_ATTRIBUTES_POLICY_IDS:-}" if [[ -z "$POLICY_IDS" || "$POLICY_IDS" == "null" || "$POLICY_IDS" == "[]" ]]; then echo "WARNING: No policy_ids found in link attributes, skipping Cedar policy deletion" - exit 0 -fi - -POLICY_COUNT=$(echo "$POLICY_IDS" | jq 'length') -echo "=== Deleting $POLICY_COUNT AVP Cedar Policies ===" +else + POLICY_COUNT=$(echo "$POLICY_IDS" | jq 'length') + echo "=== Deleting $POLICY_COUNT AVP Cedar Policies ===" -for i in $(seq 0 $((POLICY_COUNT - 1))); do - POLICY_ID=$(echo "$POLICY_IDS" | jq -r ".[$i]") - echo "Deleting Cedar policy: $POLICY_ID" - aws verifiedpermissions delete-policy \ - --policy-store-id "$AVP_POLICY_STORE_ID" \ - --region "$AVP_AWS_REGION" \ - --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete policy $POLICY_ID (may already be deleted)" - echo "✅ Cedar policy $POLICY_ID deleted" -done + for i in $(seq 0 $((POLICY_COUNT - 1))); do + POLICY_ID=$(echo "$POLICY_IDS" | jq -r ".[$i]") + echo "Deleting Cedar policy: $POLICY_ID" + aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" || echo "WARNING: Failed to delete policy $POLICY_ID (may already be deleted)" + echo "✅ Cedar policy $POLICY_ID deleted" + done +fi From afb62948106a232a8aed7e5d78b5360b58729aba Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 23:29:10 -0300 Subject: [PATCH 083/101] feat: add avp_policy_store_id param to service spec with auto-creation Adds avp_policy_store_id as an optional service parameter. If left empty during creation, a new AVP policy store is automatically provisioned and its ID is stored in service results for subsequent actions. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/avp/ensure_policy_store | 26 +++++++++++++++++++ .../scripts/istio/build_link_context | 3 +++ .../scripts/np/update_service_results | 15 +++++++++++ endpoint-exposer/specs/service-spec.json.tpl | 17 ++++++++++-- endpoint-exposer/workflows/istio/create.yaml | 9 +++++++ .../workflows/istio/link-update.yaml | 2 ++ endpoint-exposer/workflows/istio/link.yaml | 2 ++ endpoint-exposer/workflows/istio/update.yaml | 3 +++ 8 files changed, 75 insertions(+), 2 deletions(-) create mode 100755 endpoint-exposer/scripts/avp/ensure_policy_store create mode 100755 endpoint-exposer/scripts/np/update_service_results diff --git a/endpoint-exposer/scripts/avp/ensure_policy_store b/endpoint-exposer/scripts/avp/ensure_policy_store new file mode 100755 index 0000000..5afca69 --- /dev/null +++ b/endpoint-exposer/scripts/avp/ensure_policy_store @@ -0,0 +1,26 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" + +# Check user-provided value first, then stored results from previous action +CONFIGURED_ID=$(echo "$CONTEXT" | jq -r ' + .parameters.avp_policy_store_id // + .notification.parameters.avp_policy_store_id // + ""') + +if [[ -n "$CONFIGURED_ID" ]]; then + echo "Using configured AVP policy store: $CONFIGURED_ID" + export AVP_POLICY_STORE_ID="$CONFIGURED_ID" +else + echo "No AVP policy store configured, creating a new one..." + CREATED_ID=$(aws verifiedpermissions create-policy-store \ + --validation-settings '{"mode": "OFF"}' \ + --region "$AVP_AWS_REGION" \ + --query 'policyStoreId' --output text) + echo "✅ Created new AVP policy store: $CREATED_ID" + export AVP_POLICY_STORE_ID="$CREATED_ID" +fi diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context index f242d46..322f7f1 100644 --- a/endpoint-exposer/scripts/istio/build_link_context +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -32,6 +32,8 @@ if [[ -z "$SCOPE" ]]; then fi echo "Inferred scope: $SCOPE" +AVP_POLICY_STORE_ID=$(echo "$CONTEXT" | jq -r '.service.parameters.avp_policy_store_id // .notification.parameters.avp_policy_store_id // ""') + K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" if [[ -z "${OUTPUT_DIR:-}" ]]; then @@ -47,3 +49,4 @@ export ROUTES_JSON export SCOPE export HTTPROUTE_NAME export K8S_NAMESPACE +export AVP_POLICY_STORE_ID diff --git a/endpoint-exposer/scripts/np/update_service_results b/endpoint-exposer/scripts/np/update_service_results new file mode 100755 index 0000000..d3f8a6f --- /dev/null +++ b/endpoint-exposer/scripts/np/update_service_results @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail + +CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" + +RESULTS=$(echo "$CONTEXT" | jq \ + --arg avp_policy_store_id "$AVP_POLICY_STORE_ID" \ + '(.notification.parameters // .parameters // {}) + { + avp_policy_store_id: $avp_policy_store_id + }') + +echo "Storing service results: avp_policy_store_id=$AVP_POLICY_STORE_ID" +np service action update --results "$RESULTS" +echo "✅ Service results stored" diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index ca0229b..b21df0e 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -7,9 +7,22 @@ "required": [], "uiSchema": { "type": "VerticalLayout", - "elements": [] + "elements": [ + { + "type": "Control", + "label": "AVP Policy Store ID", + "scope": "#/properties/avp_policy_store_id" + } + ] }, - "properties": {} + "properties": { + "avp_policy_store_id": { + "type": "string", + "title": "AVP Policy Store ID", + "description": "ID del Policy Store de Amazon Verified Permissions. Si se deja vacío, se creará un policy store de AVP por defecto.", + "editableOn": ["create"] + } + } }, "values": {} }, diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index e808eb5..5695721 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -27,6 +27,12 @@ steps: type: environment - name: APPLICATION_ID type: environment + - name: "ensure avp policy store" + type: script + file: "$SERVICE_PATH/scripts/avp/ensure_policy_store" + output: + - name: AVP_POLICY_STORE_ID + type: environment - name: "build public httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_httproute" @@ -54,3 +60,6 @@ steps: configuration: ACTION: apply DRY_RUN: false + - name: "store service results" + type: script + file: "$SERVICE_PATH/scripts/np/update_service_results" diff --git a/endpoint-exposer/workflows/istio/link-update.yaml b/endpoint-exposer/workflows/istio/link-update.yaml index 28b3e02..c9dd935 100644 --- a/endpoint-exposer/workflows/istio/link-update.yaml +++ b/endpoint-exposer/workflows/istio/link-update.yaml @@ -25,6 +25,8 @@ steps: type: environment - name: K8S_NAMESPACE type: environment + - name: AVP_POLICY_STORE_ID + type: environment - name: "delete avp policies" type: script file: "$SERVICE_PATH/scripts/avp/delete_link_policies_by_name" diff --git a/endpoint-exposer/workflows/istio/link.yaml b/endpoint-exposer/workflows/istio/link.yaml index 96a14ed..ef11d8a 100644 --- a/endpoint-exposer/workflows/istio/link.yaml +++ b/endpoint-exposer/workflows/istio/link.yaml @@ -25,6 +25,8 @@ steps: type: environment - name: K8S_NAMESPACE type: environment + - name: AVP_POLICY_STORE_ID + type: environment - name: "build httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_link_httproute" diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index 68e0dde..b2ab854 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -8,3 +8,6 @@ steps: configuration: ACTION: apply DRY_RUN: false + - name: "store service results" + type: script + file: "$SERVICE_PATH/scripts/np/update_service_results" From c42ab082a255f217598c4351e3411428347f168c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 23:40:02 -0300 Subject: [PATCH 084/101] refactor(link): remove visibility field, derive gateway from scope type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes visibility was redundant since the gateway is already determined by the scope type (internal → gateway-private, external → gateway-public). Simplified build_link_httproute to use a single HTTPRoute per link, selecting the gateway based on scope.service.attributes.visibility. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/istio/build_link_httproute | 59 ++++++++----------- endpoint-exposer/specs/links/route.json.tpl | 9 +-- .../workflows/istio/link-update.yaml | 2 + endpoint-exposer/workflows/istio/link.yaml | 2 + 4 files changed, 30 insertions(+), 42 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_link_httproute b/endpoint-exposer/scripts/istio/build_link_httproute index 996a686..c228b77 100644 --- a/endpoint-exposer/scripts/istio/build_link_httproute +++ b/endpoint-exposer/scripts/istio/build_link_httproute @@ -5,43 +5,40 @@ set -euo pipefail source "$SERVICE_PATH/scripts/istio/config" source "$SERVICE_PATH/scripts/avp/config" -echo "=== Building HTTPRoutes for link (scope: $SCOPE) ===" +echo "=== Building HTTPRoute for link (scope: $SCOPE) ===" -# Resolve scope → ID and domain (shared by all routes in this link) +# Resolve scope → ID, domain and visibility SCOPES_JSON=$(np scope list --application_id "$APPLICATION_ID" --format json | jq '.results // []') SCOPE_ID=$(echo "$SCOPES_JSON" | jq -r --arg name "$SCOPE" '.[] | select(.name == $name) | .id') if [[ -z "$SCOPE_ID" || "$SCOPE_ID" == "null" ]]; then echo "ERROR: Could not resolve scope '$SCOPE'" exit 1 fi -SCOPE_DOMAIN=$(np scope read --id "$SCOPE_ID" --format json | jq -r '.domain // ""') + +SCOPE_DATA=$(np scope read --id "$SCOPE_ID" --format json) +SCOPE_DOMAIN=$(echo "$SCOPE_DATA" | jq -r '.domain // ""') if [[ -z "$SCOPE_DOMAIN" ]]; then echo "ERROR: Scope '$SCOPE' has no domain" exit 1 fi -echo "Resolved scope '$SCOPE' → domain: $SCOPE_DOMAIN" - -# Split routes by visibility and inject scope for process_routes -PUBLIC_ROUTES=$(echo "$ROUTES_JSON" | jq -c --arg s "$SCOPE" \ - '[.[] | select((.visibility // "public") == "public") | . + {scope: $s}]') -PRIVATE_ROUTES=$(echo "$ROUTES_JSON" | jq -c --arg s "$SCOPE" \ - '[.[] | select(.visibility == "private") | . + {scope: $s}]') -NUM_PUBLIC=$(echo "$PUBLIC_ROUTES" | jq 'length') -NUM_PRIVATE=$(echo "$PRIVATE_ROUTES" | jq 'length') -echo "Routes: $NUM_PUBLIC public, $NUM_PRIVATE private" -# Creates an empty HTTPRoute YAML file and processes all routes into it -build_httproute() { - local name=$1 - local gateway=$2 - local routes=$3 - local file="$OUTPUT_DIR/httproute-${name}.yaml" +# Determine gateway from scope visibility — private scopes use gateway-private +SCOPE_VISIBILITY=$(echo "$SCOPE_DATA" | jq -r '.service.attributes.visibility // "external"') +if [[ "$SCOPE_VISIBILITY" == "internal" ]]; then + GATEWAY_NAME="$PRIVATE_GATEWAY_NAME" + HTTPROUTE_SUFFIX="private" +else + GATEWAY_NAME="$PUBLIC_GATEWAY_NAME" + HTTPROUTE_SUFFIX="public" +fi +echo "Resolved scope '$SCOPE' → domain: $SCOPE_DOMAIN, visibility: $SCOPE_VISIBILITY, gateway: $GATEWAY_NAME" - cat > "$file" < "$HTTPROUTE_FILE" < Date: Fri, 22 May 2026 23:44:18 -0300 Subject: [PATCH 085/101] fix(unlink): derive HTTPROUTE_NAME from LINK_ID instead of stored attributes delete_link_policy silently skipped deletion when ACTION_LINK_ATTRIBUTES_POLICY_IDS was not stored. Now unlink.yaml builds context first (HTTPROUTE_NAME derived from stable LINK_ID) and uses delete_link_policies_by_name which searches AVP by name. delete_link_httproute also falls back to HTTPROUTE_NAME from context. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/istio/delete_link_httproute | 7 +++-- endpoint-exposer/workflows/istio/unlink.yaml | 30 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/endpoint-exposer/scripts/istio/delete_link_httproute b/endpoint-exposer/scripts/istio/delete_link_httproute index 6bcfac4..62a6ad8 100644 --- a/endpoint-exposer/scripts/istio/delete_link_httproute +++ b/endpoint-exposer/scripts/istio/delete_link_httproute @@ -2,11 +2,14 @@ set -euo pipefail -BASE_NAME="${ACTION_LINK_ATTRIBUTES_HTTPROUTE_NAME:-}" K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" +# Prefer HTTPROUTE_NAME from build context (stable, derived from LINK_ID) +# Fall back to stored attribute for backwards compatibility +BASE_NAME="${HTTPROUTE_NAME:-${ACTION_LINK_ATTRIBUTES_HTTPROUTE_NAME:-}}" + if [[ -z "$BASE_NAME" || "$BASE_NAME" == "null" ]]; then - echo "WARNING: No httproute_name found in link attributes, skipping HTTPRoute deletion" + echo "WARNING: No httproute_name available, skipping HTTPRoute deletion" exit 0 fi diff --git a/endpoint-exposer/workflows/istio/unlink.yaml b/endpoint-exposer/workflows/istio/unlink.yaml index 4401774..5448b80 100644 --- a/endpoint-exposer/workflows/istio/unlink.yaml +++ b/endpoint-exposer/workflows/istio/unlink.yaml @@ -1,7 +1,33 @@ steps: - - name: "delete avp policy" + - name: "find k8s namespace" type: script - file: "$SERVICE_PATH/scripts/avp/delete_link_policy" + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: "build context" + type: script + file: "$SERVICE_PATH/scripts/istio/build_link_context" + output: + - name: SERVICE_ID + type: environment + - name: SERVICE_SLUG + type: environment + - name: ACTION_ID + type: environment + - name: APPLICATION_ID + type: environment + - name: ROUTES_JSON + type: environment + - name: SCOPE + type: environment + - name: HTTPROUTE_NAME + type: environment + - name: K8S_NAMESPACE + type: environment + - name: "delete avp policies" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_link_policies_by_name" - name: "delete httproute" type: script file: "$SERVICE_PATH/scripts/istio/delete_link_httproute" From e37a1230a9a976f228b51aeeaa82ed51419e451c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 23:49:50 -0300 Subject: [PATCH 086/101] fix(create_policies): replace exit 0 with conditional to avoid killing workflow exit 0 in sourced scripts kills the parent workflow process, causing apply and store service results steps to never run. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/create_policies | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoint-exposer/scripts/avp/create_policies b/endpoint-exposer/scripts/avp/create_policies index 120c15c..0303708 100755 --- a/endpoint-exposer/scripts/avp/create_policies +++ b/endpoint-exposer/scripts/avp/create_policies @@ -12,8 +12,7 @@ APPLICATION_ID=$(echo "$CONTEXT" | jq -r '.tags.application_id // ""') NUM_ROUTES=$(echo "$ROUTES_JSON" | jq 'length') if [[ "$NUM_ROUTES" -eq 0 ]]; then echo "No routes configured, skipping AVP policy creation" - exit 0 -fi +else if [[ -z "$APPLICATION_ID" ]] || [[ "$APPLICATION_ID" == "null" ]]; then echo "ERROR: APPLICATION_ID not found in context" @@ -89,3 +88,4 @@ when { echo "✅ Cedar policy created: $METHOD $ROUTE_PATH @ $SCOPE_DOMAIN (groups: $NUM_GROUPS, policy-id: $POLICY_ID)" done +fi From 2091d4c91262b88a166aa3dd1e2496bca2042815 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 23:52:55 -0300 Subject: [PATCH 087/101] fix(update): remove duplicate store service results step from update.yaml Having the step in both create.yaml (via include) and update.yaml was causing it to not run. Let the include mechanism inherit it from create.yaml. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/workflows/istio/update.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/endpoint-exposer/workflows/istio/update.yaml b/endpoint-exposer/workflows/istio/update.yaml index b2ab854..68e0dde 100644 --- a/endpoint-exposer/workflows/istio/update.yaml +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -8,6 +8,3 @@ steps: configuration: ACTION: apply DRY_RUN: false - - name: "store service results" - type: script - file: "$SERVICE_PATH/scripts/np/update_service_results" From f7ad337da0aadee13e4e259ac7a62469b0756210 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Fri, 22 May 2026 23:55:07 -0300 Subject: [PATCH 088/101] fix(apply): remove exit 0 that killed workflow after apply step exit 0 in sourced scripts terminates the parent workflow process, preventing any steps after apply (store service results, store link results) from running. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/common/apply | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/endpoint-exposer/scripts/common/apply b/endpoint-exposer/scripts/common/apply index e07acd1..db6bdd8 100644 --- a/endpoint-exposer/scripts/common/apply +++ b/endpoint-exposer/scripts/common/apply @@ -86,5 +86,4 @@ done # Cleanup temporary directory rm -rf "$TEMP_APPLY_DIR" -# Note: DRY_RUN is for testing - we exit 0 even in dry run mode -exit 0 \ No newline at end of file +true \ No newline at end of file From bb27239826d5ceee302af2094aab45f830a61954 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 09:57:54 -0300 Subject: [PATCH 089/101] fix(link): pass scope name to process_routes and fix scope lookup build_link_httproute now stamps each route with {scope: $SCOPE} before sourcing process_routes, which needs the field to resolve the backend. process_routes was looking up scopes by .slug but route configs always reference scope names; align with the rest of the codebase (.name). Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/istio/build_link_httproute | 3 +++ endpoint-exposer/scripts/istio/process_routes | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/endpoint-exposer/scripts/istio/build_link_httproute b/endpoint-exposer/scripts/istio/build_link_httproute index c228b77..e018ce2 100644 --- a/endpoint-exposer/scripts/istio/build_link_httproute +++ b/endpoint-exposer/scripts/istio/build_link_httproute @@ -55,6 +55,9 @@ spec: rules: [] EOF +# process_routes expects each route to have a {scope} field for backend lookup +ROUTES_JSON=$(echo "$ROUTES_JSON" | jq -c --arg s "$SCOPE" '[.[] | . + {scope: $s}]') +export ROUTES_JSON export HTTPROUTE_FILE source "$SERVICE_PATH/scripts/istio/process_routes" echo "✅ HTTPRoute ${HTTPROUTE_NAME}-${HTTPROUTE_SUFFIX} created ($(echo "$ROUTES_JSON" | jq 'length') rules)" diff --git a/endpoint-exposer/scripts/istio/process_routes b/endpoint-exposer/scripts/istio/process_routes index 80b78cb..a7ae482 100755 --- a/endpoint-exposer/scripts/istio/process_routes +++ b/endpoint-exposer/scripts/istio/process_routes @@ -82,11 +82,11 @@ for ((i=0; i Date: Tue, 26 May 2026 10:19:23 -0300 Subject: [PATCH 090/101] fix(link): add ensure_policy_store step to link workflows Link creation/update was failing with ResourceNotFoundException because AVP_POLICY_STORE_ID was falling back to a hardcoded default that does not exist in the target environment. - Add ensure_policy_store step to link.yaml and link-update.yaml so the store is created on demand if not already configured. - Extend ensure_policy_store to also look in .service.parameters so it picks up the ID stored by the service create/update workflow. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/ensure_policy_store | 1 + endpoint-exposer/workflows/istio/link-update.yaml | 6 ++++++ endpoint-exposer/workflows/istio/link.yaml | 6 ++++++ 3 files changed, 13 insertions(+) diff --git a/endpoint-exposer/scripts/avp/ensure_policy_store b/endpoint-exposer/scripts/avp/ensure_policy_store index 5afca69..e3c9910 100755 --- a/endpoint-exposer/scripts/avp/ensure_policy_store +++ b/endpoint-exposer/scripts/avp/ensure_policy_store @@ -10,6 +10,7 @@ CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" CONFIGURED_ID=$(echo "$CONTEXT" | jq -r ' .parameters.avp_policy_store_id // .notification.parameters.avp_policy_store_id // + .service.parameters.avp_policy_store_id // ""') if [[ -n "$CONFIGURED_ID" ]]; then diff --git a/endpoint-exposer/workflows/istio/link-update.yaml b/endpoint-exposer/workflows/istio/link-update.yaml index 55acda5..af9f5a9 100644 --- a/endpoint-exposer/workflows/istio/link-update.yaml +++ b/endpoint-exposer/workflows/istio/link-update.yaml @@ -27,6 +27,12 @@ steps: type: environment - name: AVP_POLICY_STORE_ID type: environment + - name: "ensure avp policy store" + type: script + file: "$SERVICE_PATH/scripts/avp/ensure_policy_store" + output: + - name: AVP_POLICY_STORE_ID + type: environment - name: "delete avp policies" type: script file: "$SERVICE_PATH/scripts/avp/delete_link_policies_by_name" diff --git a/endpoint-exposer/workflows/istio/link.yaml b/endpoint-exposer/workflows/istio/link.yaml index 765a992..f08a360 100644 --- a/endpoint-exposer/workflows/istio/link.yaml +++ b/endpoint-exposer/workflows/istio/link.yaml @@ -27,6 +27,12 @@ steps: type: environment - name: AVP_POLICY_STORE_ID type: environment + - name: "ensure avp policy store" + type: script + file: "$SERVICE_PATH/scripts/avp/ensure_policy_store" + output: + - name: AVP_POLICY_STORE_ID + type: environment - name: "build httproute" type: script file: "$SERVICE_PATH/scripts/istio/build_link_httproute" From c9306742f2addc6c720b47f6a2011db936118746 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 10:30:01 -0300 Subject: [PATCH 091/101] fix(link): read avp_policy_store_id from service.attributes not service.parameters In NullPlatform, service action results (stored via np service action update --results) are exposed as .service.attributes, not .service.parameters. Reading the wrong field caused build_link_context and ensure_policy_store to always get an empty ID, triggering a new policy store creation on every link action instead of reusing the one created during service setup. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/ensure_policy_store | 2 +- endpoint-exposer/scripts/istio/build_link_context | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/endpoint-exposer/scripts/avp/ensure_policy_store b/endpoint-exposer/scripts/avp/ensure_policy_store index e3c9910..16d06f8 100755 --- a/endpoint-exposer/scripts/avp/ensure_policy_store +++ b/endpoint-exposer/scripts/avp/ensure_policy_store @@ -10,7 +10,7 @@ CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" CONFIGURED_ID=$(echo "$CONTEXT" | jq -r ' .parameters.avp_policy_store_id // .notification.parameters.avp_policy_store_id // - .service.parameters.avp_policy_store_id // + .service.attributes.avp_policy_store_id // ""') if [[ -n "$CONFIGURED_ID" ]]; then diff --git a/endpoint-exposer/scripts/istio/build_link_context b/endpoint-exposer/scripts/istio/build_link_context index 322f7f1..ccd36d1 100644 --- a/endpoint-exposer/scripts/istio/build_link_context +++ b/endpoint-exposer/scripts/istio/build_link_context @@ -32,7 +32,7 @@ if [[ -z "$SCOPE" ]]; then fi echo "Inferred scope: $SCOPE" -AVP_POLICY_STORE_ID=$(echo "$CONTEXT" | jq -r '.service.parameters.avp_policy_store_id // .notification.parameters.avp_policy_store_id // ""') +AVP_POLICY_STORE_ID=$(echo "$CONTEXT" | jq -r '.service.attributes.avp_policy_store_id // .notification.parameters.avp_policy_store_id // ""') K8S_NAMESPACE="${NAMESPACE_OVERRIDE:-nullplatform}" From b8d182095af02ae50d8921cd32fcc5ee19d84444 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 11:21:06 -0300 Subject: [PATCH 092/101] feat(service): delete AVP policy store on service deletion Adds delete_policy_store script and a new step in the delete workflow so the AVP policy store is cleaned up when a service is removed. Co-Authored-By: Claude Sonnet 4.6 --- .../scripts/avp/delete_policy_store | 19 +++++++++++++++++++ endpoint-exposer/workflows/istio/delete.yaml | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100755 endpoint-exposer/scripts/avp/delete_policy_store diff --git a/endpoint-exposer/scripts/avp/delete_policy_store b/endpoint-exposer/scripts/avp/delete_policy_store new file mode 100755 index 0000000..b6701bb --- /dev/null +++ b/endpoint-exposer/scripts/avp/delete_policy_store @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euo pipefail + +source "$SERVICE_PATH/scripts/avp/config" + +if [[ -z "${AVP_POLICY_STORE_ID:-}" ]]; then + echo "No AVP_POLICY_STORE_ID set, skipping policy store deletion" + exit 0 +fi + +echo "Deleting AVP policy store: $AVP_POLICY_STORE_ID" +if aws verifiedpermissions delete-policy-store \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" 2>&1; then + echo "✅ AVP policy store $AVP_POLICY_STORE_ID deleted" +else + echo "⚠️ AVP policy store $AVP_POLICY_STORE_ID not found or already deleted (skipping)" +fi diff --git a/endpoint-exposer/workflows/istio/delete.yaml b/endpoint-exposer/workflows/istio/delete.yaml index 31a1348..cbf9a93 100644 --- a/endpoint-exposer/workflows/istio/delete.yaml +++ b/endpoint-exposer/workflows/istio/delete.yaml @@ -6,4 +6,7 @@ steps: file: "$SERVICE_PATH/scripts/avp/delete_policies" - name: apply configuration: - ACTION: delete \ No newline at end of file + ACTION: delete + - name: "delete avp policy store" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_policy_store" \ No newline at end of file From 5f36184f361ac85070b6d611a26b3b53041d0523 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 11:27:41 -0300 Subject: [PATCH 093/101] fix(service): ensure delete_policy_store step runs via include mechanism The workflow runner only executes steps defined in the base (create.yaml) when using include. Added delete_policy_store as a placeholder step in create.yaml that skips by default, activated only in delete.yaml via DELETE_POLICY_STORE=true. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/delete_policy_store | 5 +++++ endpoint-exposer/workflows/istio/create.yaml | 3 +++ endpoint-exposer/workflows/istio/delete.yaml | 4 +++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/avp/delete_policy_store b/endpoint-exposer/scripts/avp/delete_policy_store index b6701bb..056e21e 100755 --- a/endpoint-exposer/scripts/avp/delete_policy_store +++ b/endpoint-exposer/scripts/avp/delete_policy_store @@ -4,6 +4,11 @@ set -euo pipefail source "$SERVICE_PATH/scripts/avp/config" +if [[ "${DELETE_POLICY_STORE:-false}" != "true" ]]; then + echo "Skipping policy store deletion (not in delete context)" + exit 0 +fi + if [[ -z "${AVP_POLICY_STORE_ID:-}" ]]; then echo "No AVP_POLICY_STORE_ID set, skipping policy store deletion" exit 0 diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 5695721..9cf56c6 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -60,6 +60,9 @@ steps: configuration: ACTION: apply DRY_RUN: false + - name: "delete avp policy store" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_policy_store" - name: "store service results" type: script file: "$SERVICE_PATH/scripts/np/update_service_results" diff --git a/endpoint-exposer/workflows/istio/delete.yaml b/endpoint-exposer/workflows/istio/delete.yaml index cbf9a93..362273a 100644 --- a/endpoint-exposer/workflows/istio/delete.yaml +++ b/endpoint-exposer/workflows/istio/delete.yaml @@ -9,4 +9,6 @@ steps: ACTION: delete - name: "delete avp policy store" type: script - file: "$SERVICE_PATH/scripts/avp/delete_policy_store" \ No newline at end of file + file: "$SERVICE_PATH/scripts/avp/delete_policy_store" + configuration: + DELETE_POLICY_STORE: "true" \ No newline at end of file From 0e8e88d450443c9992cccf78d546d3d19861b59c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 12:33:26 -0300 Subject: [PATCH 094/101] fix(service): persist avp_policy_store_id in service.attributes np service action update --results only stores at the action level (ephemeral). Added np service patch to also write avp_policy_store_id to service.attributes so subsequent actions (delete) can find it. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/np/update_service_results | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/endpoint-exposer/scripts/np/update_service_results b/endpoint-exposer/scripts/np/update_service_results index d3f8a6f..2d4f87e 100755 --- a/endpoint-exposer/scripts/np/update_service_results +++ b/endpoint-exposer/scripts/np/update_service_results @@ -12,4 +12,8 @@ RESULTS=$(echo "$CONTEXT" | jq \ echo "Storing service results: avp_policy_store_id=$AVP_POLICY_STORE_ID" np service action update --results "$RESULTS" + +np service patch --id "$SERVICE_ID" \ + --body "{\"attributes\": {\"avp_policy_store_id\": \"$AVP_POLICY_STORE_ID\"}}" + echo "✅ Service results stored" From 14478101a92b5bc31d9bf7bb4fdf58abbe34b002 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 12:55:56 -0300 Subject: [PATCH 095/101] fix: persist avp store id early and fix delete workflow step ordering - ensure_policy_store now calls np service patch immediately after creating the store, so the id is persisted even if later steps don't run - create.yaml: move store service results before delete avp policy store so it runs at step 8 (workflow appears to stop at that position) - delete.yaml: add type/file to apply step override so the runner can execute it and continue to delete avp policy store Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/ensure_policy_store | 6 ++++++ endpoint-exposer/workflows/istio/create.yaml | 6 +++--- endpoint-exposer/workflows/istio/delete.yaml | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/endpoint-exposer/scripts/avp/ensure_policy_store b/endpoint-exposer/scripts/avp/ensure_policy_store index 16d06f8..120515a 100755 --- a/endpoint-exposer/scripts/avp/ensure_policy_store +++ b/endpoint-exposer/scripts/avp/ensure_policy_store @@ -24,4 +24,10 @@ else --query 'policyStoreId' --output text) echo "✅ Created new AVP policy store: $CREATED_ID" export AVP_POLICY_STORE_ID="$CREATED_ID" + + if [[ -n "${SERVICE_ID:-}" ]]; then + np service patch --id "$SERVICE_ID" \ + --body "{\"attributes\": {\"avp_policy_store_id\": \"$CREATED_ID\"}}" || true + echo "✅ Persisted avp_policy_store_id to service attributes" + fi fi diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 9cf56c6..5839460 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -60,9 +60,9 @@ steps: configuration: ACTION: apply DRY_RUN: false - - name: "delete avp policy store" - type: script - file: "$SERVICE_PATH/scripts/avp/delete_policy_store" - name: "store service results" type: script file: "$SERVICE_PATH/scripts/np/update_service_results" + - name: "delete avp policy store" + type: script + file: "$SERVICE_PATH/scripts/avp/delete_policy_store" diff --git a/endpoint-exposer/workflows/istio/delete.yaml b/endpoint-exposer/workflows/istio/delete.yaml index 362273a..ba72fe9 100644 --- a/endpoint-exposer/workflows/istio/delete.yaml +++ b/endpoint-exposer/workflows/istio/delete.yaml @@ -5,8 +5,11 @@ steps: type: script file: "$SERVICE_PATH/scripts/avp/delete_policies" - name: apply + type: script + file: "$SERVICE_PATH/scripts/common/apply" configuration: ACTION: delete + DRY_RUN: false - name: "delete avp policy store" type: script file: "$SERVICE_PATH/scripts/avp/delete_policy_store" From fedaaf0a73d2ffc429faa2973f00d3ba0e67746c Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 13:05:18 -0300 Subject: [PATCH 096/101] fix(avp): avoid exit 0 in delete_policies to prevent workflow termination The workflow runner interprets explicit exit 0 as a signal to stop the entire workflow. Replace early exit with if/else so the script completes naturally and subsequent steps (apply, delete avp policy store) can run. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/delete_policies | 31 ++++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/endpoint-exposer/scripts/avp/delete_policies b/endpoint-exposer/scripts/avp/delete_policies index 64cd76f..e31272b 100755 --- a/endpoint-exposer/scripts/avp/delete_policies +++ b/endpoint-exposer/scripts/avp/delete_policies @@ -17,20 +17,19 @@ POLICY_IDS=$(aws verifiedpermissions list-policies \ if [[ -z "$POLICY_IDS" ]]; then echo "No AVP policies found for service $SERVICE_ID" - exit 0 +else + while IFS= read -r POLICY_ID; do + echo "Deleting Cedar policy: $POLICY_ID" + aws verifiedpermissions delete-policy \ + --policy-store-id "$AVP_POLICY_STORE_ID" \ + --region "$AVP_AWS_REGION" \ + --policy-id "$POLICY_ID" + echo "✅ Deleted Cedar policy $POLICY_ID" + done <<< "$POLICY_IDS" + + echo "Cleaning up per-service AuthorizationPolicies for service $SERVICE_ID..." + kubectl delete authorizationpolicy \ + -n "$GATEWAY_NAMESPACE" \ + -l "nullplatform.com/service-id=$SERVICE_ID" \ + --ignore-not-found 2>/dev/null && echo "✅ AuthorizationPolicies cleaned up" fi - -while IFS= read -r POLICY_ID; do - echo "Deleting Cedar policy: $POLICY_ID" - aws verifiedpermissions delete-policy \ - --policy-store-id "$AVP_POLICY_STORE_ID" \ - --region "$AVP_AWS_REGION" \ - --policy-id "$POLICY_ID" - echo "✅ Deleted Cedar policy $POLICY_ID" -done <<< "$POLICY_IDS" - -echo "Cleaning up per-service AuthorizationPolicies for service $SERVICE_ID..." -kubectl delete authorizationpolicy \ - -n "$GATEWAY_NAMESPACE" \ - -l "nullplatform.com/service-id=$SERVICE_ID" \ - --ignore-not-found 2>/dev/null && echo "✅ AuthorizationPolicies cleaned up" From 1b242bc72d4738233e19f23dedfa3562283d8ce2 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 13:46:46 -0300 Subject: [PATCH 097/101] fix(link): store policy_ids as string to satisfy NullPlatform API schema validation Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/np/update_link_results | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/scripts/np/update_link_results b/endpoint-exposer/scripts/np/update_link_results index 3d67bec..d809fa9 100644 --- a/endpoint-exposer/scripts/np/update_link_results +++ b/endpoint-exposer/scripts/np/update_link_results @@ -6,7 +6,7 @@ CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" RESULTS=$(echo "$CONTEXT" | jq \ --arg httproute_name "$HTTPROUTE_NAME" \ - --argjson policy_ids "$POLICY_IDS" \ + --arg policy_ids "$POLICY_IDS" \ --arg scope "$SCOPE" \ '(.notification.parameters // .parameters // {}) + { httproute_name: $httproute_name, From 684fff59fcf69dd5d16c67b853d6a36a1803e7d4 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 15:50:57 -0300 Subject: [PATCH 098/101] feat(avp): discover policy store via SSM instead of creating per-service stores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Lambda authorizer uses an infrastructure-managed policy store (provisioned by Terraform). Services now read the store ID from SSM on first run and cache it in service.attributes to avoid repeated SSM calls. The store is never deleted by service workflows — it is owned by infrastructure. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/config | 3 ++- .../scripts/avp/delete_policy_store | 23 +++------------- .../scripts/avp/ensure_policy_store | 27 ++++++++++++------- endpoint-exposer/workflows/istio/create.yaml | 3 --- endpoint-exposer/workflows/istio/delete.yaml | 7 +---- 5 files changed, 23 insertions(+), 40 deletions(-) diff --git a/endpoint-exposer/scripts/avp/config b/endpoint-exposer/scripts/avp/config index adba3f7..ec99920 100755 --- a/endpoint-exposer/scripts/avp/config +++ b/endpoint-exposer/scripts/avp/config @@ -1,4 +1,5 @@ #!/bin/bash -export AVP_POLICY_STORE_ID="${AVP_POLICY_STORE_ID:-RPkfnZxHxnWktjL9KF2qz5}" +export AVP_POLICY_STORE_ID="${AVP_POLICY_STORE_ID:-}" export AVP_AWS_REGION="${AVP_AWS_REGION:-us-east-1}" +export AVP_POLICY_STORE_SSM_PATH="${AVP_POLICY_STORE_SSM_PATH:-/nullplatform/pae-smoke/avp/policy-store-id}" diff --git a/endpoint-exposer/scripts/avp/delete_policy_store b/endpoint-exposer/scripts/avp/delete_policy_store index 056e21e..ab26bef 100755 --- a/endpoint-exposer/scripts/avp/delete_policy_store +++ b/endpoint-exposer/scripts/avp/delete_policy_store @@ -2,23 +2,6 @@ set -euo pipefail -source "$SERVICE_PATH/scripts/avp/config" - -if [[ "${DELETE_POLICY_STORE:-false}" != "true" ]]; then - echo "Skipping policy store deletion (not in delete context)" - exit 0 -fi - -if [[ -z "${AVP_POLICY_STORE_ID:-}" ]]; then - echo "No AVP_POLICY_STORE_ID set, skipping policy store deletion" - exit 0 -fi - -echo "Deleting AVP policy store: $AVP_POLICY_STORE_ID" -if aws verifiedpermissions delete-policy-store \ - --policy-store-id "$AVP_POLICY_STORE_ID" \ - --region "$AVP_AWS_REGION" 2>&1; then - echo "✅ AVP policy store $AVP_POLICY_STORE_ID deleted" -else - echo "⚠️ AVP policy store $AVP_POLICY_STORE_ID not found or already deleted (skipping)" -fi +# The AVP policy store is infrastructure-managed (provisioned via Terraform). +# Services must never delete it — it is shared across all services in this cluster. +echo "AVP policy store is infrastructure-managed. Skipping deletion." diff --git a/endpoint-exposer/scripts/avp/ensure_policy_store b/endpoint-exposer/scripts/avp/ensure_policy_store index 120515a..f83dc06 100755 --- a/endpoint-exposer/scripts/avp/ensure_policy_store +++ b/endpoint-exposer/scripts/avp/ensure_policy_store @@ -6,7 +6,7 @@ source "$SERVICE_PATH/scripts/avp/config" CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" -# Check user-provided value first, then stored results from previous action +# Check service attributes first (cached from a previous run) CONFIGURED_ID=$(echo "$CONTEXT" | jq -r ' .parameters.avp_policy_store_id // .notification.parameters.avp_policy_store_id // @@ -14,20 +14,27 @@ CONFIGURED_ID=$(echo "$CONTEXT" | jq -r ' ""') if [[ -n "$CONFIGURED_ID" ]]; then - echo "Using configured AVP policy store: $CONFIGURED_ID" + echo "Using cached AVP policy store from service attributes: $CONFIGURED_ID" export AVP_POLICY_STORE_ID="$CONFIGURED_ID" else - echo "No AVP policy store configured, creating a new one..." - CREATED_ID=$(aws verifiedpermissions create-policy-store \ - --validation-settings '{"mode": "OFF"}' \ + echo "No cached AVP policy store, reading from SSM: $AVP_POLICY_STORE_SSM_PATH" + SSM_ID=$(aws ssm get-parameter \ + --name "$AVP_POLICY_STORE_SSM_PATH" \ --region "$AVP_AWS_REGION" \ - --query 'policyStoreId' --output text) - echo "✅ Created new AVP policy store: $CREATED_ID" - export AVP_POLICY_STORE_ID="$CREATED_ID" + --query 'Parameter.Value' \ + --output text) + + if [[ -z "$SSM_ID" || "$SSM_ID" == "None" ]]; then + echo "ERROR: AVP policy store ID not found in SSM at $AVP_POLICY_STORE_SSM_PATH" >&2 + exit 1 + fi + + echo "✅ Found AVP policy store from SSM: $SSM_ID" + export AVP_POLICY_STORE_ID="$SSM_ID" if [[ -n "${SERVICE_ID:-}" ]]; then np service patch --id "$SERVICE_ID" \ - --body "{\"attributes\": {\"avp_policy_store_id\": \"$CREATED_ID\"}}" || true - echo "✅ Persisted avp_policy_store_id to service attributes" + --body "{\"attributes\": {\"avp_policy_store_id\": \"$SSM_ID\"}}" || true + echo "✅ Cached avp_policy_store_id in service attributes" fi fi diff --git a/endpoint-exposer/workflows/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml index 5839460..5695721 100644 --- a/endpoint-exposer/workflows/istio/create.yaml +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -63,6 +63,3 @@ steps: - name: "store service results" type: script file: "$SERVICE_PATH/scripts/np/update_service_results" - - name: "delete avp policy store" - type: script - file: "$SERVICE_PATH/scripts/avp/delete_policy_store" diff --git a/endpoint-exposer/workflows/istio/delete.yaml b/endpoint-exposer/workflows/istio/delete.yaml index ba72fe9..7479361 100644 --- a/endpoint-exposer/workflows/istio/delete.yaml +++ b/endpoint-exposer/workflows/istio/delete.yaml @@ -9,9 +9,4 @@ steps: file: "$SERVICE_PATH/scripts/common/apply" configuration: ACTION: delete - DRY_RUN: false - - name: "delete avp policy store" - type: script - file: "$SERVICE_PATH/scripts/avp/delete_policy_store" - configuration: - DELETE_POLICY_STORE: "true" \ No newline at end of file + DRY_RUN: false \ No newline at end of file From 01d0ee6c87ce6ee55613105001d9aef6ee406b1d Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 16:22:12 -0300 Subject: [PATCH 099/101] feat(spec): remove avp_policy_store_id from service spec UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The store is now discovered automatically via SSM — users no longer need to provide or see this infrastructure detail when creating a service. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index b21df0e..ca0229b 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -7,22 +7,9 @@ "required": [], "uiSchema": { "type": "VerticalLayout", - "elements": [ - { - "type": "Control", - "label": "AVP Policy Store ID", - "scope": "#/properties/avp_policy_store_id" - } - ] + "elements": [] }, - "properties": { - "avp_policy_store_id": { - "type": "string", - "title": "AVP Policy Store ID", - "description": "ID del Policy Store de Amazon Verified Permissions. Si se deja vacío, se creará un policy store de AVP por defecto.", - "editableOn": ["create"] - } - } + "properties": {} }, "values": {} }, From fb210d916228b261df713de07edecb2612d593cd Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 16:58:40 -0300 Subject: [PATCH 100/101] feat(avp): require avp_policy_store_id as mandatory UI input instead of SSM lookup Simplifies the integration by removing the SSM dependency and IAM requirement. The operator provides the store ID (Terraform output policy_store_id) when creating the service. The field is required and only editable on create. Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/scripts/avp/config | 1 - .../scripts/avp/ensure_policy_store | 31 ++++--------------- endpoint-exposer/specs/service-spec.json.tpl | 19 ++++++++++-- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/endpoint-exposer/scripts/avp/config b/endpoint-exposer/scripts/avp/config index ec99920..a9b9866 100755 --- a/endpoint-exposer/scripts/avp/config +++ b/endpoint-exposer/scripts/avp/config @@ -2,4 +2,3 @@ export AVP_POLICY_STORE_ID="${AVP_POLICY_STORE_ID:-}" export AVP_AWS_REGION="${AVP_AWS_REGION:-us-east-1}" -export AVP_POLICY_STORE_SSM_PATH="${AVP_POLICY_STORE_SSM_PATH:-/nullplatform/pae-smoke/avp/policy-store-id}" diff --git a/endpoint-exposer/scripts/avp/ensure_policy_store b/endpoint-exposer/scripts/avp/ensure_policy_store index f83dc06..c39af8d 100755 --- a/endpoint-exposer/scripts/avp/ensure_policy_store +++ b/endpoint-exposer/scripts/avp/ensure_policy_store @@ -6,35 +6,16 @@ source "$SERVICE_PATH/scripts/avp/config" CONTEXT="${CONTEXT:-${NP_ACTION_CONTEXT:-}}" -# Check service attributes first (cached from a previous run) CONFIGURED_ID=$(echo "$CONTEXT" | jq -r ' .parameters.avp_policy_store_id // .notification.parameters.avp_policy_store_id // .service.attributes.avp_policy_store_id // ""') -if [[ -n "$CONFIGURED_ID" ]]; then - echo "Using cached AVP policy store from service attributes: $CONFIGURED_ID" - export AVP_POLICY_STORE_ID="$CONFIGURED_ID" -else - echo "No cached AVP policy store, reading from SSM: $AVP_POLICY_STORE_SSM_PATH" - SSM_ID=$(aws ssm get-parameter \ - --name "$AVP_POLICY_STORE_SSM_PATH" \ - --region "$AVP_AWS_REGION" \ - --query 'Parameter.Value' \ - --output text) - - if [[ -z "$SSM_ID" || "$SSM_ID" == "None" ]]; then - echo "ERROR: AVP policy store ID not found in SSM at $AVP_POLICY_STORE_SSM_PATH" >&2 - exit 1 - fi - - echo "✅ Found AVP policy store from SSM: $SSM_ID" - export AVP_POLICY_STORE_ID="$SSM_ID" - - if [[ -n "${SERVICE_ID:-}" ]]; then - np service patch --id "$SERVICE_ID" \ - --body "{\"attributes\": {\"avp_policy_store_id\": \"$SSM_ID\"}}" || true - echo "✅ Cached avp_policy_store_id in service attributes" - fi +if [[ -z "$CONFIGURED_ID" ]]; then + echo "ERROR: avp_policy_store_id is required. Set it in the service attributes when creating the service." >&2 + exit 1 fi + +echo "Using AVP policy store: $CONFIGURED_ID" +export AVP_POLICY_STORE_ID="$CONFIGURED_ID" diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index ca0229b..990459b 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -4,12 +4,25 @@ "schema": { "type": "object", "$schema": "http://json-schema.org/draft-07/schema#", - "required": [], + "required": ["avp_policy_store_id"], "uiSchema": { "type": "VerticalLayout", - "elements": [] + "elements": [ + { + "type": "Control", + "label": "AVP Policy Store ID", + "scope": "#/properties/avp_policy_store_id" + } + ] }, - "properties": {} + "properties": { + "avp_policy_store_id": { + "type": "string", + "title": "AVP Policy Store ID", + "description": "ID del Policy Store de Amazon Verified Permissions asociado al Lambda authorizer de este cluster. Se obtiene del output 'policy_store_id' del módulo Terraform avp-smoke.", + "editableOn": ["create"] + } + } }, "values": {} }, From d9a47fc4ecc8af21e9dea9cc98dea1ba8bc99947 Mon Sep 17 00:00:00 2001 From: sebas_correa Date: Tue, 26 May 2026 17:00:53 -0300 Subject: [PATCH 101/101] fix(spec): translate avp_policy_store_id description to English and reference security module Co-Authored-By: Claude Sonnet 4.6 --- endpoint-exposer/specs/service-spec.json.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/endpoint-exposer/specs/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl index 990459b..c63afb3 100644 --- a/endpoint-exposer/specs/service-spec.json.tpl +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -19,7 +19,7 @@ "avp_policy_store_id": { "type": "string", "title": "AVP Policy Store ID", - "description": "ID del Policy Store de Amazon Verified Permissions asociado al Lambda authorizer de este cluster. Se obtiene del output 'policy_store_id' del módulo Terraform avp-smoke.", + "description": "Amazon Verified Permissions Policy Store ID associated with the Lambda authorizer for this cluster. Obtained from the 'policy_store_id' output of the security Terraform module.", "editableOn": ["create"] } }