Skip to content

Commit 41a4f54

Browse files
committed
test: add K8s integration tests and CI workflow
Add integration tests for the Kubernetes deployment support: - tests/test_occ_commands_k8s.py: Python test script with 4 groups: Group A: K8s daemon registration validation (--k8s flag, expose types, nodeport range, manual upstream host, traffic policy, etc.) Group B: Single-role K8s deploy lifecycle (deploy, enable/disable, unregister with/without --rm-data) Group C: Multi-role K8s deploy lifecycle (2 roles, expose only one, enable/disable scales both, unregister cleans all) Group D: Failure & edge cases (bad image rollback, --force unregister, --silent nonexistent app) - .github/workflows/tests-deploy-k8s.yml: CI workflow with 2 jobs: k8s-daemon-validation: fast (~3min), no k3s needed k8s-deploy-lifecycle: full e2e with k3s + HaRP K8s backend Temporarily builds HaRP from dev branch (has K8s support). After merge, switch to ghcr.io/nextcloud/nextcloud-appapi-harp:latest. Signed-off-by: Oleksander Piskun <oleksandr2088@icloud.com>
1 parent da53638 commit 41a4f54

3 files changed

Lines changed: 842 additions & 0 deletions

File tree

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
2+
# SPDX-License-Identifier: MIT
3+
name: Tests - K8s Deploy
4+
5+
on:
6+
pull_request:
7+
branches: [main]
8+
push:
9+
branches: [main]
10+
workflow_dispatch:
11+
inputs:
12+
harp_ref:
13+
description: 'HaRP branch to build from'
14+
default: 'dev' # TEMPORARY: change to 'main' after K8s merge
15+
16+
permissions:
17+
contents: read
18+
19+
concurrency:
20+
group: tests-deploy-k8s-${{ github.head_ref || github.run_id }}
21+
cancel-in-progress: true
22+
23+
env:
24+
HARP_REF: ${{ github.event.inputs.harp_ref || 'dev' }} # TEMPORARY: change to 'main' after K8s merge
25+
HP_SHARED_KEY: 'test_shared_key_12345'
26+
27+
jobs:
28+
k8s-deploy-lifecycle:
29+
runs-on: ubuntu-22.04
30+
name: K8s Deploy Lifecycle (k3s + HaRP)
31+
32+
services:
33+
postgres:
34+
image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest # zizmor: ignore[unpinned-images]
35+
ports:
36+
- 4444:5432/tcp
37+
env:
38+
POSTGRES_USER: root
39+
POSTGRES_PASSWORD: rootpassword
40+
POSTGRES_DB: nextcloud
41+
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
42+
43+
steps:
44+
- name: Set app env
45+
run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
46+
47+
- name: Checkout server
48+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
49+
with:
50+
persist-credentials: false
51+
submodules: true
52+
repository: nextcloud/server
53+
ref: master
54+
55+
- name: Checkout AppAPI
56+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
57+
with:
58+
persist-credentials: false
59+
path: apps/${{ env.APP_NAME }}
60+
61+
- name: Set up php
62+
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2
63+
with:
64+
php-version: '8.3'
65+
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql
66+
coverage: none
67+
ini-file: development
68+
env:
69+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70+
71+
- name: Check composer file existence
72+
id: check_composer
73+
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v2
74+
with:
75+
files: apps/${{ env.APP_NAME }}/composer.json
76+
77+
- name: Set up dependencies
78+
if: steps.check_composer.outputs.files_exists == 'true'
79+
working-directory: apps/${{ env.APP_NAME }}
80+
run: composer i
81+
82+
- name: Set up Nextcloud
83+
env:
84+
DB_PORT: 4444
85+
run: |
86+
mkdir data
87+
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 \
88+
--database-port=$DB_PORT --database-user=root --database-pass=rootpassword \
89+
--admin-user admin --admin-pass admin
90+
./occ config:system:set loglevel --value=0 --type=integer
91+
./occ config:system:set debug --value=true --type=boolean
92+
./occ config:system:set overwrite.cli.url --value http://127.0.0.1 --type=string
93+
./occ app:enable --force ${{ env.APP_NAME }}
94+
95+
- name: Install k3s
96+
run: |
97+
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik --disable servicelb" sh -
98+
sudo chmod 644 /etc/rancher/k3s/k3s.yaml
99+
echo "KUBECONFIG=/etc/rancher/k3s/k3s.yaml" >> $GITHUB_ENV
100+
101+
- name: Wait for k3s and create namespace
102+
run: |
103+
kubectl wait --for=condition=Ready node --all --timeout=120s
104+
kubectl create namespace nextcloud-exapps
105+
106+
- name: Create K8s service account for HaRP
107+
run: |
108+
kubectl -n nextcloud-exapps create serviceaccount harp-sa
109+
kubectl create clusterrolebinding harp-admin \
110+
--clusterrole=cluster-admin \
111+
--serviceaccount=nextcloud-exapps:harp-sa
112+
K3S_TOKEN=$(kubectl -n nextcloud-exapps create token harp-sa --duration=2h)
113+
echo "K3S_TOKEN=${K3S_TOKEN}" >> $GITHUB_ENV
114+
115+
- name: Pre-pull ExApp image into k3s
116+
run: sudo k3s ctr images pull ghcr.io/nextcloud/app-skeleton-python:latest
117+
118+
# TEMPORARY: Build HaRP from dev branch. After K8s merge, change to:
119+
# docker pull ghcr.io/nextcloud/nextcloud-appapi-harp:latest
120+
# docker tag ghcr.io/nextcloud/nextcloud-appapi-harp:latest harp:test
121+
- name: Build HaRP from dev branch
122+
run: |
123+
git clone --branch ${{ env.HARP_REF }} --depth 1 https://github.com/nextcloud/HaRP.git /tmp/HaRP
124+
cd /tmp/HaRP && docker build -t harp:test .
125+
126+
- name: Start HaRP with K8s backend
127+
run: |
128+
docker run --net host --name appapi-harp \
129+
-e HP_SHARED_KEY="${{ env.HP_SHARED_KEY }}" \
130+
-e NC_INSTANCE_URL="http://127.0.0.1" \
131+
-e HP_LOG_LEVEL="debug" \
132+
-e HP_K8S_ENABLED="true" \
133+
-e HP_K8S_API_SERVER="https://127.0.0.1:6443" \
134+
-e HP_K8S_BEARER_TOKEN="${{ env.K3S_TOKEN }}" \
135+
-e HP_K8S_NAMESPACE="nextcloud-exapps" \
136+
-e HP_K8S_VERIFY_SSL="false" \
137+
--restart unless-stopped \
138+
-d harp:test
139+
140+
- name: Start nginx proxy
141+
run: |
142+
docker run --net host --name nextcloud --rm \
143+
-v $(pwd)/apps/${{ env.APP_NAME }}/tests/simple-nginx-NOT-FOR-PRODUCTION.conf:/etc/nginx/conf.d/default.conf:ro \
144+
-d nginx
145+
146+
- name: Start Nextcloud
147+
run: PHP_CLI_SERVER_WORKERS=2 php -S 127.0.0.1:8080 &
148+
149+
- name: Wait for HaRP K8s readiness
150+
run: |
151+
for i in $(seq 1 30); do
152+
if curl -sf http://127.0.0.1:8780/exapps/app_api/info \
153+
-H "harp-shared-key: ${{ env.HP_SHARED_KEY }}" 2>/dev/null | grep -q '"kubernetes"'; then
154+
echo "HaRP is ready with K8s backend"
155+
exit 0
156+
fi
157+
echo "Waiting for HaRP... ($i/30)"
158+
sleep 2
159+
done
160+
echo "HaRP K8s readiness check failed"
161+
docker logs appapi-harp
162+
exit 1
163+
164+
- name: Register K8s daemon
165+
run: |
166+
./occ app_api:daemon:register \
167+
k8s_test "K8s Test" "kubernetes-install" "http" "127.0.0.1:8780" "http://127.0.0.1" \
168+
--harp --harp_shared_key "${{ env.HP_SHARED_KEY }}" --harp_frp_address "127.0.0.1:8782" \
169+
--k8s --k8s_expose_type=nodeport --set-default
170+
./occ app_api:daemon:list
171+
172+
- name: Run K8s integration tests
173+
run: python3 apps/${{ env.APP_NAME }}/tests/test_occ_commands_k8s.py
174+
175+
- name: Collect HaRP logs
176+
if: always()
177+
run: docker logs appapi-harp > harp.log 2>&1
178+
179+
- name: Collect K8s resources
180+
if: always()
181+
run: |
182+
kubectl -n nextcloud-exapps get all -o wide > k8s-resources.txt 2>&1 || true
183+
kubectl -n nextcloud-exapps describe pods > k8s-pods-describe.txt 2>&1 || true
184+
kubectl -n nextcloud-exapps get pvc -o wide >> k8s-resources.txt 2>&1 || true
185+
186+
- name: Show all logs
187+
if: always()
188+
run: |
189+
echo "=== HaRP logs ===" && cat harp.log || true
190+
echo "=== K8s resources ===" && cat k8s-resources.txt || true
191+
echo "=== K8s pods ===" && cat k8s-pods-describe.txt || true
192+
echo "=== Nextcloud log (last 100 lines) ===" && tail -100 data/nextcloud.log || true
193+
194+
- name: Upload HaRP logs
195+
if: always()
196+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
197+
with:
198+
name: k8s_deploy_harp.log
199+
path: harp.log
200+
if-no-files-found: warn
201+
202+
- name: Upload K8s resources
203+
if: always()
204+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
205+
with:
206+
name: k8s_deploy_resources.txt
207+
path: |
208+
k8s-resources.txt
209+
k8s-pods-describe.txt
210+
if-no-files-found: warn
211+
212+
- name: Upload NC logs
213+
if: always()
214+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
215+
with:
216+
name: k8s_deploy_nextcloud.log
217+
path: data/nextcloud.log
218+
if-no-files-found: warn
219+
220+
tests-success:
221+
permissions:
222+
contents: none
223+
runs-on: ubuntu-22.04
224+
needs: [k8s-deploy-lifecycle]
225+
name: K8s-Tests-OK
226+
steps:
227+
- run: echo "K8s tests passed successfully"

