From bda35541bb87a8dcc40054b642e783f731bb21e6 Mon Sep 17 00:00:00 2001 From: Javi Date: Fri, 19 Dec 2025 16:09:10 -0300 Subject: [PATCH 01/40] 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 02/40] 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 03/40] 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 04/40] 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 05/40] 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 06/40] 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 07/40] 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 08/40] 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 09/40] 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 10/40] 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 11/40] 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 12/40] 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 13/40] 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 14/40] 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 15/40] 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 16/40] 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 17/40] 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 18/40] 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 19/40] 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 20/40] 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 21/40] 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 22/40] 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 23/40] 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 24/40] 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 25/40] 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 26/40] 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 27/40] 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 28/40] 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 29/40] 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 30/40] 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 31/40] 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 32/40] 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 33/40] 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 34/40] 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 35/40] 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 36/40] 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 37/40] 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 38/40] 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 39/40] 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 40/40] 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