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..06d1058 --- /dev/null +++ b/endpoint-exposer/container-scope-override/deployment/sync_exposer @@ -0,0 +1,172 @@ +#!/bin/bash + +echo "=== DEBUG: Starting sync_exposer script ===" + +APPLICATION_NRN=$(jq -r .application.nrn <<< "$CONTEXT") + +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 "$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=$(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'" + exit 1 +fi + +echo "DEBUG: SERVICE_SPEC_ID=$SERVICE_SPEC_ID" + +# 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) + +SERVICE=$(jq -c --arg spec_id "$SERVICE_SPEC_ID" ' + .results + | map(select(.specification_id == $spec_id)) + | .[0] +' <<< "$SERVICES") + +SERVICE_ID=$(jq -r .id <<< "$SERVICE") + +if [[ -z "$SERVICE_ID" || "$SERVICE_ID" == "null" ]]; then + echo "Could not find service instance for specification '$SERVICE_SPEC_ID', skipping exposer sync" + exit 0 +fi + +echo "DEBUG: SERVICE_ID=$SERVICE_ID" + +# 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") + +echo "DEBUG: PARAMETERS=$PARAMETERS" + +# 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) + +ACTION_SPEC=$(jq -c --arg slug "$ACTION_SLUG" ' + .results + | map(select(.slug == $slug)) + | .[0] +' <<< "$SERVICE_ACTIONS") + +ACTION_SPEC_ID=$(jq -r .id <<< "$ACTION_SPEC") + +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 + +echo "DEBUG: ACTION_SPEC_ID=$ACTION_SPEC_ID" + +# Step 5: Create service action with parameters (with retry for concurrency) +echo "DEBUG: Creating service action..." + +MAX_CREATE_RETRIES=10 +RETRY_DELAY=5 +create_attempt=0 +ACTION_ID="" + +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 +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 update the endpoint exposer." + exit 1 + fi + + echo "DEBUG: Reading action status..." + 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]" + + 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: 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..101aee8 --- /dev/null +++ b/endpoint-exposer/container-scope-override/values.yaml @@ -0,0 +1,3 @@ +configuration: + SERVICE_SPECIFICATION_SLUG: endpoint-exposer + \ 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/scripts/common/apply b/endpoint-exposer/scripts/common/apply new file mode 100644 index 0000000..2692c3c --- /dev/null +++ b/endpoint-exposer/scripts/common/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..9bbb3c1 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_context @@ -0,0 +1,31 @@ +#!/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 domain from parameters +PUBLIC_DOMAIN=$(echo "$CONTEXT" | jq -r '.parameters.public_domain // ""') + +# Extract routes array from parameters +ROUTES_JSON=$(echo "$CONTEXT" | jq -c '.parameters.routes // []') + +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 ROUTES_JSON \ 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..cfa9bb8 --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_ingress @@ -0,0 +1,25 @@ +#!/bin/bash + +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 "Output file: $HTTPROUTE_FILE" + +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" + +# 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/build_ingress_with_rule b/endpoint-exposer/scripts/istio/build_ingress_with_rule new file mode 100755 index 0000000..bfbf2ff --- /dev/null +++ b/endpoint-exposer/scripts/istio/build_ingress_with_rule @@ -0,0 +1,384 @@ +#!/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 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 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 + # 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 + 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:-}") + 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:-}") + + # 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_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..1846bef --- /dev/null +++ b/endpoint-exposer/scripts/istio/fetch_httproute @@ -0,0 +1,52 @@ +#!/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" +TEMPLATE="$SERVICE_PATH/templates/istio/empty.yaml.tpl" +echo "HTTPRoute name: $HTTPROUTE_NAME" + +# Define output file +if [[ -n "${OUTPUT_FILE:-}" ]]; then + HTTPROUTE_FILE="$OUTPUT_FILE" +else + HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +fi + +# 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 + +# Export the file path for the workflow +export HTTPROUTE_FILE +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..670d1a7 --- /dev/null +++ b/endpoint-exposer/scripts/istio/process_routes @@ -0,0 +1,110 @@ +#!/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 + +# 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)" + +# Define HTTPRoute file +HTTPROUTE_FILE="$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" +HTTPROUTE_NAME="${SERVICE_NAME}-${SERVICE_ID}-route" + +export HTTPROUTE_FILE +echo "HTTPRoute file: $HTTPROUTE_FILE" +echo "HTTPRoute name: $HTTPROUTE_NAME" + +# 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 "$HTTPROUTE_FILE" + +echo "HTTPRoute hostname updated to: $DOMAIN" +echo "HTTPRoute parentRefs set to: $GATEWAY" + +# 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/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/service-spec.json.tpl b/endpoint-exposer/specs/service-spec.json.tpl new file mode 100644 index 0000000..343245c --- /dev/null +++ b/endpoint-exposer/specs/service-spec.json.tpl @@ -0,0 +1,118 @@ +{ + "assignable_to": "dimension", + "attributes": { + "schema": { + "type": "object", + "$schema": "http://json-schema.org/draft-07/schema#", + "uiSchema": { + "type": "VerticalLayout", + "elements": [ + { + "type": "Group", + "label": "Domain", + "elements": [ + { + "type": "Control", + "scope": "#/properties/publicDomain" + } + ] + }, + { + "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": "Scope", + "scope": "#/items/properties/scope" + } + ] + }, + "showSortButtons": true + } + } + ] + } + ] + }, + "properties": { + "publicDomain": { + "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" + }, + "scope": { + "type": "string", + "title": "Scope", + "description": "The scope slug", + "additionalKeywords": { + "enum": "[.scopes[]?.slug]" + } + } + }, + "required": [ + "method", + "path", + "scope" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "publicDomain" + ], + "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/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/istio/create.yaml b/endpoint-exposer/workflows/istio/create.yaml new file mode 100644 index 0000000..a104121 --- /dev/null +++ b/endpoint-exposer/workflows/istio/create.yaml @@ -0,0 +1,46 @@ +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: 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: environment + - 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: apply + type: script + 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/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/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/update.yaml b/endpoint-exposer/workflows/istio/update.yaml new file mode 100644 index 0000000..7d12cd8 --- /dev/null +++ b/endpoint-exposer/workflows/istio/update.yaml @@ -0,0 +1,53 @@ +steps: + - name: "find k8s namespace" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_provider_data" + output: + - name: K8S_NAMESPACE + type: environment + - name: 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: ROUTES_JSON + type: environment + - name: "fetch existing httproute" + type: script + file: "$SERVICE_PATH/scripts/istio/fetch_httproute" + output: + - name: HTTPROUTE_FILE + 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" + type: script + file: "$SERVICE_PATH/scripts/istio/process_routes" + output: + - name: HTTPROUTE_FILE + type: file + file: "$OUTPUT_DIR/httproute-$SERVICE_ID-public.yaml" + - name: apply + type: script + file: "$SERVICE_PATH/scripts/common/apply" + configuration: + ACTION: apply + DRY_RUN: false