Skip to content

Commit 84deb1f

Browse files
Use charmcraft test & concierge (#573)
1 parent 66bfe57 commit 84deb1f

File tree

77 files changed

+1857
-1168
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+1857
-1168
lines changed

.github/workflows/ci.yaml

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -63,43 +63,14 @@ jobs:
6363
uses: canonical/data-platform-workflows/.github/workflows/[email protected]
6464

6565
integration-test:
66-
strategy:
67-
fail-fast: false
68-
matrix:
69-
juju:
70-
- agent: 2.9.51 # renovate: juju-agent-pin-minor
71-
libjuju: ^2
72-
allure_on_amd64: false
73-
- agent: 3.6.2 # renovate: juju-agent-pin-minor
74-
allure_on_amd64: true
75-
architecture:
76-
- amd64
77-
include:
78-
- juju:
79-
agent: 3.6.2 # renovate: juju-agent-pin-minor
80-
allure_on_amd64: true
81-
architecture: arm64
82-
name: Integration | ${{ matrix.juju.agent }} | ${{ matrix.architecture }}
66+
name: Integration test charm
8367
needs:
8468
- lint
8569
- unit-test
8670
- build
87-
uses: canonical/data-platform-workflows/.github/workflows/integration_test_charm.yaml@v29.0.0
71+
uses: ./.github/workflows/integration_test.yaml
8872
with:
8973
artifact-prefix: ${{ needs.build.outputs.artifact-prefix }}
90-
architecture: ${{ matrix.architecture }}
91-
cloud: microk8s
92-
microk8s-snap-channel: 1.31-strict/stable # renovate: latest microk8s
93-
juju-agent-version: ${{ matrix.juju.agent }}
94-
libjuju-version-constraint: ${{ matrix.juju.libjuju }}
95-
_beta_allure_report: ${{ matrix.juju.allure_on_amd64 && matrix.architecture == 'amd64' }}
96-
secrets:
97-
# GitHub appears to redact each line of a multi-line secret
98-
# Avoid putting `{` or `}` on a line by itself so that it doesn't get redacted in logs
99-
integration-test: |
100-
{ "AWS_ACCESS_KEY": "${{ secrets.AWS_ACCESS_KEY }}",
101-
"AWS_SECRET_KEY": "${{ secrets.AWS_SECRET_KEY }}",
102-
"GCP_ACCESS_KEY": "${{ secrets.GCP_ACCESS_KEY }}",
103-
"GCP_SECRET_KEY": "${{ secrets.GCP_SECRET_KEY }}", }
74+
secrets: inherit
10475
permissions:
105-
contents: write # Needed for Allure Report beta
76+
contents: write # Needed for Allure Report
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
on:
2+
workflow_call:
3+
inputs:
4+
artifact-prefix:
5+
description: |
6+
Prefix for charm package GitHub artifact(s)
7+
8+
Use canonical/data-platform-workflows build_charm.yaml to build the charm(s)
9+
required: true
10+
type: string
11+
12+
jobs:
13+
collect-integration-tests:
14+
name: Collect integration test spread jobs
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 5
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
- name: Set up environment
21+
run: |
22+
sudo snap install charmcraft --classic
23+
pipx install tox poetry
24+
- name: Collect spread jobs
25+
id: collect-jobs
26+
shell: python
27+
run: |
28+
import json
29+
import os
30+
import subprocess
31+
32+
spread_jobs = (
33+
subprocess.run(
34+
["charmcraft", "test", "--list", "github-ci"], capture_output=True, check=True, text=True
35+
)
36+
.stdout.strip()
37+
.split("\n")
38+
)
39+
jobs = []
40+
for job in spread_jobs:
41+
# Example `job`: "github-ci:ubuntu-24.04:tests/spread/test_charm.py:juju36"
42+
_, runner, task, variant = job.split(":")
43+
# Example: "test_charm.py"
44+
task = task.removeprefix("tests/spread/")
45+
if runner.endswith("-arm"):
46+
architecture = "arm64"
47+
else:
48+
architecture = "amd64"
49+
# Example: "test_charm.py:juju36 | amd64"
50+
name = f"{task}:{variant} | {architecture}"
51+
# ":" character not valid in GitHub Actions artifact
52+
name_in_artifact = f"{task}-{variant}-{architecture}"
53+
jobs.append({
54+
"spread_job": job,
55+
"name": name,
56+
"name_in_artifact": name_in_artifact,
57+
"runner": runner,
58+
})
59+
output = f"jobs={json.dumps(jobs)}"
60+
print(output)
61+
with open(os.environ["GITHUB_OUTPUT"], "a") as file:
62+
file.write(output)
63+
- name: Generate Allure default test results
64+
if: ${{ github.event_name == 'schedule' && github.run_attempt == '1' }}
65+
run: tox run -e integration -- tests/integration --allure-default-dir=allure-default-results
66+
- name: Upload Allure default results
67+
# Default test results in case the integration tests time out or runner set up fails
68+
# (So that Allure report will show "unknown"/"failed" test result, instead of omitting the test)
69+
if: ${{ github.event_name == 'schedule' && github.run_attempt == '1' }}
70+
uses: actions/upload-artifact@v4
71+
with:
72+
name: allure-default-results-integration-test
73+
path: allure-default-results/
74+
if-no-files-found: error
75+
outputs:
76+
jobs: ${{ steps.collect-jobs.outputs.jobs }}
77+
78+
integration-test:
79+
strategy:
80+
fail-fast: false
81+
matrix:
82+
job: ${{ fromJSON(needs.collect-integration-tests.outputs.jobs) }}
83+
name: ${{ matrix.job.name }}
84+
needs:
85+
- collect-integration-tests
86+
runs-on: ${{ matrix.job.runner }}
87+
timeout-minutes: 217 # Sum of steps `timeout-minutes` + 5
88+
steps:
89+
- name: Free up disk space
90+
timeout-minutes: 1
91+
run: |
92+
printf '\nDisk usage before cleanup\n'
93+
df --human-readable
94+
# Based on https://github.com/actions/runner-images/issues/2840#issuecomment-790492173
95+
rm -r /opt/hostedtoolcache/
96+
printf '\nDisk usage after cleanup\n'
97+
df --human-readable
98+
- name: Checkout
99+
timeout-minutes: 3
100+
uses: actions/checkout@v4
101+
- name: Set up environment
102+
timeout-minutes: 5
103+
run: sudo snap install charmcraft --classic
104+
# TODO: remove when https://github.com/canonical/charmcraft/issues/2105 and
105+
# https://github.com/canonical/charmcraft/issues/2130 fixed
106+
- run: |
107+
sudo snap install go --classic
108+
go install github.com/snapcore/spread/cmd/spread@latest
109+
- name: Download packed charm(s)
110+
timeout-minutes: 5
111+
uses: actions/download-artifact@v4
112+
with:
113+
pattern: ${{ inputs.artifact-prefix }}-*
114+
merge-multiple: true
115+
- name: Run spread job
116+
timeout-minutes: 180
117+
id: spread
118+
# TODO: replace with `charmcraft test` when
119+
# https://github.com/canonical/charmcraft/issues/2105 and
120+
# https://github.com/canonical/charmcraft/issues/2130 fixed
121+
run: ~/go/bin/spread -vv -artifacts=artifacts '${{ matrix.job.spread_job }}'
122+
env:
123+
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
124+
AWS_SECRET_KEY: ${{ secrets.AWS_SECRET_KEY }}
125+
GCP_ACCESS_KEY: ${{ secrets.GCP_ACCESS_KEY }}
126+
GCP_SECRET_KEY: ${{ secrets.GCP_SECRET_KEY }}
127+
- name: Upload Allure results
128+
timeout-minutes: 3
129+
# Only upload results from one spread system & one spread variant
130+
# Allure can only process one result per pytest test ID. If parameterization is done via
131+
# spread instead of pytest, there will be overlapping pytest test IDs.
132+
if: ${{ (success() || (failure() && steps.spread.outcome == 'failure')) && startsWith(matrix.job.spread_job, 'github-ci:ubuntu-24.04:') && endsWith(matrix.job.spread_job, ':juju36') && github.event_name == 'schedule' && github.run_attempt == '1' }}
133+
uses: actions/upload-artifact@v4
134+
with:
135+
name: allure-results-integration-test-${{ matrix.job.name_in_artifact }}
136+
path: artifacts/${{ matrix.job.spread_job }}/allure-results/
137+
if-no-files-found: error
138+
- timeout-minutes: 1
139+
if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }}
140+
run: snap list
141+
- name: Select model
142+
timeout-minutes: 1
143+
# `!contains(matrix.job.spread_job, 'juju29')` workaround for juju 2 error:
144+
# "ERROR cannot acquire lock file to read controller concierge-microk8s: unable to open
145+
# /tmp/juju-store-lock-3635383939333230: permission denied"
146+
# Unable to workaround error with `sudo rm /tmp/juju-*`
147+
if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }}
148+
id: juju-switch
149+
run: |
150+
# sudo needed since spread runs scripts as root
151+
# "testing" is default model created by concierge
152+
sudo juju switch testing
153+
mkdir ~/logs/
154+
- name: juju status
155+
timeout-minutes: 1
156+
if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }}
157+
run: sudo juju status --color --relations | tee ~/logs/juju-status.txt
158+
- name: juju debug-log
159+
timeout-minutes: 3
160+
if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }}
161+
run: sudo juju debug-log --color --replay --no-tail | tee ~/logs/juju-debug-log.txt
162+
- name: jhack tail
163+
timeout-minutes: 3
164+
if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }}
165+
run: sudo jhack tail --printer raw --replay --no-watch | tee ~/logs/jhack-tail.txt
166+
- name: Upload logs
167+
timeout-minutes: 5
168+
if: ${{ !contains(matrix.job.spread_job, 'juju29') && (success() || (failure() && steps.spread.outcome == 'failure')) }}
169+
uses: actions/upload-artifact@v4
170+
with:
171+
name: logs-integration-test-${{ matrix.job.name_in_artifact }}
172+
path: ~/logs/
173+
if-no-files-found: error
174+
- name: Disk usage
175+
timeout-minutes: 1
176+
if: ${{ success() || (failure() && steps.spread.outcome == 'failure') }}
177+
run: df --human-readable
178+
179+
allure-report:
180+
# TODO future improvement: use concurrency group for job
181+
name: Publish Allure report
182+
if: ${{ !cancelled() && github.event_name == 'schedule' && github.run_attempt == '1' }}
183+
needs:
184+
- integration-test
185+
runs-on: ubuntu-latest
186+
timeout-minutes: 5
187+
steps:
188+
- name: Download Allure
189+
# Following instructions from https://allurereport.org/docs/install-for-linux/#install-from-a-deb-package
190+
run: gh release download --repo allure-framework/allure2 --pattern 'allure_*.deb'
191+
env:
192+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
193+
- name: Install Allure
194+
run: |
195+
sudo apt-get update
196+
sudo apt-get install ./allure_*.deb -y
197+
# For first run, manually create branch with no history
198+
# (e.g.
199+
# git checkout --orphan gh-pages-beta
200+
# git rm -rf .
201+
# touch .nojekyll
202+
# git add .nojekyll
203+
# git commit -m "Initial commit"
204+
# git push origin gh-pages-beta
205+
# )
206+
- name: Checkout GitHub pages branch
207+
uses: actions/checkout@v4
208+
with:
209+
ref: gh-pages-beta
210+
path: repo/
211+
- name: Download default test results
212+
# Default test results in case the integration tests time out or runner set up fails
213+
# (So that Allure report will show "unknown"/"failed" test result, instead of omitting the test)
214+
uses: actions/download-artifact@v4
215+
with:
216+
path: allure-default-results/
217+
name: allure-default-results-integration-test
218+
- name: Download test results
219+
uses: actions/download-artifact@v4
220+
with:
221+
path: allure-results/
222+
pattern: allure-results-integration-test-*
223+
merge-multiple: true
224+
- name: Combine Allure default results & actual results
225+
# For every test: if actual result available, use that. Otherwise, use default result
226+
# So that, if actual result not available, Allure report will show "unknown"/"failed" test result
227+
# instead of omitting the test
228+
shell: python
229+
run: |
230+
import dataclasses
231+
import json
232+
import pathlib
233+
234+
235+
@dataclasses.dataclass(frozen=True)
236+
class Result:
237+
test_case_id: str
238+
path: pathlib.Path
239+
240+
def __eq__(self, other):
241+
if not isinstance(other, type(self)):
242+
return False
243+
return self.test_case_id == other.test_case_id
244+
245+
246+
actual_results = pathlib.Path("allure-results")
247+
default_results = pathlib.Path("allure-default-results")
248+
249+
results: dict[pathlib.Path, set[Result]] = {
250+
actual_results: set(),
251+
default_results: set(),
252+
}
253+
for directory, results_ in results.items():
254+
for path in directory.glob("*-result.json"):
255+
with path.open("r") as file:
256+
id_ = json.load(file)["testCaseId"]
257+
results_.add(Result(id_, path))
258+
259+
actual_results.mkdir(exist_ok=True)
260+
261+
missing_results = results[default_results] - results[actual_results]
262+
for default_result in missing_results:
263+
# Move to `actual_results` directory
264+
default_result.path.rename(actual_results / default_result.path.name)
265+
- name: Load test report history
266+
run: |
267+
if [[ -d repo/_latest/history/ ]]
268+
then
269+
echo 'Loading history'
270+
cp -r repo/_latest/history/ allure-results/
271+
fi
272+
- name: Create executor.json
273+
shell: python
274+
run: |
275+
# Reverse engineered from https://github.com/simple-elf/allure-report-action/blob/eca283b643d577c69b8e4f048dd6cd8eb8457cfd/entrypoint.sh
276+
import json
277+
278+
DATA = {
279+
"name": "GitHub Actions",
280+
"type": "github",
281+
"buildOrder": ${{ github.run_number }}, # TODO future improvement: use run ID
282+
"buildName": "Run ${{ github.run_id }}",
283+
"buildUrl": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}",
284+
"reportUrl": "../${{ github.run_number }}/",
285+
}
286+
with open("allure-results/executor.json", "w") as file:
287+
json.dump(DATA, file)
288+
- name: Generate Allure report
289+
run: allure generate
290+
- name: Create index.html
291+
shell: python
292+
run: |
293+
DATA = f"""<!DOCTYPE html>
294+
<meta charset="utf-8">
295+
<meta http-equiv="cache-control" content="no-cache">
296+
<meta http-equiv="refresh" content="0; url=${{ github.run_number }}">
297+
"""
298+
with open("repo/index.html", "w") as file:
299+
file.write(DATA)
300+
- name: Update GitHub pages branch
301+
working-directory: repo/
302+
# TODO future improvement: commit message
303+
run: |
304+
mkdir '${{ github.run_number }}'
305+
rm -f _latest
306+
ln -s '${{ github.run_number }}' _latest
307+
cp -r ../allure-report/. _latest/
308+
git add .
309+
git config user.name "GitHub Actions"
310+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
311+
git commit -m "Allure report ${{ github.run_number }}"
312+
# Uses token set in checkout step
313+
git push origin gh-pages-beta

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
uses: ./.github/workflows/ci.yaml
3333
secrets: inherit
3434
permissions:
35-
contents: write # Needed for Allure Report beta
35+
contents: write # Needed for Allure Report
3636

3737
release:
3838
name: Release charm

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ poetry install
4141
tox run -e format # update your code according to linting rules
4242
tox run -e lint # code style
4343
tox run -e unit # unit tests
44-
tox run -e integration # integration tests
44+
charmcraft test lxd-vm: # integration tests
4545
tox # runs 'lint' and 'unit' environments
4646
```
4747

concierge.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
juju:
2+
model-defaults:
3+
logging-config: <root>=INFO; unit=DEBUG
4+
providers:
5+
microk8s:
6+
enable: true
7+
bootstrap: true
8+
addons:
9+
- dns
10+
- hostpath-storage
11+
host:
12+
snaps:
13+
jhack:
14+
channel: latest/edge
15+
connections:
16+
- jhack:dot-local-share-juju snapd

0 commit comments

Comments
 (0)