Skip to content

Commit 7c4d408

Browse files
authored
add retries for ephemeral instance api calls (#61)
1 parent 40084a8 commit 7c4d408

File tree

4 files changed

+183
-48
lines changed

4 files changed

+183
-48
lines changed

.github/workflows/ephemeral.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
name: LocalStack Ephemeral Instance Test
22
on:
3-
workflow_dispatch:
3+
pull_request:
4+
paths-ignore:
5+
- ./*.md
6+
- LICENSE
47

58
jobs:
69
preview-test:

ephemeral/retry-function.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/bin/bash
2+
3+
# retry() function: Retries a given command up to 'retries' times with a 'wait' interval.
4+
# Usage: retry <command>
5+
# Example: retry my_api_call_function
6+
retry() {
7+
local retries=5
8+
local count=0
9+
local wait=5
10+
local output
11+
while [ $count -lt $retries ]; do
12+
# We disable set -e for the command and capture its output.
13+
output=$(set +e; "$@")
14+
local exit_code=$?
15+
if [ $exit_code -eq 0 ]; then
16+
echo "$output"
17+
return 0
18+
fi
19+
count=$((count + 1))
20+
echo "Command failed with exit code $exit_code. Retrying in $wait seconds... ($count/$retries)" >&2
21+
sleep $wait
22+
done
23+
echo "Command failed after $retries retries." >&2
24+
echo "$output" # Also return the output of the last failed attempt for debugging
25+
return 1
26+
}
27+
28+
# Helper function to check for a JSON error response from the API
29+
# Usage: check_for_api_error "<response_body>" "<context_message>"
30+
check_for_api_error() {
31+
local response="$1"
32+
local context_message="$2"
33+
if echo "$response" | jq -e 'if type == "object" and has("error") then true else false end' > /dev/null; then
34+
echo "API error during '$context_message': $response" >&2
35+
return 1
36+
fi
37+
return 0
38+
}

ephemeral/shutdown/action.yml

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
name: Shutdown Ephemeral Instance
2+
description: 'Shutdowns an Ephemeral Instance (PR Preview)'
23

34
inputs:
45
localstack-api-key:
5-
description: 'LocalStack API key used to access the platform api'
6-
required: true
6+
description: 'LocalStack Auth Token used to access the platform api'
7+
required: false
78
github-token:
89
description: 'Github token used to create PR comments'
910
required: true
@@ -34,16 +35,38 @@ runs:
3435
- name: Shutdown ephemeral instance
3536
shell: bash
3637
run: |
37-
response=$(curl -X DELETE \
38-
-s -o /dev/null -w "%{http_code}" \
39-
-H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \
40-
-H "content-type: application/json" \
41-
https://api.localstack.cloud/v1/compute/instances/$previewName)
42-
if [[ "$response" -ne 200 ]]; then
43-
# In case the deletion fails, e.g. if the instance cannot be found, we raise a proper error on the platform
44-
echo "Unable to delete preview environment. API response: $response"
45-
exit 1
46-
fi
38+
AUTH_HEADER="ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}"
39+
CONTENT_TYPE_HEADER="content-type: application/json"
40+
API_URL_BASE="https://api.localstack.cloud/v1/compute/instances"
41+
42+
source ${{ github.action_path }}/../retry-function.sh
43+
shutdown_instance() {
44+
# The API returns a 200 on successful deletion.
45+
# We use --fail-with-body so curl fails on server errors (5xx) and triggers the retry.
46+
local response
47+
response=$(curl --fail-with-body -s -w "\n%{http_code}" -X DELETE \
48+
-H "$AUTH_HEADER" \
49+
-H "$CONTENT_TYPE_HEADER" \
50+
"$API_URL_BASE/$previewName")
51+
local exit_code=$?
52+
local http_code=$(echo "$response" | tail -n1)
53+
local body=$(echo "$response" | sed '$d')
54+
55+
if [ $exit_code -ne 0 ]; then
56+
# A 404 means it's already gone, which is a success case for shutdown.
57+
if [ "$http_code" -ne 404 ]; then
58+
echo "Error deleting instance, curl failed with exit code $exit_code. API response: $body" >&2
59+
return 1
60+
fi
61+
fi
62+
if [ "$http_code" -eq 200 ]; then
63+
echo "Instance '$previewName' deleted successfully."
64+
elif [ "$http_code" -eq 404 ]; then
65+
echo "Instance '$previewName' was already deleted (not found)."
66+
fi
67+
}
68+
69+
retry shutdown_instance
4770
4871
- name: Update status comment
4972
uses: actions-cool/maintain-one-comment@v3.1.1

ephemeral/startup/action.yml

Lines changed: 106 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
name: Create PR Preview
2+
description: 'Spins up an Ephemeral Instance for a PR Preview'
23

34
inputs:
45
github-token:
56
description: 'Github token used to create PR comments'
67
required: true
78
localstack-api-key:
8-
description: 'LocalStack API key used to create the preview environment'
9+
description: 'LocalStack Auth Token used to create the preview environment'
910
required: false
1011
preview-cmd:
1112
description: 'Command(s) used to create a preview of the PR (can use $AWS_ENDPOINT_URL)'
@@ -28,18 +29,6 @@ inputs:
2829
runs:
2930
using: composite
3031
steps:
31-
- run: >
32-
echo "GH_ACTION_ROOT=$(
33-
ls -d $(
34-
ls -d ./../../_actions/* |
35-
grep -i localstack |
36-
tail -n1
37-
)/setup-localstack/* |
38-
grep -v completed |
39-
tail -n1
40-
)" >> $GITHUB_ENV
41-
shell: bash
42-
4332
- name: Initial PR comment
4433
if: inputs.github-token
4534
uses: jenseng/dynamic-uses@5175289a9a87978dcfcb9cf512b821d23b2a53eb # v1
@@ -57,44 +46,96 @@ runs:
5746

5847
- name: Setup preview name
5948
shell: bash
49+
id: preview-name
6050
run: |
6151
prId=$(<pr-id.txt)
6252
repoName=$GITHUB_REPOSITORY
6353
repoNameCleaned=$(echo -n "$repoName" | tr -c '[:alnum:]' '-')
6454
previewName=preview-$repoNameCleaned-$prId
6555
echo "previewName=$previewName" >> $GITHUB_ENV
56+
echo "name=$previewName" >> $GITHUB_OUTPUT
6657
6758
- name: Create preview environment
6859
shell: bash
6960
id: create-instance
7061
run: |
62+
AUTH_HEADER="ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}"
63+
CONTENT_TYPE_HEADER="content-type: application/json"
64+
API_URL_BASE="https://api.localstack.cloud/v1/compute/instances"
65+
66+
source ${{ github.action_path }}/../retry-function.sh
67+
68+
fetch_instances() {
69+
local list_response
70+
list_response=$(curl --fail-with-body -s -X GET \
71+
-H "$AUTH_HEADER" \
72+
-H "$CONTENT_TYPE_HEADER" \
73+
"$API_URL_BASE")
74+
if [ $? -ne 0 ]; then echo "curl command failed while fetching instances. Response: $list_response" >&2; return 1; fi
75+
if ! check_for_api_error "$list_response" "fetch instances"; then return 1; fi
76+
echo "$list_response"
77+
}
78+
79+
if ! list_response=$(retry fetch_instances); then
80+
echo "Error: Failed to fetch instances after multiple retries."
81+
exit 1
82+
fi
83+
7184
autoLoadPod="${AUTO_LOAD_POD:-${{ inputs.auto-load-pod }}}"
7285
extensionAutoInstall="${EXTENSION_AUTO_INSTALL:-${{ inputs.extension-auto-install }}}"
7386
lifetime="${{ inputs.lifetime }}"
7487
75-
list_response=$(curl -X GET \
76-
-H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \
77-
-H "content-type: application/json" \
78-
https://api.localstack.cloud/v1/compute/instances)
79-
8088
instance_exists=$(echo "$list_response" | jq --arg NAME "$previewName" '.[] | select(.instance_name == $NAME)')
8189
90+
delete_instance() {
91+
# We expect a 200 on success or 404 if it's already gone. Other codes are errors.
92+
local response
93+
response=$(curl --fail-with-body -s -w "\n%{http_code}" -X DELETE \
94+
-H "$AUTH_HEADER" \
95+
-H "$CONTENT_TYPE_HEADER" \
96+
"$API_URL_BASE/$previewName")
97+
local exit_code=$?
98+
local http_code=$(echo "$response" | tail -n1)
99+
local body=$(echo "$response" | sed '$d')
100+
101+
if [ $exit_code -ne 0 ]; then echo "curl command failed while deleting instance. Response: $body" >&2; return 1; fi
102+
if ! check_for_api_error "$body" "delete instance"; then return 1; fi
103+
104+
if [ "$http_code" -eq 200 ]; then
105+
echo "Instance '$previewName' deleted successfully."
106+
fi
107+
}
108+
82109
if [ -n "$instance_exists" ]; then
83-
del_response=$(curl -X DELETE \
84-
-H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \
85-
-H "content-type: application/json" \
86-
https://api.localstack.cloud/v1/compute/instances/$previewName)
110+
echo "Found existing instance using '$previewName', trying to delete the old one..."
111+
if ! retry delete_instance; then
112+
echo "Error: Failed to delete existing instance after multiple retries."
113+
exit 1
114+
fi
87115
fi
88116
89-
response=$(curl -X POST -d "{\"instance_name\": \"${previewName}\", \"lifetime\": ${lifetime} ,\"env_vars\": {\"AUTO_LOAD_POD\": \"${autoLoadPod}\", \"EXTENSION_AUTO_INSTALL\": \"${extensionAutoInstall}\"}}"\
90-
-H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \
91-
-H "content-type: application/json" \
92-
https://api.localstack.cloud/v1/compute/instances)
93-
endpointUrl=$(echo "$response" | jq -r .endpoint_url)
94-
if [ "$endpointUrl" = "null" ] || [ "$endpointUrl" = "" ]; then
95-
echo "Unable to create preview environment. API response: $response"
117+
create_instance_func() {
118+
local response
119+
response=$(curl --fail-with-body -s -X POST -d "{\"instance_name\": \"${previewName}\", \"lifetime\": ${lifetime} ,\"env_vars\": {\"AUTO_LOAD_POD\": \"${autoLoadPod}\", \"EXTENSION_AUTO_INSTALL\": \"${extensionAutoInstall}\"}}"\
120+
-H "$AUTH_HEADER" \
121+
-H "$CONTENT_TYPE_HEADER" \
122+
"$API_URL_BASE")
123+
if [ $? -ne 0 ]; then echo "curl command failed while creating instance. Response: $response" >&2; return 1; fi
124+
if ! check_for_api_error "$response" "create instance"; then return 1; fi
125+
if ! echo "$response" | jq -e 'has("endpoint_url") and (.endpoint_url | test(".+"))' > /dev/null; then
126+
echo "Invalid response from instance creation API: $response" >&2; return 1;
127+
fi
128+
echo "$response"
129+
}
130+
131+
echo "Creating preview environment ..."
132+
if ! response=$(retry create_instance_func); then
133+
echo "Error: Failed to create preview environment after multiple retries."
96134
exit 1
97135
fi
136+
137+
endpointUrl=$(echo "$response" | jq -r .endpoint_url)
138+
98139
echo "Created preview environment with endpoint URL: $endpointUrl"
99140
100141
echo $endpointUrl > ./ls-preview-url.txt
@@ -110,16 +151,46 @@ runs:
110151
- name: Run preview deployment
111152
if: ${{ inputs.preview-cmd != '' }}
112153
shell: bash
113-
run:
154+
run: |
114155
${{ inputs.preview-cmd }}
115156
116157
- name: Print logs of ephemeral instance
117158
if: ${{ !cancelled() && steps.create-instance.outcome == 'success' }}
118159
shell: bash
160+
env:
161+
previewName: ${{ steps.preview-name.outputs.name }}
119162
run: |
120-
log_response=$(curl -X GET \
121-
-H "ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}" \
122-
-H "content-type: application/json" \
123-
https://api.localstack.cloud/v1/compute/instances/$previewName/logs)
124-
163+
AUTH_HEADER="ls-api-key: ${LOCALSTACK_AUTH_TOKEN:-${LOCALSTACK_API_KEY:-${{ inputs.localstack-api-key }}}}"
164+
CONTENT_TYPE_HEADER="content-type: application/json"
165+
API_URL_BASE="https://api.localstack.cloud/v1/compute/instances"
166+
167+
source ${{ github.action_path }}/../retry-function.sh
168+
fetch_logs() {
169+
local log_response
170+
log_response=$(curl --fail-with-body -s -X GET \
171+
-H "$AUTH_HEADER" \
172+
-H "$CONTENT_TYPE_HEADER" \
173+
"$API_URL_BASE/$previewName/logs")
174+
if [ $? -ne 0 ]; then echo "curl command failed while fetching logs. Response: $log_response" >&2; return 1; fi
175+
if ! check_for_api_error "$log_response" "fetch logs"; then return 1; fi
176+
177+
# A valid log response must be a JSON array.
178+
if ! echo "$log_response" | jq -e 'if type == "array" then true else false end' > /dev/null; then
179+
echo "Invalid response from logs API (expected a JSON array): $log_response" >&2; return 1;
180+
fi
181+
182+
# Check if the logs contain the "Ready." message, indicating the instance is fully started.
183+
if ! echo "$log_response" | jq -e '.[] | select(.content | contains("Ready."))' > /dev/null; then
184+
echo "Instance is not ready yet, waiting for 'Ready.' message in logs..." >&2
185+
return 1
186+
fi
187+
188+
echo "$log_response"
189+
}
190+
echo "Fetching logs for $previewName ..."
191+
if ! log_response=$(retry fetch_logs); then
192+
echo "Error: Failed to fetch logs after multiple retries."
193+
exit 1
194+
fi
195+
echo "$previewName logs:"
125196
echo "$log_response" | jq -r '.[].content'

0 commit comments

Comments
 (0)