tests/php/Service/AppAPIServiceTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
use OCA\AppAPI\Db\ExApp;
1313
use OCA\AppAPI\DeployActions\DockerActions;
14+
use OCA\AppAPI\DeployActions\KubernetesActions;
1415
use OCA\AppAPI\DeployActions\ManualActions;
1516
use OCA\AppAPI\Service\AppAPICommonService;
1617
use OCA\AppAPI\Service\AppAPIService;
1718
use OCA\AppAPI\Service\DaemonConfigService;
19+
use OCA\AppAPI\Service\ExAppDeployOptionsService;
1820
use OCA\AppAPI\Service\ExAppService;
1921
use OCA\AppAPI\Service\HarpService;
2022
use OCP\Http\Client\IClient;
@@ -37,6 +39,7 @@ class AppAPIServiceTest extends TestCase {
3739
private LoggerInterface&MockObject $logger;
3840
private ExAppService&MockObject $exAppService;
3941
private DockerActions&MockObject $dockerActions;
42+
private KubernetesActions&MockObject $kubernetesActions;
4043
private ManualActions&MockObject $manualActions;
4144
private IClient&MockObject $client;
4245
private AppAPICommonService&MockObject $commonService;
@@ -62,9 +65,11 @@ protected function setUp(): void {
6265
$l10nFactory = $this->createMock(IFactory::class);
6366
$this->exAppService = $this->createMock(ExAppService::class);
6467
$this->dockerActions = $this->createMock(DockerActions::class);
68+
$this->kubernetesActions = $this->createMock(KubernetesActions::class);
6569
$this->manualActions = $this->createMock(ManualActions::class);
6670
$this->commonService = $this->createMock(AppAPICommonService::class);
6771
$daemonConfigService = $this->createMock(DaemonConfigService::class);
72+
$exAppDeployOptionsService = $this->createMock(ExAppDeployOptionsService::class);
6873
$harpService = $this->createMock(HarpService::class);
6974

7075
$this->service = new AppAPIService(
@@ -79,9 +84,11 @@ protected function setUp(): void {
7984
$l10nFactory,
8085
$this->exAppService,
8186
$this->dockerActions,
87+
$this->kubernetesActions,
8288
$this->manualActions,
8389
$this->commonService,
8490
$daemonConfigService,
91+
$exAppDeployOptionsService,
8592
$harpService,
8693
);
8794
}
@@ -136,6 +143,7 @@ private function createMockRequest(string $appId, string $version, string $secre
136143
private function setupExAppUrlMocks(): void {
137144
// Make getExAppUrl take the manual actions path and return a test URL
138145
$this->dockerActions->method('getAcceptsDeployId')->willReturn('docker-install');
146+
$this->kubernetesActions->method('getAcceptsDeployId')->willReturn('kubernetes-install');
139147
$this->manualActions->method('getAcceptsDeployId')->willReturn('manual-install');
140148
$this->manualActions->method('resolveExAppUrl')->willReturn('http://localhost:23000');
141149
}

0 commit comments

Comments
 (0)