Skip to content

Commit a8ea975

Browse files
authored
Merge pull request #5 from buildkite-plugins/SUP-4893/Retry
Adds automatic retry logic with exponential backoff
2 parents bdb4f30 + c4c4183 commit a8ea975

File tree

4 files changed

+145
-6
lines changed

4 files changed

+145
-6
lines changed

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ A `pipeline.yml` like this will read each secret out into a ENV variable:
1818
steps:
1919
- command: echo "The content of ANIMAL is \$ANIMAL"
2020
plugins:
21-
- secrets#v1.0.0:
21+
- secrets#v1.0.1:
2222
variables:
2323
ANIMAL: llamas
2424
FOO: bar
2525
```
2626
2727
### Multiple
2828
29-
Create a single Buildkite secret with one variable per line, encoded as base64 for storage.
29+
Create a single Buildkite secret with one variable per line, encoded as base64 for storage.
3030
3131
For example, setting three variables looks like this in a file:
3232
@@ -50,7 +50,7 @@ job environment using a pipeline.yml like this:
5050
steps:
5151
- command: build.sh
5252
plugins:
53-
- secrets#v1.0.0:
53+
- secrets#v1.0.1:
5454
env: "llamas"
5555
```
5656
@@ -63,8 +63,36 @@ The secret key name to fetch multiple from Buildkite secrets.
6363
Specify a dictionary of `key: value` pairs to inject as environment variables, where the key is the name of the
6464
environment variable to be set, and the value is the Buildkite Secret key.
6565

66+
### `retry-max-attempts` (optional, number, default: 5)
67+
68+
Maximum number of retry attempts for transient failures when fetching secrets (e.g., 5xx server errors, network issues).
69+
70+
### `retry-base-delay` (optional, number, default: 2)
71+
72+
Base delay in seconds for exponential backoff between retry attempts.
73+
74+
## Retry Behavior
75+
76+
This plugin implements automatic retry logic with exponential backoff for secret calls. This will occur for 5xx server errors or any local network issues. If a 4xx code is received, a fast failure will be served.
77+
78+
By default, the base delay will be 2 seconds, with a maximum of 5 retries.
79+
80+
### Example with Custom Retry
81+
82+
```yaml
83+
steps:
84+
- command: build.sh
85+
plugins:
86+
- secrets#v1.0.1:
87+
env: "llamas"
88+
retry-max-attempts: 10
89+
retry-base-delay: 2
90+
```
91+
6692
## Testing
93+
6794
You can run the tests using `docker-compose`:
95+
6896
```bash
6997
docker compose run --rm tests
7098
```

hooks/environment

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,66 @@
11
#!/bin/bash
22
set -euo pipefail
33

