Skip to content

Commit be1b96a

Browse files
authored
feat: buildkit action (#32)
2 parents cc3803f + 95074b2 commit be1b96a

File tree

1 file changed

+319
-0
lines changed

1 file changed

+319
-0
lines changed

actions/buildkit/action.yml

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
name: 'Buildkit build and push image'
2+
description: 'Build docker image using buildkits buildctl'
3+
inputs:
4+
path:
5+
description: 'Root directory'
6+
required: false
7+
default: '.'
8+
context:
9+
description: 'Folder to use as context during image build'
10+
required: false
11+
default: '.'
12+
dockerfile:
13+
description: 'Dockerfile path to use for the build'
14+
required: false
15+
default: 'Dockerfile'
16+
platforms:
17+
description: 'Platform(s) that the image should be build for, multiple platforms can be specified comma separated (linux/amd64,linux/arm64)'
18+
required: true
19+
default: 'linux/amd64'
20+
tags:
21+
description: 'Tags to build to the image'
22+
required: true
23+
default: ''
24+
labels:
25+
description: 'Labels for the image'
26+
required: false
27+
default: ''
28+
buildkit-daemon-address:
29+
description: 'Address of the buildkit daemon to use'
30+
required: true
31+
default: 'tcp://buildkit-service.buildkit-service.svc:1234'
32+
buildkit-cert-ca-file:
33+
description: 'The ca certificate file to use for the buildkit client'
34+
required: false
35+
default: '/buildkit-certs/ca.pem'
36+
buildkit-cert-file:
37+
description: 'The certificate file to use for the buildkit client'
38+
required: false
39+
default: '/buildkit-certs/cert.pem'
40+
buildkit-cert-key-file:
41+
description: 'The certificate key file to use for the buildkit client'
42+
required: false
43+
default: '/buildkit-certs/key.pem'
44+
buildkit-cert-ca:
45+
description: 'The ca certificate content to use for the buildkit client (inline)'
46+
required: false
47+
buildkit-cert:
48+
description: 'The certificate content to use for the buildkit client (inline)'
49+
required: false
50+
buildkit-cert-key:
51+
description: 'The certificate key content to use for the buildkit client (inline)'
52+
required: false
53+
buildkit-svc-count:
54+
description: 'Number of buildkit services for modulo distribution'
55+
required: false
56+
default: '6'
57+
buildkit-service-enabled:
58+
description: 'Toggle buildkit service usage'
59+
required: false
60+
default: 'true'
61+
fallback-enabled:
62+
description: 'Enable fallback to local buildkit when service is unavailable'
63+
required: false
64+
default: 'false'
65+
git-default-branch:
66+
description: 'Default git branch for caching'
67+
required: false
68+
default: 'main'
69+
cache-enabled:
70+
description: 'Enable build cache'
71+
required: false
72+
default: 'true'
73+
cache-from-refs:
74+
description: 'Additional cache references to import from (comma separated)'
75+
required: false
76+
cache-to-refs:
77+
description: 'Additional cache references to export to (comma separated)'
78+
required: false
79+
push:
80+
description: 'Defines whether the image should be pushed to the registry or not, default is true'
81+
required: false
82+
default: "true"
83+
build-args:
84+
description: 'Build arguments to be passed to the build'
85+
required: false
86+
target:
87+
description: 'Build stage to build'
88+
required: false
89+
secrets:
90+
description: 'Build secrets to be passed to the build'
91+
required: false
92+
registry:
93+
description: 'The docker registry to push built images'
94+
required: false
95+
registry-username:
96+
description: 'The docker registry user'
97+
required: false
98+
registry-password:
99+
description: 'The docker registry password'
100+
required: false
101+
buildkit-version:
102+
description: 'The buildkit version to use'
103+
required: false
104+
default: 'v0.22.0'
105+
runs:
106+
using: 'composite'
107+
steps:
108+
- name: install buildkit
109+
shell: bash
110+
env:
111+
ARCH: ${{ runner.arch }}
112+
BUILDKIT_VERSION: ${{ inputs.buildkit-version }}
113+
run: |
114+
# Convert runner.arch to buildkit architecture format
115+
case "$ARCH" in
116+
X64)
117+
BUILDKIT_ARCH="amd64"
118+
;;
119+
X86)
120+
BUILDKIT_ARCH="386"
121+
;;
122+
ARM)
123+
BUILDKIT_ARCH="arm"
124+
;;
125+
ARM64)
126+
BUILDKIT_ARCH="arm64"
127+
;;
128+
*)
129+
BUILDKIT_ARCH="$ARCH"
130+
;;
131+
esac
132+
133+
wget -q "https://github.com/moby/buildkit/releases/download/$BUILDKIT_VERSION/buildkit-$BUILDKIT_VERSION.linux-$BUILDKIT_ARCH.tar.gz" && mkdir buildkit && cat buildkit-$BUILDKIT_VERSION.linux-$BUILDKIT_ARCH.tar.gz \
134+
| tar -C buildkit -zxvf -
135+
136+
# Create a bin directory in the workspace
137+
mkdir -p $GITHUB_WORKSPACE/bin
138+
139+
# Move buildctl to the workspace bin directory
140+
mv buildkit/bin/buildctl $GITHUB_WORKSPACE/bin/
141+
142+
# Add to PATH using GitHub's recommended approach
143+
echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
144+
145+
rm -rf buildkit
146+
147+
- name: build and push
148+
shell: bash
149+
env:
150+
INPUT_PATH: ${{ inputs.path }}
151+
INPUT_SECRET: ${{ inputs.secrets }}
152+
INPUT_REGISTRY: ${{ inputs.registry }}
153+
INPUT_REGISTRY_USERNAME: ${{ inputs.registry-username }}
154+
INPUT_REGISTRY_PASSWORD: ${{ inputs.registry-password }}
155+
INPUT_TARGET: ${{ inputs.target }}
156+
INPUT_BUILDKIT_DAEMON_ADDRESS: ${{ inputs.buildkit-daemon-address }}
157+
INPUT_BUILDKIT_CERT_CA_FILE: ${{ inputs.buildkit-cert-ca-file }}
158+
INPUT_BUILDKIT_CERT_FILE: ${{ inputs.buildkit-cert-file }}
159+
INPUT_BUILDKIT_CERT_KEY_FILE: ${{ inputs.buildkit-cert-key-file }}
160+
INPUT_BUILDKIT_CERT_CA: ${{ inputs.buildkit-cert-ca }}
161+
INPUT_BUILDKIT_CERT: ${{ inputs.buildkit-cert }}
162+
INPUT_BUILDKIT_CERT_KEY: ${{ inputs.buildkit-cert-key }}
163+
INPUT_BUILDKIT_SVC_COUNT: ${{ inputs.buildkit-svc-count }}
164+
INPUT_BUILDKIT_SERVICE_ENABLED: ${{ inputs.buildkit-service-enabled }}
165+
INPUT_FALLBACK_ENABLED: ${{ inputs.fallback-enabled }}
166+
INPUT_GIT_DEFAULT_BRANCH: ${{ inputs.git-default-branch }}
167+
INPUT_CACHE_ENABLED: ${{ inputs.cache-enabled }}
168+
INPUT_CACHE_FROM_REFS: ${{ inputs.cache-from-refs }}
169+
INPUT_CACHE_TO_REFS: ${{ inputs.cache-to-refs }}
170+
INPUT_CONTEXT: ${{ inputs.context }}
171+
INPUT_DOCKERFILE: ${{ inputs.dockerfile }}
172+
INPUT_PLATFORMS: ${{ inputs.platforms }}
173+
INPUT_LABELS: ${{ inputs.labels }}
174+
INPUT_BUILD_ARGS: ${{ inputs.build-args }}
175+
INPUT_TAGS: ${{ inputs.tags }}
176+
INPUT_PUSH: ${{ inputs.push }}
177+
run: |
178+
cd "$INPUT_PATH"
179+
180+
# handle secrets to args
181+
declare -a secret_args
182+
while IFS='=' read -r key val; do
183+
if [[ -n "$key" && -n "$val" ]]; then
184+
val="${val%\'*}"
185+
val="${val%\"*}"
186+
val="${val#\'}"
187+
val="${val#\"}"
188+
export SECRET_ENV_${key}="${val}"
189+
secret_args+=("--secret id=${key},env=SECRET_ENV_${key}")
190+
fi
191+
done <<< "$INPUT_SECRETS"
192+
193+
# login to docker registry
194+
export DOCKER_CONFIG=~/.docker
195+
if [ -n "$INPUT_REGISTRY" ]; then
196+
mkdir -p $DOCKER_CONFIG
197+
echo "{\"auths\":{\"$INPUT_REGISTRY\":{\"username\":\"$INPUT_REGISTRY_USERNAME\",\"password\":\"$INPUT_REGISTRY_PASSWORD\"}}}" > $DOCKER_CONFIG/config.json
198+
fi
199+
200+
target_args=""
201+
if [ -n "$INPUT_TARGET" ]; then
202+
target_args="--target=$INPUT_TARGET"
203+
fi
204+
205+
# Setup cache options
206+
cache_options=""
207+
if [ "$INPUT_CACHE_ENABLED" = "true" ]; then
208+
# Extract first tag for cache reference
209+
first_tag=$(echo "$INPUT_TAGS" | cut -d ',' -f 1)
210+
211+
# Import cache from default branch
212+
cache_options="--import-cache type=registry,ref=$first_tag:cache-$INPUT_GIT_DEFAULT_BRANCH"
213+
214+
# Import cache from additional refs if provided
215+
if [ -n "$INPUT_CACHE_FROM_REFS" ]; then
216+
IFS=',' read -ra CACHE_REFS <<< "$INPUT_CACHE_FROM_REFS"
217+
for ref in "${CACHE_REFS[@]}"; do
218+
cache_options="$cache_options --import-cache type=registry,ref=$ref"
219+
done
220+
fi
221+
222+
# Export cache if specified
223+
if [ -n "$INPUT_CACHE_TO_REFS" ]; then
224+
IFS=',' read -ra CACHE_REFS <<< "$INPUT_CACHE_TO_REFS"
225+
for ref in "${CACHE_REFS[@]}"; do
226+
cache_options="$cache_options --export-cache type=registry,mode=max,image-manifest=true,ignore-error=true,ref=$ref"
227+
done
228+
fi
229+
fi
230+
231+
# Apply modulo distribution if enabled
232+
buildkit_addr="$INPUT_BUILDKIT_DAEMON_ADDRESS"
233+
if [ -n "$INPUT_BUILDKIT_SVC_COUNT" ] && [ "$INPUT_BUILDKIT_SVC_COUNT" -gt 0 ]; then
234+
# Extract image name from tags for hashing
235+
image_name=$(echo "$INPUT_TAGS" | cut -d ':' -f 1)
236+
237+
# Calculate pod number using consistent hashing
238+
pod_hash_ref="$image_name"
239+
pod_num=$(( 0x$(echo "$pod_hash_ref" | md5sum | cut -d ' ' -f 1 | head -c 15) ))
240+
[ $pod_num -lt 0 ] && pod_num=$((pod_num * -1))
241+
pod_num=$(( $pod_num % $INPUT_BUILDKIT_SVC_COUNT ))
242+
243+
# Modify buildkit address to target specific numbered subdomain
244+
prefix_addr="${buildkit_addr%%.*}"
245+
protocol="${prefix_addr%%://*}"
246+
subdomain="${prefix_addr#*//}"
247+
248+
buildkit_addr=$(echo "$buildkit_addr" | sed "s|$prefix_addr|$protocol://$subdomain-$pod_num.$subdomain|")
249+
echo "Using buildkit service: $buildkit_addr"
250+
fi
251+
252+
# Define buildctl command based on service enabled flag
253+
buildctl_cmd="buildctl"
254+
buildctl_mtls_options=""
255+
256+
if [ "$INPUT_BUILDKIT_SERVICE_ENABLED" = "true" ]; then
257+
buildctl_cmd="buildctl --addr $buildkit_addr"
258+
259+
# Handle TLS certificates (file-based or inline)
260+
if [ -n "$INPUT_BUILDKIT_CERT_CA" ] && [ -n "$INPUT_BUILDKIT_CERT" ] && [ -n "$INPUT_BUILDKIT_CERT_KEY" ]; then
261+
# Use inline certificates
262+
cert_dir=$(mktemp -d)
263+
echo "$INPUT_BUILDKIT_CERT_CA" > "$cert_dir/ca.pem"
264+
echo "$INPUT_BUILDKIT_CERT" > "$cert_dir/cert.pem"
265+
echo "$INPUT_BUILDKIT_CERT_KEY" > "$cert_dir/key.pem"
266+
buildctl_mtls_options="--tlscacert $cert_dir/ca.pem --tlscert $cert_dir/cert.pem --tlskey $cert_dir/key.pem"
267+
else
268+
# Use file-based certificates
269+
buildctl_mtls_options="--tlscacert $INPUT_BUILDKIT_CERT_CA_FILE --tlscert $INPUT_BUILDKIT_CERT_FILE --tlskey $INPUT_BUILDKIT_CERT_KEY_FILE"
270+
fi
271+
else
272+
buildctl_cmd="buildctl-daemonless.sh"
273+
fi
274+
275+
# Function to run buildctl with all options
276+
runBuildctl() {
277+
local cmd="$1"
278+
local mtls_options="$2"
279+
280+
$cmd $mtls_options build \
281+
--frontend dockerfile.v0 \
282+
--local context=$INPUT_CONTEXT \
283+
--local dockerfile=$INPUT_CONTEXT \
284+
--opt platform=$INPUT_PLATFORMS \
285+
$(echo "$INPUT_BUILD_ARGS" | sed -r '/^\s*$/d' - | sed -r 's/(.*)/--opt build-arg:\1 \\/' -) \
286+
$(echo "$INPUT_LABELS" | sed -r '/^\s*$/d' - | sed -r 's/(.*)/--opt label:\1 \\/' -) \
287+
"${secret_args[@]}" \
288+
"$target_args" \
289+
$cache_options \
290+
--opt filename=./$INPUT_DOCKERFILE \
291+
--output type=image,\"name=$(echo "$INPUT_TAGS" | paste -sd ',' -)\",push=$INPUT_PUSH
292+
}
293+
294+
# Create temporary file for capturing output
295+
tempfile=$(mktemp)
296+
297+
# Run buildctl and capture output
298+
set +e
299+
runBuildctl "$buildctl_cmd" "$buildctl_mtls_options" 2>&1 | tee "$tempfile"
300+
status=$?
301+
set -e
302+
303+
# Handle fallback if enabled and needed
304+
if [ "$INPUT_FALLBACK_ENABLED" = "true" ] && [ "$status" -ne 0 ]; then
305+
echo "Command failed. Checking if fallback is needed..."
306+
if grep -q -e "listing workers for Build: failed to list workers: Unavailable" -e "closing transport due to: connection error" "$tempfile"; then
307+
echo "buildkit service unavailable, falling back to local build"
308+
# Use buildctl-daemonless.sh instead
309+
buildctl_cmd="buildctl-daemonless.sh"
310+
buildctl_mtls_options=""
311+
runBuildctl "$buildctl_cmd" "$buildctl_mtls_options"
312+
else
313+
exit $status
314+
fi
315+
elif [ "$status" -ne 0 ]; then
316+
exit $status
317+
fi
318+
319+
echo "Build succeeded."

0 commit comments

Comments
 (0)