Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 166 additions & 41 deletions .github/workflows/k8s.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ jobs:
- suite: apiAuthApp
# - suite: apiAntivirus
- suite: apiOcm
# - suite: apiCollaboration
- suite: apiCollaboration
- suite: apiVault
# - suite: "coreApiAuth,coreApiCapabilities,coreApiFavorites,coreApiMain,coreApiVersions"
- suite: "coreApiShareManagementBasicToShares,coreApiShareManagementToShares"
- suite: "coreApiSharees"
Expand Down Expand Up @@ -103,30 +104,32 @@ jobs:

- name: Prepare hosts
run: |
echo "127.0.0.1 ocis-server clamav email collabora onlyoffice fakeoffice tika federation-ocis-server" \
echo "127.0.0.1 ocis-server clamav email collabora onlyoffice fakeoffice tika federation-ocis-server keycloak" \
| sudo tee -a /etc/hosts

- name: Spin up K3d Cluster
run: make -C tests/config/k8s create-cluster

- name: Prepare Helm Charts & Deploy oCIS
- name: Prepare Helm Charts
env:
ENABLE_ANTIVIRUS: ${{ matrix.suite == 'apiAntivirus' }}
ENABLE_EMAIL: ${{ matrix.suite == 'apiNotification' || matrix.suite == 'apiSettings' || matrix.suite == 'apiOcm' }}
ENABLE_TIKA: ${{ matrix.suite == 'apiSearchContent' }}
ENABLE_TIKA: ${{ matrix.suite == 'apiSearchContent' || matrix.suite == 'apiVault' }}
ENABLE_WOPI: ${{ matrix.suite == 'apiCollaboration' }}
ENABLE_OCM: ${{ matrix.suite == 'apiOcm' }}
ENABLE_AUTH_APP: ${{ matrix.suite == 'apiAuthApp' }}
ENABLE_VAULT: ${{ matrix.suite == 'apiVault' }}
run: |
cd tests/config/k8s
make prepare-charts
kubectl get pods -n ocis-server -Aw &
make deploy-ocis
if [[ "${{ matrix.suite }}" == "apiOcm" ]]; then
OCM=true make prepare-charts
OCM=true make deploy-ocis
fi

