Skip to content

Commit fea1afa

Browse files
authored
Merge pull request #145 from juanpark-dandy/master
Add support for GCS
2 parents 326c94e + 409bbb4 commit fea1afa

File tree

3 files changed

+510
-1
lines changed

3 files changed

+510
-1
lines changed

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The plugin supports various backends and compression algorithms, and some enviro
3636
- `tar` for tar compression
3737
- `zip/unzip` for zip compression
3838
- `aws` AWS CLI for reading and writing to an AWS S3 backend
39+
- `gsutil` or `gcloud storage` Google Cloud SDK CLI for reading and writing to a GCS backend
3940

4041
## Mandatory parameters
4142

@@ -62,6 +63,7 @@ You can specify multiple levels in an array to save the same artifact as a cache
6263
Defines how the cache is stored and restored. Can be any string (see [Customizable Backends](#customizable-backends)), but the plugin natively supports the following:
6364
* `fs` (default)
6465
* `s3`
66+
* `gcs`
6567

6668
#### `fs`
6769

@@ -130,6 +132,38 @@ steps:
130132
compression: zstd
131133
```
132134

135+
#### `gcs`
136+
137+
Store things in a Google Cloud Storage (GCS) bucket. The backend automatically detects and uses either `gcloud storage` (preferred) or `gsutil` CLI tools, whichever is available. You need to make sure at least one of these commands is available and appropriately configured with the necessary credentials and access permissions.
138+
139+
You also need the agent to have access to the following defined environment variables:
140+
* `BUILDKITE_PLUGIN_GCS_CACHE_BUCKET`: the bucket to use (backend will fail if not defined)
141+
* `BUILDKITE_PLUGIN_GCS_CACHE_PREFIX`: optional prefix to use for the cache within the bucket
142+
* `BUILDKITE_PLUGIN_GCS_CACHE_CLI`: optional CLI preference, either `gcloud` or `gsutil` (auto-detects if not set, preferring `gcloud storage`)
143+
144+
Setting the `BUILDKITE_PLUGIN_GCS_CACHE_QUIET` environment variable will reduce logging of file operations to GCS.
145+
146+
#### Example
147+
148+
```yaml
149+
env:
150+
BUILDKITE_PLUGIN_GCS_CACHE_BUCKET: "my-cache-bucket" # Required: GCS bucket to store cache objects
151+
BUILDKITE_PLUGIN_GCS_CACHE_PREFIX: "buildkite/cache"
152+
BUILDKITE_PLUGIN_GCS_CACHE_QUIET: "true"
153+
154+
steps:
155+
- label: ':nodejs: Install dependencies'
156+
command: npm ci
157+
plugins:
158+
- cache#v1.8.1:
159+
backend: gcs
160+
path: node_modules
161+
manifest: package-lock.json
162+
restore: file
163+
save: file
164+
compression: zstd
165+
```
166+
133167
### `compression` (string)
134168

135169
Allows for the cached file/folder to be saved/restored as a single file. You will need to make sure to use the same compression when saving and restoring or it will cause a cache miss.
@@ -300,4 +334,4 @@ steps:
300334

301335
## License
302336

303-
MIT (see [LICENSE](LICENSE))
337+
MIT (see [LICENSE](LICENSE))

backends/cache_gcs

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/bin/bash
2+
3+
if [ -z "${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}" ]; then
4+
echo '+++ 🚨 Missing GCS bucket configuration'
5+
exit 1
6+
fi
7+
8+
# Detect which GCS CLI to use
9+
detect_gcs_cli() {
10+
# Check for explicit preference
11+
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_CLI}" ]; then
12+
echo "${BUILDKITE_PLUGIN_GCS_CACHE_CLI}"
13+
return
14+
fi
15+
16+
# Auto-detect: prefer gcloud storage if available
17+
if command -v gcloud &>/dev/null && gcloud storage --help &>/dev/null 2>&1; then
18+
echo "gcloud"
19+
elif command -v gsutil &>/dev/null; then
20+
echo "gsutil"
21+
else
22+
echo '+++ 🚨 Neither gcloud storage nor gsutil found' >&2
23+
exit 1
24+
fi
25+
}
26+
27+
# Lazy load CLI selection
28+
get_gcs_cli() {
29+
if [ -z "${GCS_CLI}" ]; then
30+
GCS_CLI=$(detect_gcs_cli)
31+
fi
32+
echo "${GCS_CLI}"
33+
}
34+
35+
build_key() {
36+
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_PREFIX}" ]; then
37+
echo "${BUILDKITE_PLUGIN_GCS_CACHE_PREFIX}/${1}"
38+
else
39+
echo "$1"
40+
fi
41+
}
42+
43+
gcs_cmd() {
44+
local cmd_args=()
45+
local cli
46+
cli=$(get_gcs_cli)
47+
48+
if [ "${cli}" = "gcloud" ]; then
49+
cmd_args=(gcloud storage)
50+
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_QUIET}" ]; then
51+
cmd_args+=(--verbosity=none)
52+
fi
53+
else
54+
cmd_args=(gsutil)
55+
if [ -n "${BUILDKITE_PLUGIN_GCS_CACHE_QUIET}" ]; then
56+
cmd_args+=(-q)
57+
fi
58+
fi
59+
60+
"${cmd_args[@]}" "$@"
61+
}
62+
63+
gcs_copy() {
64+
local from="$1"
65+
local to="$2"
66+
local use_rsync="${3:-true}"
67+
local cli
68+
cli=$(get_gcs_cli)
69+
70+
if [ "${use_rsync}" = 'true' ]; then
71+
# Use rsync for directories
72+
# Note: gcloud uses --delete-unmatched-destination-objects instead of -d
73+
if [ "${cli}" = "gcloud" ]; then
74+
gcs_cmd rsync -r --delete-unmatched-destination-objects "${from}" "${to}"
75+
else
76+
gcs_cmd -m rsync -r -d "${from}" "${to}"
77+
fi
78+
else
79+
# Use cp for single files
80+
gcs_cmd cp "${from}" "${to}"
81+
fi
82+
}
83+
84+
gcs_exists() {
85+
local key="$1"
86+
local full_path
87+
full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/$(build_key "${key}")"
88+
local cli
89+
cli=$(get_gcs_cli)
90+
91+
# Check if exact object exists or if it's a directory prefix
92+
if [ "${cli}" = "gcloud" ]; then
93+
# For gcloud, check if ls produces any output (exit code alone is unreliable)
94+
local output
95+
# Check for objects with this exact path or under this prefix
96+
output=$(gcs_cmd ls "${full_path}" 2>/dev/null) || true
97+
if [ -n "${output}" ]; then
98+
return 0
99+
fi
100+
# Also try with trailing slash for folder-like prefixes
101+
output=$(gcs_cmd ls "${full_path}/" 2>/dev/null) || true
102+
if [ -n "${output}" ]; then
103+
return 0
104+
fi
105+
else
106+
# For gsutil, use stat for exact object match
107+
if gcs_cmd stat "${full_path}" &>/dev/null; then
108+
return 0
109+
fi
110+
# Check if it's a prefix (folder) using ls -d
111+
if gcs_cmd ls -d "${full_path}/" &>/dev/null; then
112+
return 0
113+
fi
114+
fi
115+
return 1
116+
}
117+
118+
restore_cache() {
119+
local from="$1"
120+
local to="$2"
121+
local use_rsync='false'
122+
local key
123+
key="$(build_key "${from}")"
124+
local full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/${key}"
125+
local cli
126+
cli=$(get_gcs_cli)
127+
128+
# Check if it's a directory by trying to list it as a prefix
129+
if [ "${cli}" = "gcloud" ]; then
130+
# For gcloud, check if there are objects under this prefix (directory)
131+
if gcs_cmd ls "${full_path}/" &>/dev/null; then
132+
use_rsync='true'
133+
fi
134+
else
135+
# For gsutil, use ls -d for efficiency
136+
if gcs_cmd ls -d "${full_path}/" &>/dev/null; then
137+
use_rsync='true'
138+
fi
139+
fi
140+
141+
gcs_copy "${full_path}" "${to}" "${use_rsync}"
142+
}
143+
144+
save_cache() {
145+
local to="$1"
146+
local from="$2"
147+
local use_rsync='true'
148+
local key
149+
key="$(build_key "${to}")"
150+
local full_path="gs://${BUILDKITE_PLUGIN_GCS_CACHE_BUCKET}/${key}"
151+
152+
if [ -f "${from}" ]; then
153+
use_rsync='false'
154+
fi
155+
156+
gcs_copy "${from}" "${full_path}" "${use_rsync}"
157+
}
158+
159+
exists_cache() {
160+
if [ -z "$1" ]; then exit 1; fi
161+
gcs_exists "$1"
162+
}
163+
164+
OPCODE="$1"
165+
shift
166+
167+
if [ "$OPCODE" = 'exists' ]; then
168+
exists_cache "$@"
169+
elif [ "$OPCODE" = 'get' ]; then
170+
restore_cache "$@"
171+
elif [ "$OPCODE" = 'save' ]; then
172+
save_cache "$@"
173+
else
174+
exit 255
175+
fi

0 commit comments

Comments
 (0)