Skip to content

Commit 92f2495

Browse files
authored
CSI release workflow (#80)
This PR ensures the CSI release is triggered when version in `VERSION` file is not present in GitHub releases. When a new commit is pushed on `main` branch: - The CSI image `latest-edge` and Helm chart `v0-latest-edge` are updated. - If version in `VERSION` file does not exist in GitHub releases: - Publishes CSI image and Helm chart under version `vX.Y.Z`. It also tags them with major and minor version. For example, image tags for version `v1.2.3` would be `v1`, `v1.2`, `v1.2.3`. GitHub release is created only if all the tests have passed, the CSI image is uploaded, and the Helm chart is published. If anything goes wrong in the process, the action can be retried. Even if image or helm chart are published, but the release fails, the next run republishes the image/chart. If successful on next try, release is created. --- Version file | GH release exists? | Publish versions --- | --- | --- v0.0.1 | Y | `latest-edge` 0.0.2 | N | No release, we expect version in format `v<major>.<minor>.<bugfix>` v0.0.2 | N | `latest-edge`, `v0.0.2`, `v0.0`, `v0`
2 parents fd8506b + 3b4382a commit 92f2495

File tree

15 files changed

+203
-524
lines changed

15 files changed

+203
-524
lines changed

.github/actions/setup-k8s/action.yml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ inputs:
1818
description: Path where Kubeconfig file will be stored
1919
default: "/home/runner/.kube/config"
2020
type: string
21+
k8s-csi-image-tag:
22+
description: Tag of the LXD CSI driver image to deploy to the cluster
23+
default: ""
24+
type: string
25+
k8s-csi-image-path:
26+
description: Path to the LXD CSI driver image tarball to import to cluster nodes (empty to skip)
27+
default: ""
28+
type: string
2129

2230
runs:
2331
using: composite
@@ -29,6 +37,7 @@ runs:
2937
K8S_NODE_COUNT: ${{ inputs.k8s-node-count }}
3038
K8S_SNAP_CHANNEL: ${{ inputs.k8s-snap-channel }}
3139
K8S_KUBECONFIG_PATH: ${{ inputs.k8s-kubeconfig-path }}
32-
K8S_CSI_IMAGE_PATH: lxd-csi-driver.tar
40+
K8S_CSI_IMAGE_PATH: ${{ inputs.k8s-csi-image-path }}
41+
K8S_CSI_IMAGE_TAG: ${{ inputs.k8s-csi-image-tag }}
3342
run: |
3443
${{ github.action_path }}/k8s.sh deploy

.github/actions/setup-k8s/k8s.sh

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,13 @@ cleanup() {
3030
trap cleanup EXIT INT TERM
3131

3232
setEnv() {
33-
# Precheck kubectl is installed.
34-
if ! command -v kubectl &> /dev/null; then
35-
echo "Error: kubectl is not installed. Use 'snap install kubectl --classic' to install it."
36-
exit 1
37-
fi
38-
39-
# Precheck lxc is installed.
40-
if ! command -v lxc &> /dev/null; then
41-
echo "Error: lxc is not installed"
42-
exit 1
43-
fi
33+
# Precheck required binaries are installed.
34+
for cmd in kubectl helm lxc; do
35+
if ! command -v "${cmd}" &> /dev/null; then
36+
echo "Error: ${cmd} is not installed."
37+
exit 1
38+
fi
39+
done
4440

4541
# Precheck that LXD is accessible and trusts us.
4642
if [ $(lxc query /1.0 | jq -r .auth) != "trusted" ]; then
@@ -59,7 +55,8 @@ setEnv() {
5955
: "${K8S_NODE_COUNT:=1}"
6056
: "${K8S_SNAP_CHANNEL:=latest/edge}"
6157
: "${K8S_KUBECONFIG_PATH:=${ROOT_DIR}/.kube/${K8S_CLUSTER_NAME}.yml}" # Do not use "${HOME}/..." by default to avoid overwriting user's kubeconfig.
62-
# K8S_CSI_IMAGE_PATH - Used to import locally built CSI image tarball to all cluster nodes.
58+
: "${K8S_CSI_IMAGE_PATH:=}" # Path to the custom LXD CSI driver image to import to cluster nodes.
59+
: "${K8S_CSI_IMAGE_TAG:=latest-edge}"
6360

6461
# LXD instance, storage, and network configuration.
6562
: "${LXD_INSTANCE_IMAGE:=ubuntu-minimal-daily:24.04}"
@@ -323,11 +320,25 @@ k8sWaitReady() {
323320
echo "Error: Kubernetes cluster is not ready after ${timeout} seconds!" >&2
324321
' ERR
325322

326-
echo "===> Waiting for Kubernetes nodes to become ready ..."
327-
kubectl --kubeconfig "${kubeconfigPath}" wait --for=condition=Ready nodes --all --timeout="${timeout}s"
323+
local deadline=$((SECONDS + timeout))
324+
local nodesReady=0
325+
local podsReady=0
326+
327+
echo "===> Waiting for all Kubernetes nodes and pods to be ready ..."
328+
while (( SECONDS < deadline )); do
329+
[ "${nodesReady}" -eq 0 ] && kubectl --kubeconfig "${kubeconfigPath}" wait --for=condition=Ready nodes --all --timeout=30s && nodesReady=1
330+
[ "${podsReady}" -eq 0 ] && kubectl --kubeconfig "${kubeconfigPath}" wait --for=condition=Ready pods --all -A --timeout=30s && podsReady=1
331+
332+
if [ "${nodesReady}" -eq 1 ] && [ "${podsReady}" -eq 1 ]; then
333+
break
334+
fi
328335

329-
echo "===> Waiting for all system pods to become ready ..."
330-
kubectl --kubeconfig "${kubeconfigPath}" wait --for=condition=Ready pods --all --all-namespaces --timeout="${timeout}s"
336+
sleep 2
337+
done
338+
339+
if (( SECONDS >= deadline )); then
340+
return 1
341+
fi
331342

332343
trap - ERR
333344
}
@@ -371,6 +382,7 @@ k8sImportImageTarball() {
371382
return 1
372383
fi
373384

385+
# Import the image tarball to all cluster nodes.
374386
for i in $(seq 1 "${K8S_NODE_COUNT}"); do
375387
instance="${K8S_CLUSTER_NAME}-node-${i}"
376388
echo "Importing image ${imagePath} to node ${instance} ..."
@@ -385,7 +397,8 @@ k8sImportImageTarball() {
385397
# It creates the necessary namespace and applies the deployment manifests.
386398
installLXDCSIDriver() {
387399
local kubeconfigPath="${K8S_KUBECONFIG_PATH}"
388-
local csiDeployPath="${ROOT_DIR}/deploy"
400+
local imagePath="${K8S_CSI_IMAGE_PATH}"
401+
local chartRepo="oci://ghcr.io/canonical/charts/lxd-csi-driver"
389402
local project="${LXD_PROJECT_NAME}"
390403
local name="${K8S_CLUSTER_NAME}-lxd-csi"
391404
local group="${name}-group"
@@ -410,7 +423,24 @@ installLXDCSIDriver() {
410423
echo "===> Installing LXD CSI driver ..."
411424
kubectl --kubeconfig "${kubeconfigPath}" create namespace lxd-csi --save-config
412425
kubectl --kubeconfig "${kubeconfigPath}" create secret generic lxd-csi-secret --namespace lxd-csi --from-literal=token="${token}"
413-
kubectl --kubeconfig "${kubeconfigPath}" apply -f "${csiDeployPath}"
426+
427+
if [ "${K8S_CSI_IMAGE_PATH}" != "" ]; then
428+
# Build image from source and import it to cluster nodes.
429+
k8sImportImageTarball "${imagePath}"
430+
fi
431+
432+
if [ "${K8S_CSI_IMAGE_TAG}" = "dev" ]; then
433+
# Use local chart from repository.
434+
chartRepo="${ROOT_DIR}/charts"
435+
fi
436+
437+
helm install lxd-csi "${chartRepo}" \
438+
--kubeconfig "${kubeconfigPath}" \
439+
--namespace lxd-csi \
440+
--timeout 120s \
441+
--atomic \
442+
--wait \
443+
--set driver.image.tag="${K8S_CSI_IMAGE_TAG}"
414444
}
415445

416446
# help prints the usage information for this script.
@@ -468,18 +498,16 @@ case "${cmd}" in
468498
# Copy kubeconfig to host and adjust the server address.
469499
k8sCopyKubeconfig "${firstNode}" "${K8S_KUBECONFIG_PATH}"
470500

471-
if [ "${K8S_CSI_IMAGE_PATH:-}" != "" ]; then
472-
k8sImportImageTarball "${K8S_CSI_IMAGE_PATH}"
473-
fi
474-
475-
# Ensure cluster is ready before installing the CSI driver.
501+
# Ensure cluster is ready.
476502
k8sWaitReady
477503

478-
# Install the LXD CSI driver.
479-
installLXDCSIDriver
504+
if [ "${K8S_CSI_IMAGE_TAG}" != "" ]; then
505+
# Install the LXD CSI driver.
506+
installLXDCSIDriver
480507

481-
# Wait for the CSI to become ready.
482-
k8sWaitReady
508+
# Wait for the CSI to become ready.
509+
k8sWaitReady
510+
fi
483511

484512
echo "==> Done"
485513
echo -e "\nKubernetes cluster:"

.github/workflows/e2e-tests.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ jobs:
4040
with:
4141
go-version-file: 'go.mod'
4242

43+
# Build the image before disabling Docker.
4344
- name: Build CSI driver image
45+
id: image
4446
run: |
4547
set -e
4648
make image-export
49+
echo "path=lxd-csi-driver.tar" >> "$GITHUB_OUTPUT"
4750
4851
# Disable Docker to avoid conflicts with LXD.
4952
- name: Disable Docker
@@ -61,16 +64,19 @@ jobs:
6164
channel: latest/edge
6265

6366
- name: Set Kubeconfig path
67+
id: kubeconfig
6468
run: |
65-
echo "K8S_KUBECONFIG_PATH=${HOME}/.kube/config" >> $GITHUB_ENV
69+
echo "path=${HOME}/.kube/config" >> "$GITHUB_OUTPUT"
6670
6771
- name: Deploy Kubernetes cluster
6872
uses: ./.github/actions/setup-k8s
6973
with:
7074
k8s-cluster-name: ${{ env.K8S_CLUSTER_NAME }}
7175
k8s-node-count: 2
7276
k8s-snap-channel: latest/edge
73-
k8s-kubeconfig-path: ${{ env.K8S_KUBECONFIG_PATH }}
77+
k8s-kubeconfig-path: ${{ steps.kubeconfig.outputs.path }}
78+
k8s-csi-image-path: ${{ steps.image.outputs.path }}
79+
k8s-csi-image-tag: dev
7480

7581
- name: Print cluster info
7682
run: |
@@ -84,6 +90,7 @@ jobs:
8490
- name: Run E2E tests
8591
env:
8692
TEST_LXD_STORAGE_DRIVERS: "${{ matrix.storage_driver }}"
93+
K8S_KUBECONFIG_PATH: ${{ steps.kubeconfig.outputs.path }}
8794
run: |
8895
set -e
8996
cd test/e2e

.github/workflows/publish.yml

Lines changed: 105 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
name: Publish CSI image
1+
name: Release CSI image
22
on:
33
push:
44
branches:
55
- main
6-
tags:
7-
- "v*"
86

97
permissions:
108
contents: read
@@ -22,38 +20,89 @@ env:
2220
VERSION: ${{ github.ref_name == 'main' && 'latest-edge' || github.ref_name }}
2321

2422
jobs:
25-
tests:
26-
uses: ./.github/workflows/tests.yml
27-
28-
e2e-tests:
29-
needs: tests
30-
uses: ./.github/workflows/e2e-tests.yml
31-
32-
# Publish container image to GHCR if tests passed and we are on main branch.
33-
publish:
23+
# Precheck job checks whether the CSI image (and Helm chart) publishing is required, and
24+
# whether a new GitHub release should be created.
25+
# The image/chart are published as "latest-edge" on each push to main branch.
26+
# If the GitHub release matching the version in "VERSION" file does not exist yet,
27+
# a new release is created.
28+
precheck:
3429
runs-on: ubuntu-24.04
35-
needs: e2e-tests
36-
if: ${{ github.repository == 'canonical/lxd-csi-driver' }}
37-
permissions:
38-
contents: read
39-
packages: write
30+
outputs:
31+
versions: ${{ steps.read.outputs.versions }}
32+
release: ${{ steps.read.outputs.release }}
4033
steps:
41-
- name: Validate release version
34+
- name: Checkout
35+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
36+
37+
- name: Determine versions to be published
38+
id: read
4239
shell: bash
40+
env:
41+
# Set GH_TOKEN to authenticate "gh" CLI.
42+
GH_TOKEN: ${{ github.token }}
4343
run: |
4444
set -e
45-
isSemver=false
46-
if [[ "${VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
47-
isSemver=true
45+
release="" # GitHub release name.
46+
versions="" # CSI image and chart tags.
47+
48+
# Latest release only on the main branch.
49+
if [ "${{ github.ref_name }}" = "main" ]; then
50+
versions="${versions} latest-edge"
4851
fi
4952
50-
if [[ "${VERSION}" != "latest-edge" && "${isSemver}" != "true" ]]; then
51-
echo "Error: Invalid version '${VERSION}'. It must be either 'latest-edge' or a semantic version prefixed with 'v' (e.g., v1.2.3)."
53+
# Read and validate current version.
54+
version=$(cat VERSION)
55+
if [[ ! "${version}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
56+
echo "Error: Invalid version '${version}' in VERSION file. It must be a semantic version prefixed with 'v'."
5257
exit 1
5358
fi
5459
55-
echo "IS_SEMVER=${isSemver}" >> "$GITHUB_ENV"
60+
# Check if the version already exists.
61+
# If it does not exist, add versions that need to be published to the list.
62+
foundRelease=$(gh release list -R ${{ github.repository }} --json tagName --jq ".[] | select(.tagName == \"${version}\").tagName")
63+
if [ "${foundRelease}" = "" ]; then
64+
release="${version}"
65+
versions="${versions} ${version}" # Example: v1.2.3
66+
versions="${versions} ${version%.*}" # Example: v1.2
67+
versions="${versions} ${version%%.*}" # Example: v1
68+
fi
69+
70+
# Trim potential surrounding whitespace.
71+
versions=$(echo "$versions" | xargs)
72+
73+
# Output versions for publishing.
74+
echo "versions=${versions}" >> "$GITHUB_OUTPUT"
75+
echo "release=${release}" >> "$GITHUB_OUTPUT"
76+
77+
- name: Output versions
78+
run: |
79+
echo "Versions to be published: ${{ steps.read.outputs.versions }}"
80+
echo "Release version: ${{ steps.read.outputs.release }}"
81+
82+
tests:
83+
needs:
84+
- precheck
85+
permissions:
86+
contents: read
87+
uses: ./.github/workflows/tests.yml
88+
89+
e2e-tests:
90+
uses: ./.github/workflows/e2e-tests.yml
91+
needs:
92+
- precheck
93+
- tests
5694

95+
# Publish container image to GHCR if tests passed and we are on main branch.
96+
publish:
97+
if: ${{ github.repository == 'canonical/lxd-csi-driver' && needs.precheck.outputs.versions != '' }}
98+
needs:
99+
- precheck
100+
- e2e-tests
101+
runs-on: ubuntu-24.04
102+
permissions:
103+
contents: write # Required for creating releases.
104+
packages: write # Required for publishing to GHCR.
105+
steps:
57106
- name: Checkout
58107
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
59108

@@ -62,11 +111,20 @@ jobs:
62111
with:
63112
go-version-file: 'go.mod'
64113

65-
- name: Build image
114+
- name: Build image and create image tags
115+
id: image
66116
run: |
67117
set -e
68118
make build
69119
120+
tags=""
121+
for version in ${{ needs.precheck.outputs.versions }}; do
122+
tags=${tags},${{ env.CONTAINER_REGISTRY }}/${{ github.repository }}:${version}
123+
done
124+
125+
# Output tags with leading comma removed.
126+
echo "tags=${tags#,}" >> "$GITHUB_OUTPUT"
127+
70128
- name: Log in to the container registry
71129
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
72130
with:
@@ -80,7 +138,7 @@ jobs:
80138
with:
81139
context: .
82140
push: true
83-
tags: ${{ env.CONTAINER_REGISTRY }}/${{ github.repository }}:${{ env.VERSION }}
141+
tags: ${{ steps.image.outputs.tags }}
84142

85143
- name: Log in to the Helm registry
86144
run: |
@@ -93,12 +151,24 @@ jobs:
93151
- name: Publish new Chart
94152
run: |
95153
set -e
96-
chartVersion="${{ env.VERSION }}"
97-
98-
if [ "${{ env.IS_SEMVER }}" != "true" ]; then
99-
# Prepend "v0.0.0-" to the version to satisfy Helm versioning requirements.
100-
chartVersion="v0.0.0-${chartVersion}"
101-
fi
102-
103-
helm package charts --version "${chartVersion}" --app-version "${{ env.VERSION }}"
104-
helm push "lxd-csi-driver-${chartVersion}.tgz" "oci://${{ env.CONTAINER_REGISTRY }}/${{ github.repository_owner }}/charts"
154+
for version in ${{ needs.precheck.outputs.versions }}; do
155+
chartVersion="${version}"
156+
157+
# If the version does not start with an integer (prefix 'v' is optional),
158+
# we prefix Chart version with 'v0-' prefix to satisfy Helm chart version constraints.
159+
if [[ ! "${version}" =~ ^v?[0-9] ]]; then
160+
chartVersion="v0-${version}"
161+
fi
162+
163+
helm package charts --version "${chartVersion}" --app-version "${version}"
164+
helm push "lxd-csi-driver-${chartVersion}.tgz" "oci://${{ env.CONTAINER_REGISTRY }}/${{ github.repository_owner }}/charts"
165+
done
166+
167+
- name: Make a release
168+
uses: softprops/action-gh-release@v2
169+
if: ${{ needs.precheck.outputs.release != '' }}
170+
with:
171+
name: ${{ needs.precheck.outputs.release }}
172+
tag_name: ${{ needs.precheck.outputs.release }}
173+
generate_release_notes: true
174+
draft: false

Makefile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
REGISTRY=ghcr.io
22
IMAGE=canonical/lxd-csi-driver
3-
# Use latest-edge for development builds to match what is in deploy/* manifests.
4-
VERSION?=latest-edge
3+
VERSION?=dev
54

65
build:
76
@echo "> Building LXD CSI ...";

0 commit comments

Comments
 (0)