4+
calculate_backoff_delay() {
5+
local BASE_DELAY="$1"
6+
local ATTEMPT="$2"
7+
local DELAY=$((BASE_DELAY * (2 ** (ATTEMPT - 1))))
8+
local JITTER=$((RANDOM % (DELAY / 4 + 1)))
9+
local TOTAL_DELAY=$((DELAY + JITTER))
10+
11+
if [ "$TOTAL_DELAY" -gt 60 ]; then
12+
TOTAL_DELAY=60
13+
fi
14+
15+
echo "$TOTAL_DELAY"
16+
}
17+
18+
buildkite_agent_secret_get_with_retry() {
19+
local KEY="$1"
20+
local MAX_ATTEMPTS="${BUILDKITE_PLUGIN_SECRETS_RETRY_MAX_ATTEMPTS:-5}"
21+
local BASE_DELAY="${BUILDKITE_PLUGIN_SECRETS_RETRY_BASE_DELAY:-2}"
22+
local ATTEMPT=1
23+
local EXIT_CODE
24+
local OUTPUT
25+
26+
while [ "$ATTEMPT" -le "$MAX_ATTEMPTS" ]; do
27+
set +e
28+
OUTPUT=$(buildkite-agent secret get "${KEY}" 2>&1)
29+
EXIT_CODE=$?
30+
set -e
31+
32+
if [ "$EXIT_CODE" -eq 0 ]; then
33+
echo "$OUTPUT"
34+
return 0
35+
fi
36+
37+
if echo "$OUTPUT" | grep -qiE "(not found|unauthorized|forbidden|bad request)"; then
38+
echo "$OUTPUT"
39+
return "$EXIT_CODE"
40+
fi
41+
42+
if [ "$ATTEMPT" -lt "$MAX_ATTEMPTS" ]; then
43+
local TOTAL_DELAY
44+
TOTAL_DELAY=$(calculate_backoff_delay "$BASE_DELAY" "$ATTEMPT")
45+
46+
echo "Failed to fetch secret $KEY (attempt $ATTEMPT/$MAX_ATTEMPTS). Retrying in ${TOTAL_DELAY}s..." >&2
47+
echo "Error: $OUTPUT" >&2
48+
49+
sleep "$TOTAL_DELAY"
50+
ATTEMPT=$((ATTEMPT + 1))
51+
else
52+
echo "Failed to fetch secret $KEY after $MAX_ATTEMPTS attempts" >&2
53+
echo "Error: $OUTPUT" >&2
54+
return "$EXIT_CODE"
55+
fi
56+
done
57+
}
58+
459
# downloads the secret by provided key using the buildkite-agent secret command
560
downloadSecret() {
661
local key=$1
762

8-
if ! secret=$(buildkite-agent secret get "${key}"); then
63+
if ! secret=$(buildkite_agent_secret_get_with_retry "${key}"); then
964
echo "not found ${key}"
1065
else
1166
echo "${secret}"
@@ -57,7 +112,7 @@ processVariables() {
57112
key="${param/BUILDKITE_PLUGIN_SECRETS_VARIABLES_/}"
58113
path="${!param}"
59114

60-
if ! value=$(buildkite-agent secret get "${path}"); then
115+
if ! value=$(buildkite_agent_secret_get_with_retry "${path}"); then
61116
echo "⚠️ Unable to find secret at ${path}"
62117
exit 1
63118
else

plugin.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@ configuration:
1010
type: string
1111
variables:
1212
type: object
13+
retry-max-attempts:
14+
type: number
15+
default: 5
16+
retry-base-delay:
17+
type: number
18+
default: 2
1319
additionalProperties: false

tests/environment-hook.bats

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ setup() {
77

88
export BUILDKITE_PIPELINE_SLUG=testpipe
99
export BUILDKITE_PLUGIN_SECRETS_DUMP_ENV=true
10+
export BUILDKITE_PLUGIN_SECRETS_RETRY_BASE_DELAY=0
1011
}
1112

1213
@test "Download default env from Buildkite secrets" {
@@ -86,11 +87,60 @@ setup() {
8687

8788
stub buildkite-agent \
8889
"secret get env : echo 'not found'" \
89-
"secret get best : exit 1"
90+
"secret get best : echo 'not found' && exit 1"
9091

9192
run bash -c "$PWD/hooks/environment"
9293

9394
assert_failure
9495
assert_output --partial "⚠️ Unable to find secret at"
96+
refute_output --partial "Retrying"
97+
unstub buildkite-agent
98+
}
99+
100+
@test "Retry on transient failure" {
101+
export TESTDATA='Rk9PPWJhcgpCQVI9QmF6ClNFQ1JFVD1sbGFtYXMK'
102+
export BUILDKITE_PLUGIN_SECRETS_RETRY_MAX_ATTEMPTS=3
103+
104+
stub buildkite-agent \
105+
"secret get env : exit 1" \
106+
"secret get env : echo ${TESTDATA}"
107+
108+
run bash -c "$PWD/hooks/environment"
109+
110+
assert_success
111+
assert_output --partial "FOO=bar"
112+
assert_output --partial "Failed to fetch secret env (attempt 1/3)"
113+
unstub buildkite-agent
114+
}
115+
116+
@test "Fails after max attempts" {
117+
export BUILDKITE_PLUGIN_SECRETS_VARIABLES_ANIMAL="best"
118+
export BUILDKITE_PLUGIN_SECRETS_RETRY_MAX_ATTEMPTS=3
119+
120+
stub buildkite-agent \
121+
"secret get env : echo 'not found'" \
122+
"secret get best : exit 1" \
123+
"secret get best : exit 1" \
124+
"secret get best : exit 1"
125+
126+
run bash -c "$PWD/hooks/environment"
127+
128+
assert_failure
129+
assert_output --partial "Failed to fetch secret best after 3 attempts"
130+
unstub buildkite-agent
131+
}
132+
133+
@test "No retry on 4xx" {
134+
export BUILDKITE_PLUGIN_SECRETS_VARIABLES_ANIMAL="best"
135+
export BUILDKITE_PLUGIN_SECRETS_RETRY_MAX_ATTEMPTS=3
136+
137+
stub buildkite-agent \
138+
"secret get env : echo 'not found'" \
139+
"secret get best : echo 'unauthorized' && exit 1"
140+
141+
run bash -c "$PWD/hooks/environment"
142+
143+
assert_failure
144+
refute_output --partial "Retrying"
95145
unstub buildkite-agent
96146
}

0 commit comments

Comments
 (0)