# Keycloak/postgres (and the other suite-specific backends) must be up and
# exposed to the cluster before oCIS is deployed: the proxy validates the
# OCIS_OIDC_ISSUER well-known endpoint against Keycloak at startup in vault mode,
# so `helm install --wait` would time out waiting for the proxy pod otherwise.
- name: Deploy Suite-Specific External Backends
run: |
if [[ "${{ matrix.suite }}" == "apiNotification" || \
Expand All @@ -147,13 +150,80 @@ jobs:
bash tests/config/k8s/expose-external-svc.sh clamav:3310
fi

if [[ "${{ matrix.suite }}" == "apiSearchContent" ]]; then
if [[ "${{ matrix.suite }}" == "apiSearchContent" || "${{ matrix.suite }}" == "apiVault" ]]; then
docker run -d \
-p 9998:9998 \
--name tika \
apache/tika:3.2.2.0-full
bash tests/config/k8s/expose-external-svc.sh tika:9998
fi

if [[ "${{ matrix.suite }}" == "apiVault" ]]; then
# GitHub runners ship PostgreSQL pre-started on 5432; stop it so our
# container (needed by keycloak) can bind the same port.
sudo systemctl stop postgresql || true

mkdir -p keycloak-certs
openssl req -x509 -newkey rsa:2048 \
-keyout keycloak-certs/keycloakkey.pem \
-out keycloak-certs/keycloakcrt.pem \
-nodes -days 365 -subj "/CN=keycloak"
chmod 777 keycloak-certs/*

# patch the realm so the "web" client's redirect/origin URLs match
# the k8s ingress domain instead of the non-k8s "localhost:9200" one
sed 's|https://localhost:9200|https://ocis-server|g' \
tests/config/ci/ocis-mfa-ci-realm.dist.json > /tmp/ocis-realm.json

docker run -d --name postgres --network host \
-e POSTGRES_DB=keycloak \
-e POSTGRES_USER=keycloak \
-e POSTGRES_PASSWORD=keycloak \
postgres:alpine3.18

for i in {1..30}; do
docker exec postgres pg_isready -U keycloak && break
echo "Waiting for postgres... ($i/30)"
sleep 2
done

docker run -d --name keycloak --network host \
-e OCIS_DOMAIN=https://ocis-server \
-e KC_HOSTNAME=keycloak \
-e KC_PORT=8443 \
-e KC_DB=postgres \
-e KC_DB_URL=jdbc:postgresql://localhost:5432/keycloak \
-e KC_DB_USERNAME=keycloak \
-e KC_DB_PASSWORD=keycloak \
-e KC_FEATURES=impersonation \
-e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
-e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
-e KC_HTTPS_CERTIFICATE_FILE=/keycloak-certs/keycloakcrt.pem \
-e KC_HTTPS_CERTIFICATE_KEY_FILE=/keycloak-certs/keycloakkey.pem \
-v "$(pwd)/keycloak-certs:/keycloak-certs:ro" \
-v /tmp/ocis-realm.json:/opt/keycloak/data/import/ocis-mfa-ci-realm.dist.json:ro \
quay.io/keycloak/keycloak:26.5.6 \
start-dev --proxy-headers xforwarded \
--spi-connections-http-client-default-disable-trust-manager=true \
--import-realm --health-enabled=true

for i in {1..60}; do
curl -skf https://localhost:9000/health/ready && break
echo "Waiting for keycloak... ($i/60)"
sleep 5
done

bash tests/config/k8s/expose-external-svc.sh keycloak:8443
fi

- name: Deploy oCIS
run: |
cd tests/config/k8s
kubectl get pods -n ocis-server -Aw &
make deploy-ocis
if [[ "${{ matrix.suite }}" == "apiOcm" ]]; then
OCM=true make deploy-ocis
fi

- name: Wait for oCIS to be ready
env:
Expand All @@ -175,49 +245,90 @@ jobs:

kubectl get pods -n ocis-server

echo "Creating test file in oCIS..."

request() {
local method=$1
shift
curl -ks -o /dev/null -w "%{http_code}" \
-X "$method" \
-u admin:admin \
"$@" \
"$FILE_URL"
}

retry_http() {
local expected=$1
retry() {
local label=$1
shift
for i in {1..30}; do
status=$("$@")
if [ "$status" = "$expected" ]; then
echo "Succeeded (HTTP $status)"
if "$@"; then
echo "$label succeeded"
return 0
fi
echo "Attempt $i failed (HTTP $status), retrying in 10s..."
echo "$label attempt $i failed, retrying in 10s..."
sleep 10
done
echo "Failed after 30 attempts"
echo "$label failed after 30 attempts"
return 1
}
put_file() {
request PUT \
-H "Content-Type: text/plain" \
--data "Hello from GitHub Actions!"
}
delete_file() {
request DELETE
}
echo "Creating test file..."
retry_http 201 put_file || exit 1
echo "Deleting test file..."
retry_http 204 delete_file || exit 1

if [[ "${{ matrix.suite }}" == "apiVault" ]]; then
# In vault mode IDM_CREATE_DEMO_USERS=false, so there is no "admin"
# LDAP user to authenticate the basic-auth check below against.
# Poll the proxy's unauthenticated debug readyz endpoint instead,
# reached via port-forward since it isn't exposed by any Service.
# local port 19205 (not 9205): 9100-9399 is already bound on the
# host by the k3d loadbalancer's NodePort range, see create-cluster.
kubectl -n ocis-server port-forward deployment/proxy 19205:9205 &
PORT_FORWARD_PID=$!
trap 'kill $PORT_FORWARD_PID 2>/dev/null' EXIT

proxy_ready() {
curl -sf http://localhost:19205/readyz > /dev/null
}
retry "proxy readyz" proxy_ready || exit 1
else
echo "Creating test file in oCIS..."

request() {
local method=$1
shift
curl -ks -o /dev/null -w "%{http_code}" \
-X "$method" \
-u admin:admin \
"$@" \
"$FILE_URL"
}
put_file() {
request PUT \
-H "Content-Type: text/plain" \
--data "Hello from GitHub Actions!"
}
delete_file() {
request DELETE
}
check_status() {
local expected=$1
shift
status=$("$@")
[ "$status" = "$expected" ]
}
echo "Creating test file..."
retry "create file (HTTP 201)" check_status 201 put_file || exit 1
echo "Deleting test file..."
retry "delete file (HTTP 204)" check_status 204 delete_file || exit 1
fi

- name: Expose debug ports
run: bash tests/config/k8s/expose-debug-svc.sh


# env-config scenarios (OcisConfigContext::waitForOcisProxyReady) poll the proxy's
# unauthenticated /readyz endpoint after every config-triggered restart. Local port 9205
# is unavailable on the runner (k3d's loadbalancer reserves 9100-9399, see create-cluster),
# so forward it to 19205 instead, for the whole remainder of the job - unlike the earlier
# one-off port-forward in "Wait for oCIS to be ready", this one is intentionally left
# running (not killed via trap) since env-config scenarios can restart oCIS at any point
# during the test run. Wrapped in a reconnect loop: a config change that restarts the
# proxy pod itself would kill a plain `kubectl port-forward` for good, since it targets a
# specific pod and doesn't follow a deployment through a rollout.
- name: Expose proxy readyz
if: matrix.suite == 'apiVault'
run: |
(
while true; do
kubectl -n ocis-server port-forward deployment/proxy 19205:9205 >> /tmp/proxy-readyz-portforward.log 2>&1
sleep 1
done
) &

- name: Build ociswrapper
run: make -C tests/ociswrapper/

Expand All @@ -230,6 +341,17 @@ jobs:
--skip-ocis-run \
-n ocis-server &

- name: Install Playwright for vault tests
if: matrix.suite == 'apiVault'
run: |
composer install --no-progress
composer bin behat install --no-progress
vendor-php/bin/playwright-install
vendor-php/bin/playwright-install --browsers
env:
COMPOSER_NO_INTERACTION: "1"
COMPOSER_NO_AUDIT: "1"

- name: Prepare expected failures
if: startsWith( matrix.suite, 'core' )
env:
Expand All @@ -253,8 +375,11 @@ jobs:
OCIS_WRAPPER_URL: ${{ env.OCIS_WRAPPER_URL }}
COLLABORATION_SERVICE_URL: http://ocis-server:9304
K8S: ${{ env.K8S }}
KEYCLOAK: ${{ matrix.suite == 'apiVault' }}
KC_URL: https://keycloak:8443
PROXY_READYZ_URL: ${{ matrix.suite == 'apiVault' && 'http://localhost:19205/readyz' || '' }}
run: make test-acceptance-api

- name: Run Core ${{ matrix.suite }} tests
if: startsWith( matrix.suite, 'core' )
env:
Expand Down
5 changes: 4 additions & 1 deletion tests/acceptance/bootstrap/OcisConfigContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,10 @@ private function assertOcisRestarted(ResponseInterface $response, string $errorM
* @throws GuzzleException
*/
private function waitForOcisProxyReady(int $timeoutSeconds = 60): void {
$readyzUrl = 'http://localhost:9205/readyz';
// In k8s, port 9205 on the runner's localhost is unavailable (k3d's loadbalancer
// reserves that range), so the proxy's debug port is forwarded to a different local
// port for the lifetime of the job. See k8s.yml's "Expose proxy readyz" step.
$readyzUrl = getenv('PROXY_READYZ_URL') ?: 'http://localhost:9205/readyz';
$deadline = time() + $timeoutSeconds;
while (time() < $deadline) {
try {
Expand Down
25 changes: 23 additions & 2 deletions tests/acceptance/bootstrap/Provisioning.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ public function getOcisUserToken(string $userId): array {
return $this->userTokens[$userId];
}

/**
* Finds the oidc-client-ts user entry (key "oc_oAuth.user:<authority>:<client_id>")
* in a Playwright browser storage state and decodes its token data.
* The exact position of this entry among the other localStorage keys is not
* guaranteed, so it must be located by name rather than by a fixed index.
*
* @param array $state
*
* @return mixed
* @throws Exception
*/
public function extractOidcTokenDataFromStorageState(array $state): mixed {
$localStorage = $state['origins'][0]['localStorage'] ?? [];
foreach ($localStorage as $entry) {
if (\str_starts_with($entry['name'] ?? '', 'oc_oAuth.user:')) {
return \json_decode($entry['value']);
}
}
throw new Exception('Could not find an "oc_oAuth.user:" entry in the browser storage state.');
}

/**
* Check if this is the admin group. That group is always a local group in
* ownCloud10, even if other groups come from LDAP.
Expand Down Expand Up @@ -711,7 +732,7 @@ public function setAccessTokenForAdmin(): void {
$adminUser["actualUsername"],
$adminUser["password"],
);
$tokenData = \json_decode($state['origins'][0]['localStorage'][2]['value']);
$tokenData = $this->extractOidcTokenDataFromStorageState($state);
$this->setOcisUserToken($adminUser, $tokenData);
}

Expand All @@ -735,7 +756,7 @@ public function userHasLoggedInViaWebUI(string $user): void {
$userAttribute["actualUsername"],
$userAttribute["password"],
);
$stateData = \json_decode($state['origins'][0]['localStorage'][2]['value']);
$stateData = $this->extractOidcTokenDataFromStorageState($state);
$this->setOcisUserToken($userAttribute, $stateData);
$response = $this->graphContext->adminHasRetrievedUserUsingTheGraphApi($user);
$userAttribute['id'] = $this->getJsonDecodedResponse($response)['id'];
Expand Down
8 changes: 7 additions & 1 deletion tests/acceptance/features/apiVault/vault.feature
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ Feature: vault
@env-config @keycloak-config
Scenario: user can set custom auth level names
Given the administrator has set the Keycloak realm attribute "acr.loa.map" to '{"regular":"1","testing":"2"}'
And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing"
# OCIS_MFA_AUTH_LEVEL_NAMES is read by both the proxy (mfa.go, gates access) and the
# frontend (exposed via the capabilities endpoint, which tells the web app which acr_values
# to request during step-up). In k8s each is a separate deployment, so both must be
# reconfigured explicitly - unlike the single-binary setup, there is no "just set it
# globally" here.
And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" for "proxy" service
And the config "OCIS_MFA_AUTH_LEVEL_NAMES" has been set to "testing" for "frontend" service
And user "Alice" has logged in via web UI
When user "Alice" uploads a file inside space "Personal" with content "some content" to "vaultFile.txt" in vault using the WebDAV API
Then the HTTP status code should be "201"
Expand Down
Loading
Loading