Skip to content

Commit 1bcdf49

Browse files
committed
test(k8s): move update-preserves-options into a dedicated CI job
Signed-off-by: Oleksander Piskun <oleksandr2088@icloud.com>
1 parent f153fb7 commit 1bcdf49

2 files changed

Lines changed: 234 additions & 79 deletions

File tree

.github/workflows/tests-deploy-k8s.yml

Lines changed: 234 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,244 @@ jobs:
216216
path: data/nextcloud.log
217217
if-no-files-found: warn
218218

219+
k8s-update-preserves-deploy-options:
220+
runs-on: ubuntu-22.04
221+
name: Update preserves deploy options (K8s)
222+
# Regression test for https://github.com/nextcloud/app_api/issues/808
223+
# on the K8s deploy path. Mirrors the Docker job in tests-deploy.yml.
224+
225+
services:
226+
postgres:
227+
image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest # zizmor: ignore[unpinned-images]
228+
ports:
229+
- 4444:5432/tcp
230+
env:
231+
POSTGRES_USER: root
232+
POSTGRES_PASSWORD: rootpassword
233+
POSTGRES_DB: nextcloud
234+
options: --health-cmd pg_isready --health-interval 5s --health-timeout 2s --health-retries 5
235+
236+
steps:
237+
- name: Set app env
238+
run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV
239+
240+
- name: Checkout server
241+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
242+
with:
243+
persist-credentials: false
244+
submodules: true
245+
repository: nextcloud/server
246+
ref: master
247+
248+
- name: Checkout AppAPI
249+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
250+
with:
251+
persist-credentials: false
252+
path: apps/${{ env.APP_NAME }}
253+
254+
- name: Set up php
255+
uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2
256+
with:
257+
php-version: '8.3'
258+
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, pgsql, pdo_pgsql
259+
coverage: none
260+
ini-file: development
261+
env:
262+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
263+
264+
- name: Check composer file existence
265+
id: check_composer
266+
uses: andstor/file-existence-action@558493d6c74bf472d87c84eab196434afc2fa029 # v2
267+
with:
268+
files: apps/${{ env.APP_NAME }}/composer.json
269+
270+
- name: Set up dependencies
271+
if: steps.check_composer.outputs.files_exists == 'true'
272+
working-directory: apps/${{ env.APP_NAME }}
273+
run: composer i
274+
275+
- name: Set up Nextcloud
276+
env:
277+
DB_PORT: 4444
278+
run: |
279+
mkdir data
280+
./occ maintenance:install --verbose --database=pgsql --database-name=nextcloud --database-host=127.0.0.1 \
281+
--database-port=$DB_PORT --database-user=root --database-pass=rootpassword \
282+
--admin-user admin --admin-pass admin
283+
./occ config:system:set loglevel --value=0 --type=integer
284+
./occ config:system:set debug --value=true --type=boolean
285+
./occ app:enable --force ${{ env.APP_NAME }}
286+
287+
- name: Install k3s
288+
run: |
289+
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable traefik --disable servicelb" sh -
290+
sudo chmod 644 /etc/rancher/k3s/k3s.yaml
291+
echo "KUBECONFIG=/etc/rancher/k3s/k3s.yaml" >> $GITHUB_ENV
292+
293+
- name: Wait for k3s and create namespace
294+
run: |
295+
kubectl wait --for=condition=Ready node --all --timeout=120s
296+
kubectl create namespace nextcloud-exapps
297+
NODE_IP=$(kubectl get node -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
298+
echo "NODE_IP=${NODE_IP}" >> $GITHUB_ENV
299+
300+
- name: Configure Nextcloud for k3s networking
301+
run: |
302+
./occ config:system:set overwrite.cli.url --value "http://${{ env.NODE_IP }}" --type=string
303+
./occ config:system:set trusted_domains 1 --value "${{ env.NODE_IP }}"
304+
305+
- name: Create K8s service account for HaRP
306+
run: |
307+
kubectl -n nextcloud-exapps create serviceaccount harp-sa
308+
kubectl create clusterrolebinding harp-admin \
309+
--clusterrole=cluster-admin \
310+
--serviceaccount=nextcloud-exapps:harp-sa
311+
K3S_TOKEN=$(kubectl -n nextcloud-exapps create token harp-sa --duration=2h)
312+
echo "K3S_TOKEN=${K3S_TOKEN}" >> $GITHUB_ENV
313+
314+
- name: Pre-pull ExApp image into k3s
315+
run: sudo k3s ctr images pull ghcr.io/nextcloud/app-skeleton-python:latest
316+
317+
- name: Pull HaRP image
318+
run: docker pull ghcr.io/nextcloud/nextcloud-appapi-harp:latest
319+
320+
- name: Start HaRP with K8s backend
321+
run: |
322+
docker run --net host --name appapi-harp \
323+
-e HP_SHARED_KEY="${{ env.HP_SHARED_KEY }}" \
324+
-e NC_INSTANCE_URL="http://${{ env.NODE_IP }}" \
325+
-e HP_LOG_LEVEL="debug" \
326+
-e HP_K8S_ENABLED="true" \
327+
-e HP_K8S_API_SERVER="https://127.0.0.1:6443" \
328+
-e HP_K8S_BEARER_TOKEN="${{ env.K3S_TOKEN }}" \
329+
-e HP_K8S_NAMESPACE="nextcloud-exapps" \
330+
-e HP_K8S_VERIFY_SSL="false" \
331+
--restart unless-stopped \
332+
-d ghcr.io/nextcloud/nextcloud-appapi-harp:latest
333+
334+
- name: Start nginx proxy
335+
run: |
336+
docker run --net host --name nextcloud --rm \
337+
-v $(pwd)/apps/${{ env.APP_NAME }}/tests/simple-nginx-NOT-FOR-PRODUCTION.conf:/etc/nginx/conf.d/default.conf:ro \
338+
-d nginx
339+
340+
- name: Start Nextcloud
341+
run: PHP_CLI_SERVER_WORKERS=2 php -S 0.0.0.0:8080 &
342+
343+
- name: Wait for HaRP K8s readiness
344+
run: |
345+
for i in $(seq 1 30); do
346+
if curl -sf http://${{ env.NODE_IP }}:8780/exapps/app_api/info \
347+
-H "harp-shared-key: ${{ env.HP_SHARED_KEY }}" 2>/dev/null | grep -q '"kubernetes"'; then
348+
echo "HaRP is ready"
349+
exit 0
350+
fi
351+
sleep 2
352+
done
353+
docker logs appapi-harp
354+
exit 1
355+
356+
- name: Register K8s daemon and Skeleton v1 with user env vars
357+
run: |
358+
./occ app_api:daemon:register \
359+
k8s_test "K8s Test" "kubernetes-install" "http" "${{ env.NODE_IP }}:8780" "http://${{ env.NODE_IP }}" \
360+
--harp --harp_shared_key "${{ env.HP_SHARED_KEY }}" \
361+
--k8s --k8s_expose_type=nodeport --set-default
362+
./occ app_api:app:register app-skeleton-python k8s_test \
363+
--info-xml https://raw.githubusercontent.com/nextcloud/app-skeleton-python/main/appinfo/info.xml \
364+
--env='TEST_ENV_2=user_provided_value' --wait-finish
365+
366+
- name: Verify env vars on the freshly registered Deployment
367+
run: |
368+
kubectl -n nextcloud-exapps get deploy -l app.kubernetes.io/component=exapp -o json \
369+
| python3 -c '
370+
import json, sys
371+
items = json.load(sys.stdin)["items"]
372+
env = {}
373+
for item in items:
374+
for c in item["spec"]["template"]["spec"].get("containers", []):
375+
for e in c.get("env", []):
376+
env[e["name"]] = e.get("value", "")
377+
assert env.get("TEST_ENV_1") == "0", f"TEST_ENV_1 default missing after register: {env}"
378+
assert env.get("TEST_ENV_2") == "user_provided_value", f"TEST_ENV_2 user value missing after register: {env}"
379+
'
380+
381+
- name: Seed stray ex_deploy_options row for a second app
382+
# Without a second appid in the table, the Update.php bug from #808 is
383+
# latent: formatDeployOptions() with no $appId filter still produces
384+
# the single app's rows by luck. The `zz_` prefix ensures this stray row
385+
# iterates AFTER `app-skeleton-python`, so the last-wins flattening
386+
# actually clobbers the skeleton's env_vars entry.
387+
run: |
388+
php apps/${{ env.APP_NAME }}/tests/integration_helper.php \
389+
set-env zz_fake_second_app UNRELATED_VAR x
390+
391+
- name: Build v2 info.xml with bumped version
392+
run: |
393+
curl -sS https://raw.githubusercontent.com/nextcloud/app-skeleton-python/main/appinfo/info.xml \
394+
| sed 's#<version>[^<]*</version>#<version>999.0.0</version>#' > /tmp/info-v2.xml
395+
grep -q '<version>999.0.0</version>' /tmp/info-v2.xml || { echo "version bump failed"; exit 1; }
396+
397+
- name: Update ExApp
398+
run: |
399+
./occ app_api:app:update app-skeleton-python --info-xml /tmp/info-v2.xml --wait-finish
400+
401+
- name: After update, env vars still present on the new Deployment
402+
run: |
403+
kubectl -n nextcloud-exapps get deploy -l app.kubernetes.io/component=exapp -o json \
404+
| python3 -c '
405+
import json, sys
406+
items = json.load(sys.stdin)["items"]
407+
env = {}
408+
for item in items:
409+
for c in item["spec"]["template"]["spec"].get("containers", []):
410+
for e in c.get("env", []):
411+
env[e["name"]] = e.get("value", "")
412+
assert env.get("TEST_ENV_1") == "0", f"#808 regression: TEST_ENV_1 lost on update; env={env}"
413+
assert env.get("TEST_ENV_2") == "user_provided_value", f"#808 regression: TEST_ENV_2 user value lost on update; env={env}"
414+
'
415+
416+
- name: Collect HaRP logs
417+
if: always()
418+
run: docker logs appapi-harp > harp.log 2>&1
419+
420+
- name: Collect K8s resources
421+
if: always()
422+
run: |
423+
kubectl -n nextcloud-exapps get all -o wide > k8s-resources.txt 2>&1 || true
424+
kubectl -n nextcloud-exapps describe pods > k8s-pods-describe.txt 2>&1 || true
425+
426+
- name: Upload HaRP logs
427+
if: always()
428+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
429+
with:
430+
name: k8s_update_preserves_deploy_options_harp.log
431+
path: harp.log
432+
if-no-files-found: warn
433+
434+
- name: Upload K8s resources
435+
if: always()
436+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
437+
with:
438+
name: k8s_update_preserves_deploy_options_resources.txt
439+
path: |
440+
k8s-resources.txt
441+
k8s-pods-describe.txt
442+
if-no-files-found: warn
443+
444+
- name: Upload NC logs
445+
if: always()
446+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
447+
with:
448+
name: k8s_update_preserves_deploy_options_nextcloud.log
449+
path: data/nextcloud.log
450+
if-no-files-found: warn
451+
219452
tests-success:
220453
permissions:
221454
contents: none
222455
runs-on: ubuntu-22.04
223-
needs: [k8s-deploy-nodeport]
456+
needs: [k8s-deploy-nodeport, k8s-update-preserves-deploy-options]
224457
name: K8s-NodePort-Tests-OK
225458
steps:
226459
- run: echo "K8s NodePort tests passed successfully"

tests/test_occ_commands_k8s.py

Lines changed: 0 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
"""
1111
import json
1212
import os
13-
import re
1413
import time
15-
import urllib.request
1614
from subprocess import DEVNULL, PIPE, TimeoutExpired, run
1715

1816
SKELETON_XML_URL = (
@@ -481,81 +479,6 @@ def test_k8s_single_update_same_version():
481479
print("OK")
482480

483481

484-
def test_k8s_update_preserves_deploy_options():
485-
"""Regression test for https://github.com/nextcloud/app_api/issues/808.
486-
487-
When two or more ExApps have rows in `ex_deploy_options`, the update flow
488-
used to drop the target app's env vars because `Update.php` read the
489-
unfiltered options table. `formatDeployOptions()` flattens by `type`, so
490-
the last app in iteration order clobbers earlier apps' entries.
491-
492-
Steps:
493-
1. Seed `ex_deploy_options` with a user-provided env var for
494-
`app-skeleton-python` (simulates the saved state from a previous
495-
register --env) AND a stray row for a second appid. The `zz_`
496-
prefix forces it to iterate AFTER the skeleton so the bug actually
497-
triggers.
498-
2. Build a local v2 info.xml with a bumped version. Must use
499-
--info-xml (not --json-info) because getAppInfo() skips the deploy
500-
options merge entirely in the JSON path.
501-
3. Run `app_api:app:update`.
502-
4. Assert the env vars declared in info.xml are present on the new
503-
Deployment pod spec: TEST_ENV_1 with its default and TEST_ENV_2
504-
with the user-provided value.
505-
"""
506-
print(" test_k8s_update_preserves_deploy_options...", end=" ", flush=True)
507-
508-
helper = os.path.join(os.path.dirname(os.path.abspath(__file__)), "integration_helper.php")
509-
510-
def seed(appid, name, value):
511-
r = run(["php", helper, "set-env", appid, name, value], stdout=PIPE, stderr=PIPE)
512-
assert r.returncode == 0, f"seed {appid}/{name} failed: {r.stderr.decode()}"
513-
514-
seed("app-skeleton-python", "TEST_ENV_2", "user_provided_value")
515-
seed("zz_fake_second_app", "UNRELATED_VAR", "x")
516-
517-
xml = urllib.request.urlopen(SKELETON_XML_URL, timeout=30).read().decode()
518-
xml_v2 = re.sub(r"<version>[^<]+</version>", "<version>100.0.0</version>", xml, count=1)
519-
xml_path = "/tmp/info-808.xml"
520-
with open(xml_path, "w", encoding="utf-8") as f:
521-
f.write(xml_v2)
522-
assert "<version>100.0.0</version>" in xml_v2, "Failed to bump version in fixture"
523-
524-
r = run(
525-
[
526-
"php", "occ", "--no-warnings", "app_api:app:update",
527-
"app-skeleton-python",
528-
"--info-xml", xml_path,
529-
"--wait-finish",
530-
],
531-
stdout=PIPE, stderr=PIPE, timeout=600,
532-
)
533-
assert r.returncode == 0, f"Update failed (exit {r.returncode}): {r.stdout.decode()}"
534-
535-
deploy_json = kubectl_output(
536-
"get deploy -l app.kubernetes.io/component=exapp -o json",
537-
)
538-
data = json.loads(deploy_json)
539-
env_map = {}
540-
for item in data.get("items", []):
541-
if "app-skeleton-python" not in item["metadata"]["name"]:
542-
continue
543-
for container in item["spec"]["template"]["spec"].get("containers", []):
544-
for entry in container.get("env", []):
545-
env_map[entry["name"]] = entry.get("value", "")
546-
547-
assert env_map.get("TEST_ENV_1") == "0", (
548-
f"#808 regression: TEST_ENV_1 default lost after update; env_map={env_map}"
549-
)
550-
assert env_map.get("TEST_ENV_2") == "user_provided_value", (
551-
f"#808 regression: user-provided TEST_ENV_2 lost after update; env_map={env_map}"
552-
)
553-
554-
run(["php", helper, "remove", "zz_fake_second_app"], stdout=PIPE, stderr=PIPE)
555-
556-
print("OK")
557-
558-
559482
def test_k8s_single_unregister_keep_data():
560483
"""Unregister K8s ExApp — default keeps PVC."""
561484
print(" test_k8s_single_unregister_keep_data...", end=" ", flush=True)
@@ -630,7 +553,6 @@ def run_single_role_tests():
630553
test_k8s_single_enable_disable()
631554
test_k8s_single_update()
632555
test_k8s_single_update_same_version()
633-
test_k8s_update_preserves_deploy_options()
634556
test_k8s_single_unregister_keep_data()
635557
test_k8s_single_deploy_rm_data()
636558
print("=== Group B: All single-role tests passed ===\n")

0 commit comments

Comments
 (0)