Skip to content

Commit 590ff8e

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 590ff8e

3 files changed

Lines changed: 834 additions & 0 deletions

File tree

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