mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-25 06:42:52 +08:00
chore: merge main into stage 1 configuration updates
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
@@ -8,3 +8,15 @@ final-status-level = "fail"
|
||||
failure-output = "immediate-final"
|
||||
# Print slow test names after 30 seconds and terminate them after two minutes.
|
||||
slow-timeout = { period = "30s", terminate-after = 4 }
|
||||
|
||||
[profile.e2e-kubernetes]
|
||||
fail-fast = false
|
||||
status-level = "slow"
|
||||
final-status-level = "fail"
|
||||
failure-output = "immediate-final"
|
||||
# Flag slow tests after 60s and terminate them after five minutes.
|
||||
slow-timeout = { period = "60s", terminate-after = 5 }
|
||||
|
||||
# Relative to the profile store dir (`e2e/rust/target/nextest/e2e-kubernetes/`).
|
||||
[profile.e2e-kubernetes.junit]
|
||||
path = "../../../../../results/e2e-kubernetes.xml"
|
||||
|
||||
@@ -23,6 +23,7 @@ jobs:
|
||||
pull-requests: read
|
||||
outputs:
|
||||
should_run: ${{ steps.gate.outputs.should_run }}
|
||||
run_integration: ${{ steps.labels.outputs.run_core_e2e }}
|
||||
run_core_e2e: ${{ steps.labels.outputs.run_core_e2e }}
|
||||
run_gpu_e2e: ${{ steps.labels.outputs.run_gpu_e2e }}
|
||||
run_kubernetes_ha_e2e: ${{ steps.labels.outputs.run_kubernetes_ha_e2e }}
|
||||
@@ -199,6 +200,69 @@ jobs:
|
||||
packages: write
|
||||
uses: ./.github/workflows/build-images.yml
|
||||
|
||||
prepare-integration:
|
||||
needs: [pr_metadata, build-binaries, build-images]
|
||||
if: needs.pr_metadata.outputs.run_integration == 'true'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/prepare-integration-inputs.yml
|
||||
|
||||
# Run driver-independent conformance tests.
|
||||
conformance-integration:
|
||||
needs: prepare-integration
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
category: conformance
|
||||
source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
test-matrix: >-
|
||||
[
|
||||
{"environment":"ubuntu-docker-rootful","installer":"binaries","testsuite":"conformance"},
|
||||
{"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"conformance"},
|
||||
{"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"conformance"}
|
||||
]
|
||||
|
||||
# Run feature-specific integration tests:
|
||||
feature-specific-integration:
|
||||
needs: prepare-integration
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
category: feature-specific
|
||||
source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
test-matrix: >-
|
||||
[
|
||||
{"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"provider-refresh"},
|
||||
{"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"provider-refresh"}
|
||||
]
|
||||
|
||||
# Run driver-specific integration tests:
|
||||
# TODO: This should be added as soon as we have driver-specific tests enabled in tmachine.
|
||||
# driver-specific:
|
||||
# needs: prepare-integration
|
||||
# permissions:
|
||||
# actions: read
|
||||
# contents: read
|
||||
# packages: read
|
||||
# uses: ./.github/workflows/integration-runner.yml
|
||||
# with:
|
||||
# category: driver-specific
|
||||
# source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
# integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
# test-matrix: >-
|
||||
# [
|
||||
# ]
|
||||
|
||||
docker-e2e:
|
||||
needs: [pr_metadata, build-binaries, build-images]
|
||||
if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true'
|
||||
@@ -212,18 +276,6 @@ jobs:
|
||||
runner: linux-arm64-cpu8
|
||||
conformance-artifact-prefix: openshell-conformance
|
||||
|
||||
podman-e2e:
|
||||
needs: [pr_metadata, build-binaries, build-images]
|
||||
if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true'
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/e2e-podman-test.yml
|
||||
with:
|
||||
image-tag: ${{ github.sha }}
|
||||
conformance-artifact-prefix: openshell-conformance
|
||||
|
||||
vm-e2e:
|
||||
needs: [pr_metadata, build-binaries, build-vm-driver]
|
||||
if: needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true'
|
||||
@@ -392,7 +444,19 @@ jobs:
|
||||
|
||||
core-e2e-result:
|
||||
name: Core E2E result
|
||||
needs: [pr_metadata, docker-e2e, podman-e2e, vm-e2e, docker-external-driver-e2e, podman-external-driver-e2e, vm-external-driver-e2e, kubernetes-e2e, kubernetes-external-driver-e2e, kubernetes-workspace-managed-e2e, kubernetes-workspace-operator-e2e]
|
||||
needs:
|
||||
- pr_metadata
|
||||
- conformance-integration
|
||||
- feature-specific-integration
|
||||
- docker-e2e
|
||||
- vm-e2e
|
||||
- docker-external-driver-e2e
|
||||
- podman-external-driver-e2e
|
||||
- vm-external-driver-e2e
|
||||
- kubernetes-e2e
|
||||
- kubernetes-external-driver-e2e
|
||||
- kubernetes-workspace-managed-e2e
|
||||
- kubernetes-workspace-operator-e2e
|
||||
if: always() && needs.pr_metadata.outputs.should_run == 'true' && needs.pr_metadata.outputs.run_core_e2e == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Conformance
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
scenarios:
|
||||
description: JSON array of tmachine scenarios to run
|
||||
required: true
|
||||
type: string
|
||||
testsuites:
|
||||
description: JSON array of tmachine test suites to run
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-conformance-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare conformance inputs
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
- uses: ./.github/actions/setup-nix
|
||||
|
||||
- name: Download CLI artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openshell-x86_64-unknown-linux-musl
|
||||
path: artifacts/binaries/x86_64-unknown-linux-musl
|
||||
|
||||
- name: Download gateway artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openshell-gateway-x86_64-unknown-linux-gnu
|
||||
path: artifacts/binaries/x86_64-unknown-linux-gnu
|
||||
|
||||
- name: Download sandbox image artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: image-sandbox
|
||||
path: image-input/sandbox
|
||||
|
||||
- name: Download supervisor image artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: image-supervisor
|
||||
path: image-input/supervisor
|
||||
|
||||
- name: Convert runtime images
|
||||
shell: nix develop .#testing -c bash -euo pipefail {0}
|
||||
run: |
|
||||
mkdir -p artifacts/images
|
||||
|
||||
skopeo copy \
|
||||
--override-os linux \
|
||||
--override-arch amd64 \
|
||||
oci-archive:image-input/sandbox/images/sandbox.tar \
|
||||
docker-archive:artifacts/images/openshell-sandbox-tmachine.tar:openshell/sandbox:tmachine
|
||||
|
||||
skopeo copy \
|
||||
--override-os linux \
|
||||
--override-arch amd64 \
|
||||
oci-archive:image-input/supervisor/images/supervisor.tar \
|
||||
docker-archive:artifacts/images/openshell-supervisor-tmachine.tar:openshell/supervisor:tmachine
|
||||
|
||||
- name: Build conformance test archive
|
||||
run: nix run .#build-artifacts-test-archives
|
||||
|
||||
- name: Upload conformance inputs
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: openshell-conformance-inputs
|
||||
path: artifacts
|
||||
compression-level: 0
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
|
||||
conformance:
|
||||
name: Conformance (${{ matrix.scenario }}, ${{ matrix.testsuite }})
|
||||
needs: prepare
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
scenario: ${{ fromJSON(inputs.scenarios) }}
|
||||
testsuite: ${{ fromJSON(inputs.testsuites) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
- name: Enable KVM access
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! -c /dev/kvm ]]; then
|
||||
echo "::error::The runner did not expose /dev/kvm"
|
||||
exit 1
|
||||
fi
|
||||
sudo chmod 0666 /dev/kvm
|
||||
exec 3<>/dev/kvm
|
||||
exec 3>&-
|
||||
|
||||
- uses: ./.github/actions/setup-nix
|
||||
|
||||
- name: Cache tmachine disks
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
with:
|
||||
path: ~/.cache/tmachine
|
||||
key: tmachine-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Download conformance inputs
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openshell-conformance-inputs
|
||||
path: artifacts
|
||||
|
||||
- name: Run conformance
|
||||
env:
|
||||
SCENARIO: ${{ matrix.scenario }}
|
||||
TESTSUITE: ${{ matrix.testsuite }}
|
||||
run: nix run .#tmachine -- test "${SCENARIO}" "${TESTSUITE}"
|
||||
@@ -26,10 +26,8 @@ on:
|
||||
type: string
|
||||
default: ""
|
||||
suite-matrix:
|
||||
required: false
|
||||
required: true
|
||||
type: string
|
||||
default: >-
|
||||
[{"suite":"provider-refresh-keycloak","runner":"ubuntu-26.04","podman_major":"5","podman_package_version":"5.7.0+ds2-3build1","conmon_package_version":"2.1.13+ds1-2","cmd":"mise run --no-deps --skip-deps e2e:provider-refresh-keycloak"}]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Integration Test Runner
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
source-sha:
|
||||
description: Source revision of the prepared candidate artifacts
|
||||
required: true
|
||||
type: string
|
||||
integration-inputs-artifact-id:
|
||||
description: Immutable ID of the prepared integration inputs artifact
|
||||
required: true
|
||||
type: string
|
||||
category:
|
||||
description: Integration-test category, used for the job name and concurrency key
|
||||
required: false
|
||||
type: string
|
||||
test-matrix:
|
||||
description: JSON array of environment, installer, and testsuite combinations
|
||||
required: false
|
||||
type: string
|
||||
default: >-
|
||||
[
|
||||
{"environment":"ubuntu-docker-rootful","installer":"binaries","testsuite":"conformance"},
|
||||
{"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"conformance"},
|
||||
{"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"conformance"}
|
||||
]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-integration-${{ inputs.category }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
name: Integration test (${{ inputs.category }}, ${{ matrix.environment }}, ${{ matrix.installer }}, ${{ matrix.testsuite }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include: ${{ fromJSON(inputs['test-matrix']) }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ inputs['source-sha'] }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Enable KVM access
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ ! -c /dev/kvm ]]; then
|
||||
echo "::error::The runner did not expose /dev/kvm"
|
||||
exit 1
|
||||
fi
|
||||
sudo chmod 0666 /dev/kvm
|
||||
exec 3<>/dev/kvm
|
||||
exec 3>&-
|
||||
|
||||
- uses: ./.github/actions/setup-nix
|
||||
|
||||
- name: Cache tmachine disks
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
with:
|
||||
path: ~/.cache/tmachine
|
||||
key: tmachine-${{ runner.os }}-${{ runner.arch }}
|
||||
|
||||
- name: Download integration inputs
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
artifact-ids: ${{ inputs['integration-inputs-artifact-id'] }}
|
||||
path: artifacts
|
||||
|
||||
- name: Run test suite
|
||||
env:
|
||||
ENVIRONMENT: ${{ matrix.environment }}
|
||||
INSTALLER: ${{ matrix.installer }}
|
||||
TESTSUITE: ${{ matrix.testsuite }}
|
||||
run: nix run .#tmachine -- test "${ENVIRONMENT}" "${INSTALLER}" "${TESTSUITE}"
|
||||
@@ -0,0 +1,48 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
artifact-run-id:
|
||||
description: Workflow run containing the candidate artifacts
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
category:
|
||||
description: Integration-test category, used for the job name and concurrency key
|
||||
required: true
|
||||
type: string
|
||||
default: conformance
|
||||
test-matrix:
|
||||
description: JSON array of environment, installer, and testsuite combinations
|
||||
required: true
|
||||
type: string
|
||||
default: >-
|
||||
[
|
||||
{"environment":"ubuntu-docker-rootful","installer":"binaries","testsuite":"conformance"},
|
||||
{"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"conformance"},
|
||||
{"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"conformance"}
|
||||
]
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
uses: ./.github/workflows/prepare-integration-inputs.yml
|
||||
with:
|
||||
artifact-run-id: ${{ inputs['artifact-run-id'] }}
|
||||
|
||||
integration:
|
||||
needs: prepare
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
category: ${{ inputs.category }}
|
||||
source-sha: ${{ needs.prepare.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare.outputs.integration_inputs_artifact_id }}
|
||||
test-matrix: ${{ inputs.test-matrix }}
|
||||
@@ -0,0 +1,96 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
name: Prepare Integration Inputs
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
artifact-run-id:
|
||||
description: Workflow run containing the candidate artifacts
|
||||
required: false
|
||||
type: string
|
||||
default: ""
|
||||
outputs:
|
||||
source_sha:
|
||||
description: Source revision of the candidate artifacts
|
||||
value: ${{ jobs.prepare.outputs.source_sha }}
|
||||
integration_inputs_artifact_id:
|
||||
description: Immutable ID of the prepared integration inputs artifact
|
||||
value: ${{ jobs.prepare.outputs.integration_inputs_artifact_id }}
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare tmachine inputs
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
source_sha: ${{ steps.artifact-run.outputs.source_sha }}
|
||||
integration_inputs_artifact_id: ${{ steps.upload-integration-inputs.outputs.artifact-id }}
|
||||
steps:
|
||||
- name: Resolve artifact run
|
||||
id: artifact-run
|
||||
env:
|
||||
ARTIFACT_RUN_ID: ${{ inputs['artifact-run-id'] || github.run_id }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
head_sha=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${ARTIFACT_RUN_ID}" --jq .head_sha)
|
||||
echo "source_sha=${head_sha}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ steps.artifact-run.outputs.source_sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-nix
|
||||
|
||||
- name: Download CLI artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openshell-x86_64-unknown-linux-musl
|
||||
path: artifacts/binaries/x86_64-unknown-linux-musl
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ inputs['artifact-run-id'] || github.run_id }}
|
||||
|
||||
- name: Download gateway artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: openshell-gateway-x86_64-unknown-linux-gnu
|
||||
path: artifacts/binaries/x86_64-unknown-linux-gnu
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ inputs['artifact-run-id'] || github.run_id }}
|
||||
|
||||
- name: Log in to GHCR
|
||||
run: echo "${{ github.token }}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
|
||||
|
||||
- name: Export runtime images
|
||||
env:
|
||||
IMAGE_TAG: ${{ steps.artifact-run.outputs.source_sha }}
|
||||
run: |
|
||||
mkdir -p artifacts/images
|
||||
|
||||
docker pull "ghcr.io/nvidia/openshell/sandbox:${IMAGE_TAG}"
|
||||
docker tag "ghcr.io/nvidia/openshell/sandbox:${IMAGE_TAG}" openshell/sandbox:tmachine
|
||||
docker save --output artifacts/images/openshell-sandbox-tmachine.tar openshell/sandbox:tmachine
|
||||
|
||||
docker pull "ghcr.io/nvidia/openshell/supervisor:${IMAGE_TAG}"
|
||||
docker tag "ghcr.io/nvidia/openshell/supervisor:${IMAGE_TAG}" openshell/supervisor:tmachine
|
||||
docker save --output artifacts/images/openshell-supervisor-tmachine.tar openshell/supervisor:tmachine
|
||||
|
||||
- name: Build test archives
|
||||
run: nix run .#build-artifacts-test-archives
|
||||
|
||||
- name: Upload integration inputs
|
||||
id: upload-integration-inputs
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: openshell-tmachine-inputs
|
||||
path: artifacts
|
||||
compression-level: 0
|
||||
retention-days: 5
|
||||
if-no-files-found: error
|
||||
@@ -101,15 +101,42 @@ jobs:
|
||||
with:
|
||||
checkout-ref: ${{ github.sha }}
|
||||
|
||||
conformance:
|
||||
prepare-integration:
|
||||
needs: [build-binaries, build-images]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
uses: ./.github/workflows/conformance.yml
|
||||
packages: read
|
||||
uses: ./.github/workflows/prepare-integration-inputs.yml
|
||||
|
||||
conformance-integration:
|
||||
needs: prepare-integration
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
scenarios: '["ubuntu-docker-rootful", "fedora-podman-rootful", "fedora-podman-rootless"]'
|
||||
testsuites: '["conformance"]'
|
||||
category: conformance
|
||||
source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
|
||||
feature-specific-integration:
|
||||
needs: prepare-integration
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
category: feature-specific
|
||||
source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
test-matrix: >-
|
||||
[
|
||||
{"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"provider-refresh"},
|
||||
{"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"provider-refresh"}
|
||||
]
|
||||
|
||||
docker-e2e:
|
||||
needs: [build-binaries, build-images]
|
||||
@@ -124,18 +151,6 @@ jobs:
|
||||
runner: linux-arm64-cpu8
|
||||
conformance-artifact-prefix: openshell-conformance
|
||||
|
||||
podman-e2e:
|
||||
needs: [build-binaries, build-images]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/e2e-podman-test.yml
|
||||
with:
|
||||
image-tag: ${{ github.sha }}
|
||||
checkout-ref: ${{ github.sha }}
|
||||
conformance-artifact-prefix: openshell-conformance
|
||||
|
||||
vm-e2e:
|
||||
needs: [build-binaries, build-vm-driver]
|
||||
permissions:
|
||||
@@ -244,7 +259,17 @@ jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
release-dev:
|
||||
name: Release Dev
|
||||
needs: [compute-versions, package-binaries, build-python-wheel, conformance, docker-e2e, podman-e2e, vm-e2e, build-deb, build-rpm, build-snap]
|
||||
needs:
|
||||
- compute-versions
|
||||
- package-binaries
|
||||
- build-python-wheel
|
||||
- conformance-integration
|
||||
- feature-specific-integration
|
||||
- docker-e2e
|
||||
- vm-e2e
|
||||
- build-deb
|
||||
- build-rpm
|
||||
- build-snap
|
||||
runs-on: linux-amd64-cpu8
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
|
||||
@@ -129,6 +129,43 @@ jobs:
|
||||
image-tag: ${{ needs.compute-versions.outputs.source_sha }}
|
||||
checkout-ref: ${{ needs.compute-versions.outputs.source_sha }}
|
||||
|
||||
prepare-integration:
|
||||
needs: [compute-versions, build-binaries, build-images]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/prepare-integration-inputs.yml
|
||||
|
||||
conformance-integration:
|
||||
needs: prepare-integration
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
category: conformance
|
||||
source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
|
||||
feature-specific-integration:
|
||||
needs: prepare-integration
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/integration-runner.yml
|
||||
with:
|
||||
category: feature-specific
|
||||
source-sha: ${{ needs.prepare-integration.outputs.source_sha }}
|
||||
integration-inputs-artifact-id: ${{ needs.prepare-integration.outputs.integration_inputs_artifact_id }}
|
||||
test-matrix: >-
|
||||
[
|
||||
{"environment":"fedora-podman-rootful","installer":"binaries","testsuite":"provider-refresh"},
|
||||
{"environment":"fedora-podman-rootless","installer":"binaries","testsuite":"provider-refresh"}
|
||||
]
|
||||
|
||||
docker-e2e:
|
||||
needs: [compute-versions, build-binaries, build-images]
|
||||
permissions:
|
||||
@@ -142,18 +179,6 @@ jobs:
|
||||
runner: linux-arm64-cpu8
|
||||
conformance-artifact-prefix: openshell-conformance
|
||||
|
||||
podman-e2e:
|
||||
needs: [compute-versions, build-binaries, build-images]
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
packages: read
|
||||
uses: ./.github/workflows/e2e-podman-test.yml
|
||||
with:
|
||||
image-tag: ${{ needs.compute-versions.outputs.source_sha }}
|
||||
checkout-ref: ${{ needs.compute-versions.outputs.source_sha }}
|
||||
conformance-artifact-prefix: openshell-conformance
|
||||
|
||||
vm-e2e:
|
||||
needs: [compute-versions, build-binaries, build-vm-driver]
|
||||
permissions:
|
||||
@@ -273,7 +298,17 @@ jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
release:
|
||||
name: Release
|
||||
needs: [compute-versions, package-binaries, build-python-wheel, docker-e2e, podman-e2e, vm-e2e, build-deb, build-rpm, build-snap]
|
||||
needs:
|
||||
- compute-versions
|
||||
- package-binaries
|
||||
- build-python-wheel
|
||||
- conformance-integration
|
||||
- feature-specific-integration
|
||||
- docker-e2e
|
||||
- vm-e2e
|
||||
- build-deb
|
||||
- build-rpm
|
||||
- build-snap
|
||||
runs-on: linux-amd64-cpu8
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
/target/
|
||||
e2e/rust/target/
|
||||
target/
|
||||
|
||||
# Test result artifacts (JUnit XML, etc.)
|
||||
/results/
|
||||
debug/
|
||||
release/
|
||||
|
||||
|
||||
+13
-3
@@ -258,13 +258,23 @@ for explicit publication.
|
||||
CLI conformance runs after target provisioning and operates only through the
|
||||
configured OpenShell CLI. The smoke scenario verifies the black-box sandbox
|
||||
lifecycle by creating, inspecting, executing in, and deleting a sandbox.
|
||||
Feature suites use the same disposable guest but may provision isolated
|
||||
dependencies after installation. The Keycloak provider-refresh suite starts a
|
||||
guest-local Keycloak realm and verifies a successful OAuth refresh followed by
|
||||
revocation and the gateway's reauthorization-required recovery state.
|
||||
|
||||
The `tests/tmachine` setup and installation caches include a digest of the
|
||||
Tmachine environments define the guest machine and runtime setup, while named
|
||||
installers define how OpenShell is installed. This keeps the runtime mode
|
||||
independent from binary or package installation and lets multiple installers
|
||||
reuse the same prepared setup disk. The test command is
|
||||
`tmachine test <environment> <installer> <testsuite>`.
|
||||
|
||||
The `tests/tmachine` setup and install caches include a digest of the
|
||||
entire directory containing `ANSIBLE_CONFIG`, including local roles, task
|
||||
includes, templates, inventory, and requirements. The digest uses sorted
|
||||
relative paths, file contents, and executable permissions; source symlinks
|
||||
are unsupported. Both keys also retain the ordered playbook paths and contents,
|
||||
their base disk contents, and whether Galaxy is enabled; installation keys
|
||||
their base disk contents, and whether Galaxy is enabled; install keys
|
||||
include named binary inputs. The top-level `.roles` directory is excluded:
|
||||
Galaxy release pins in `requirements.yaml` are treated as immutable, including
|
||||
any transitive dependency pins. Cache misses with Galaxy enabled reinstall
|
||||
@@ -275,7 +285,7 @@ with musl, and the gateway and supervisor with GNU. Image assembly stages
|
||||
the gateway, sandbox, and supervisor as separate binaries for their respective
|
||||
Dockerfiles. The helpers stage binaries under `artifacts/binaries` so local and
|
||||
CI builds expose the same inputs to tmachine and image assembly. The Ubuntu
|
||||
Docker and Fedora Podman scenarios import both local runtime images and
|
||||
Docker and Fedora Podman environments import both local runtime images and
|
||||
configure the gateway to use them.
|
||||
|
||||
## Python Wheel Packaging
|
||||
|
||||
@@ -15,7 +15,12 @@ pub type CliExecution<'a> =
|
||||
Pin<Box<dyn Future<Output = Result<Output, CliExecutionError>> + Send + 'a>>;
|
||||
|
||||
pub trait CliExecutor: Send + Sync {
|
||||
fn execute(&self, args: Vec<String>, command_timeout: Duration) -> CliExecution<'_>;
|
||||
fn execute(
|
||||
&self,
|
||||
args: Vec<String>,
|
||||
environment: Vec<(String, String)>,
|
||||
command_timeout: Duration,
|
||||
) -> CliExecution<'_>;
|
||||
}
|
||||
|
||||
pub enum CliExecutionError {
|
||||
@@ -34,11 +39,17 @@ impl ProcessCli {
|
||||
}
|
||||
|
||||
impl CliExecutor for ProcessCli {
|
||||
fn execute(&self, args: Vec<String>, command_timeout: Duration) -> CliExecution<'_> {
|
||||
fn execute(
|
||||
&self,
|
||||
args: Vec<String>,
|
||||
environment: Vec<(String, String)>,
|
||||
command_timeout: Duration,
|
||||
) -> CliExecution<'_> {
|
||||
Box::pin(async move {
|
||||
let mut process = tokio::process::Command::new(&self.binary);
|
||||
process
|
||||
.args(&args)
|
||||
.envs(environment)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
@@ -248,6 +248,8 @@ pub struct OpenShellRunner {
|
||||
run_id: String,
|
||||
scenario: String,
|
||||
known_sandboxes: BTreeSet<String>,
|
||||
known_providers: BTreeSet<String>,
|
||||
known_provider_profiles: BTreeSet<String>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
@@ -311,6 +313,8 @@ impl OpenShellRunner {
|
||||
run_id: generate_run_id(),
|
||||
scenario: scenario.to_string(),
|
||||
known_sandboxes: BTreeSet::new(),
|
||||
known_providers: BTreeSet::new(),
|
||||
known_provider_profiles: BTreeSet::new(),
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
@@ -393,6 +397,16 @@ impl OpenShellRunner {
|
||||
self.known_sandboxes.remove(name);
|
||||
}
|
||||
|
||||
/// Register a provider name for cleanup.
|
||||
pub fn track_provider(&mut self, name: &str) {
|
||||
self.known_providers.insert(name.to_string());
|
||||
}
|
||||
|
||||
/// Register a provider profile ID for cleanup.
|
||||
pub fn track_provider_profile(&mut self, id: &str) {
|
||||
self.known_provider_profiles.insert(id.to_string());
|
||||
}
|
||||
|
||||
pub async fn poll_until<T, F>(
|
||||
&mut self,
|
||||
step: &str,
|
||||
@@ -440,6 +454,7 @@ impl OpenShellRunner {
|
||||
step: &str,
|
||||
expectation: &str,
|
||||
args: Vec<String>,
|
||||
environment: Vec<(String, String)>,
|
||||
command_timeout: Duration,
|
||||
) -> Result<CommandResult, RunnerError> {
|
||||
let context = self.context(step);
|
||||
@@ -447,22 +462,22 @@ impl OpenShellRunner {
|
||||
eprintln!("{context} running: {command}");
|
||||
|
||||
let started = Instant::now();
|
||||
let output =
|
||||
self.cli
|
||||
.execute(args, command_timeout)
|
||||
.await
|
||||
.map_err(|error| match error {
|
||||
CliExecutionError::Timeout => RunnerError::Timeout {
|
||||
context: context.clone(),
|
||||
command: command.clone(),
|
||||
timeout: command_timeout,
|
||||
},
|
||||
CliExecutionError::Spawn(source) => RunnerError::Spawn {
|
||||
context: context.clone(),
|
||||
command: command.clone(),
|
||||
source,
|
||||
},
|
||||
})?;
|
||||
let output = self
|
||||
.cli
|
||||
.execute(args, environment, command_timeout)
|
||||
.await
|
||||
.map_err(|error| match error {
|
||||
CliExecutionError::Timeout => RunnerError::Timeout {
|
||||
context: context.clone(),
|
||||
command: command.clone(),
|
||||
timeout: command_timeout,
|
||||
},
|
||||
CliExecutionError::Spawn(source) => RunnerError::Spawn {
|
||||
context: context.clone(),
|
||||
command: command.clone(),
|
||||
source,
|
||||
},
|
||||
})?;
|
||||
let elapsed = started.elapsed();
|
||||
eprintln!(
|
||||
"{context} completed in {:.1?}: exit {}",
|
||||
@@ -484,7 +499,10 @@ impl OpenShellRunner {
|
||||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
if self.known_sandboxes.is_empty() {
|
||||
if self.known_sandboxes.is_empty()
|
||||
&& self.known_providers.is_empty()
|
||||
&& self.known_provider_profiles.is_empty()
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -516,6 +534,56 @@ impl OpenShellRunner {
|
||||
}
|
||||
}
|
||||
|
||||
for name in self.known_providers.clone() {
|
||||
let remaining = remaining_cleanup_time(cleanup_started);
|
||||
if remaining.is_zero() {
|
||||
failures.push(format!(
|
||||
"{} cleanup budget expired before deleting provider '{name}'",
|
||||
self.context("cleanup/delete-provider")
|
||||
));
|
||||
break;
|
||||
}
|
||||
match self
|
||||
.step("cleanup/delete-provider")
|
||||
.description(format!("provider '{name}' is deleted or already absent"))
|
||||
.with_timeout(remaining)
|
||||
.run(&["provider", "delete", &name])
|
||||
.await
|
||||
{
|
||||
Ok(result) if result.success() || output_reports_not_found(&result) => {}
|
||||
Ok(result) => failures.push(result.failure_diagnostic(&format!(
|
||||
"provider '{name}' is deleted or already absent"
|
||||
))),
|
||||
Err(error) => failures.push(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
for id in self.known_provider_profiles.clone() {
|
||||
let remaining = remaining_cleanup_time(cleanup_started);
|
||||
if remaining.is_zero() {
|
||||
failures.push(format!(
|
||||
"{} cleanup budget expired before deleting provider profile '{id}'",
|
||||
self.context("cleanup/delete-provider-profile")
|
||||
));
|
||||
break;
|
||||
}
|
||||
match self
|
||||
.step("cleanup/delete-provider-profile")
|
||||
.description(format!(
|
||||
"provider profile '{id}' is deleted or already absent"
|
||||
))
|
||||
.with_timeout(remaining)
|
||||
.run(&["provider", "profile", "delete", &id])
|
||||
.await
|
||||
{
|
||||
Ok(result) if result.success() || output_reports_not_found(&result) => {}
|
||||
Ok(result) => failures.push(result.failure_diagnostic(&format!(
|
||||
"provider profile '{id}' is deleted or already absent"
|
||||
))),
|
||||
Err(error) => failures.push(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -550,11 +618,24 @@ impl<'a> CommandStep<'a> {
|
||||
|
||||
impl OpenShellCommand<'_> {
|
||||
pub async fn run(&self, args: &[&str]) -> Result<CommandResult, RunnerError> {
|
||||
self.run_with_env(args, &[]).await
|
||||
}
|
||||
|
||||
/// Run a command with environment values that are excluded from diagnostics.
|
||||
pub async fn run_with_env(
|
||||
&self,
|
||||
args: &[&str],
|
||||
environment: &[(&str, &str)],
|
||||
) -> Result<CommandResult, RunnerError> {
|
||||
self.runner
|
||||
.run_strings(
|
||||
&self.step,
|
||||
&self.description,
|
||||
args.iter().map(|arg| (*arg).to_string()).collect(),
|
||||
environment
|
||||
.iter()
|
||||
.map(|(name, value)| ((*name).to_string(), (*value).to_string()))
|
||||
.collect(),
|
||||
self.timeout,
|
||||
)
|
||||
.await
|
||||
@@ -724,7 +805,12 @@ mod tests {
|
||||
}
|
||||
|
||||
impl CliExecutor for MockCli {
|
||||
fn execute(&self, args: Vec<String>, _command_timeout: Duration) -> CliExecution<'_> {
|
||||
fn execute(
|
||||
&self,
|
||||
args: Vec<String>,
|
||||
_environment: Vec<(String, String)>,
|
||||
_command_timeout: Duration,
|
||||
) -> CliExecution<'_> {
|
||||
let response = {
|
||||
let mut state = self.state.lock().expect("lock mock CLI state");
|
||||
state.invocations.push(args);
|
||||
|
||||
@@ -34,6 +34,11 @@ COMMUNITY_SANDBOX_IMAGE="${COMMUNITY_SANDBOX_IMAGE:-ghcr.io/nvidia/openshell-com
|
||||
UMOCI_VERSION="${UMOCI_VERSION:-v0.6.0}"
|
||||
|
||||
# ── libkrunfw upstream (commit-pinned) ─────────────────────────────────
|
||||
# Repo: https://github.com/containers/libkrunfw
|
||||
# Pinned: 2026-03-27 (main branch HEAD at time of pinning)
|
||||
LIBKRUNFW_REF="${LIBKRUNFW_REF:-463f717bbdd916e1352a025b6fb2456e882b0b39}"
|
||||
# Repo: https://github.com/libkrun/libkrunfw
|
||||
# Pinned: v5.6.1 (2026-09-16)
|
||||
LIBKRUNFW_REF="${LIBKRUNFW_REF:-617938cf2ae9a3a0e5696d23a5d6252cd7a1ef4f}"
|
||||
|
||||
# ── libkrun upstream (commit-pinned) ──────────────────────────────────
|
||||
# Repo: https://github.com/libkrun/libkrun
|
||||
# Pinned: v1.19.4 (2026-09-16)
|
||||
LIBKRUN_REF="${LIBKRUN_REF:-728df8125077d0db44265f6e997c72b81b65c015}"
|
||||
|
||||
@@ -2670,7 +2670,7 @@ fn prepare_startup_configuration(
|
||||
"Effective configuration admission rejected"
|
||||
));
|
||||
}
|
||||
if provider.readiness_reason != ProviderReadinessReason::Unspecified {
|
||||
if !provider_environment_is_installable(provider.readiness_reason) {
|
||||
return Err(miette::miette!(
|
||||
"Provider credentials are not ready for installation"
|
||||
));
|
||||
@@ -2686,6 +2686,18 @@ fn prepare_startup_configuration(
|
||||
Ok((engine, process_policy, credentials))
|
||||
}
|
||||
|
||||
/// Whether the provider response contains a complete environment snapshot that
|
||||
/// can be installed. Withheld and expired credentials are intentionally absent,
|
||||
/// so those responses remain valid fail-closed snapshots.
|
||||
fn provider_environment_is_installable(reason: ProviderReadinessReason) -> bool {
|
||||
matches!(
|
||||
reason,
|
||||
ProviderReadinessReason::Unspecified
|
||||
| ProviderReadinessReason::CredentialsWithheld
|
||||
| ProviderReadinessReason::CredentialExpired
|
||||
)
|
||||
}
|
||||
|
||||
fn prepare_provider_environment(
|
||||
provider: &openshell_core::grpc_client::ProviderEnvironmentResult,
|
||||
) -> Result<ProviderCredentialState> {
|
||||
@@ -4296,7 +4308,7 @@ async fn run_policy_poll_loop_with_client<C: PolicyGatewayClient>(
|
||||
{
|
||||
Ok(provider)
|
||||
if EnvironmentIdentity::from_environment(&provider) == desired_identity
|
||||
&& provider.readiness_reason == ProviderReadinessReason::Unspecified =>
|
||||
&& provider_environment_is_installable(provider.readiness_reason) =>
|
||||
{
|
||||
provider
|
||||
}
|
||||
@@ -5562,6 +5574,49 @@ network_policies:
|
||||
assert_eq!(credentials.revision(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_configuration_accepts_fail_closed_provider_environment() {
|
||||
let policy = proto_policy_fixture();
|
||||
let mut snapshot = settings_poll_result(
|
||||
Some(policy.clone()),
|
||||
1,
|
||||
openshell_core::proto::PolicySource::Sandbox,
|
||||
);
|
||||
snapshot.provider_env_revision = 10;
|
||||
|
||||
for reason in [
|
||||
ProviderReadinessReason::CredentialsWithheld,
|
||||
ProviderReadinessReason::CredentialExpired,
|
||||
] {
|
||||
let mut provider = startup_provider(10);
|
||||
provider.readiness_reason = reason;
|
||||
provider
|
||||
.environment
|
||||
.insert("PROJECT_ID".to_string(), "example-project".to_string());
|
||||
provider
|
||||
.non_secret_environment_keys
|
||||
.push("PROJECT_ID".to_string());
|
||||
let (_, _, credentials) = prepare_startup_configuration(&snapshot, &policy, &provider)
|
||||
.expect("withheld credentials preserve fail-closed startup");
|
||||
assert_eq!(credentials.revision(), 10);
|
||||
assert!(credentials.snapshot().child_env.contains_key("PROJECT_ID"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_configuration_rejects_provider_installation_failure() {
|
||||
let policy = proto_policy_fixture();
|
||||
let snapshot = settings_poll_result(
|
||||
Some(policy.clone()),
|
||||
1,
|
||||
openshell_core::proto::PolicySource::Sandbox,
|
||||
);
|
||||
let mut provider = startup_provider(0);
|
||||
provider.readiness_reason = ProviderReadinessReason::CredentialInstallFailed;
|
||||
|
||||
assert!(prepare_startup_configuration(&snapshot, &policy, &provider).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_configuration_revalidates_on_restart_and_accepts_repair() {
|
||||
let policy = proto_policy_fixture();
|
||||
@@ -6153,6 +6208,69 @@ network_policies:
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_poll_installs_fail_closed_environment_and_acknowledges_policy() {
|
||||
let policy = proto_policy_fixture();
|
||||
let initial = settings_poll_result(
|
||||
Some(policy.clone()),
|
||||
1,
|
||||
openshell_core::proto::PolicySource::Sandbox,
|
||||
);
|
||||
let engine = Arc::new(OpaEngine::from_proto(&policy).unwrap());
|
||||
let ctx = policy_poll_test_context(
|
||||
engine,
|
||||
LoadedPolicyOrigin::Gateway {
|
||||
revision: Some(LoadedPolicyRevision::from_snapshot(&initial)),
|
||||
has_last_valid_policy: true,
|
||||
},
|
||||
default_middleware_connector(),
|
||||
);
|
||||
let credentials = ctx.provider_credentials.clone();
|
||||
let (policy_gateway, polls, mut reports) = scripted_policy_gateway();
|
||||
let (requests, mut received) = tokio::sync::mpsc::unbounded_channel();
|
||||
polls.send(initial).unwrap();
|
||||
let task = tokio::spawn(run_policy_poll_loop_with_client(
|
||||
ctx,
|
||||
ScriptedProviderGateway {
|
||||
policy: policy_gateway,
|
||||
requests,
|
||||
},
|
||||
));
|
||||
expect_policy_report(&mut reports, 1).await;
|
||||
|
||||
let mut changed = settings_poll_result(
|
||||
Some(policy),
|
||||
2,
|
||||
openshell_core::proto::PolicySource::Sandbox,
|
||||
);
|
||||
changed.provider_env_revision = 1;
|
||||
polls.send(changed).unwrap();
|
||||
let response = timeout(Duration::from_secs(1), received.recv())
|
||||
.await
|
||||
.expect("provider refresh requested")
|
||||
.expect("poll loop active");
|
||||
let mut withheld = static_provider_environment(1, None);
|
||||
withheld.policy_hash = "hash-v2".to_string();
|
||||
withheld.readiness_reason = ProviderReadinessReason::CredentialsWithheld;
|
||||
withheld
|
||||
.environment
|
||||
.insert("PROJECT_ID".to_string(), "example-project".to_string());
|
||||
withheld
|
||||
.non_secret_environment_keys
|
||||
.push("PROJECT_ID".to_string());
|
||||
assert!(response.send(Ok(withheld)).is_ok());
|
||||
|
||||
expect_policy_report(&mut reports, 2).await;
|
||||
assert_eq!(credentials.revision(), 1);
|
||||
assert!(
|
||||
credentials.snapshot().child_env.contains_key("PROJECT_ID"),
|
||||
"the reduced snapshot retains non-secret provider configuration"
|
||||
);
|
||||
|
||||
task.abort();
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_poll_preserves_static_references_across_rotation_failure_and_detach() {
|
||||
let engine = Arc::new(OpaEngine::from_proto(&proto_policy_fixture()).unwrap());
|
||||
|
||||
@@ -35,7 +35,6 @@ e2e-kubernetes-workspace-operator = ["e2e-kubernetes"]
|
||||
e2e-podman = ["e2e", "e2e-host-gateway", "e2e-local-container-driver"]
|
||||
e2e-podman-gpu = ["e2e-podman", "e2e-gpu"]
|
||||
e2e-oidc-pkce = []
|
||||
e2e-provider-refresh-keycloak = []
|
||||
e2e-vm = ["e2e", "e2e-host-gateway"]
|
||||
|
||||
[[test]]
|
||||
@@ -48,11 +47,6 @@ name = "oidc_pkce"
|
||||
path = "tests/oidc_pkce.rs"
|
||||
required-features = ["e2e-oidc-pkce"]
|
||||
|
||||
[[test]]
|
||||
name = "provider_refresh_keycloak"
|
||||
path = "tests/provider_refresh_keycloak.rs"
|
||||
required-features = ["e2e-provider-refresh-keycloak"]
|
||||
|
||||
[[test]]
|
||||
name = "vm_overlay"
|
||||
path = "tests/vm_overlay.rs"
|
||||
|
||||
+46
-11
@@ -14,6 +14,9 @@
|
||||
# via `server.hostGatewayIP`. Targeting a cluster where the test host is
|
||||
# unreachable from pods? Set OPENSHELL_E2E_KUBERNETES_FEATURES=e2e to drop the
|
||||
# alias-dependent tests entirely.
|
||||
#
|
||||
# Results: `run_suite` writes a JUnit + HTML report under `results/`. Set
|
||||
# `OPENSHELL_E2E_REPORT_NAME` to name it per run when invoking this script repeatedly.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -24,6 +27,25 @@ source "${ROOT}/e2e/support/conformance.sh"
|
||||
|
||||
E2E_FEATURES="${OPENSHELL_E2E_KUBERNETES_FEATURES-e2e,e2e-host-gateway,e2e-kubernetes}"
|
||||
|
||||
# Fixed output path of the `e2e-kubernetes` nextest profile (`.config/nextest.toml`).
|
||||
JUNIT_XML="${ROOT}/results/e2e-kubernetes.xml"
|
||||
|
||||
# Render a sibling HTML report from a JUnit XML. Best-effort: failures only warn.
|
||||
render_html() {
|
||||
local xml="${1:-${JUNIT_XML}}"
|
||||
local title="${2:-e2e-kubernetes}"
|
||||
[ -f "${xml}" ] || return 0
|
||||
local html="${xml%.xml}.html"
|
||||
if command -v xsltproc >/dev/null 2>&1; then
|
||||
xsltproc --stringparam title "${title}" \
|
||||
"${ROOT}/scripts/junit-to-html.xsl" "${xml}" >"${html}" \
|
||||
&& echo "HTML report: ${html}" \
|
||||
|| echo "WARNING: failed to render HTML report from ${xml}" >&2
|
||||
else
|
||||
echo "WARNING: xsltproc not found; skipping HTML report (${xml} still written)" >&2
|
||||
fi
|
||||
}
|
||||
|
||||
# Docker and Podman build their local gateway and CLI together in the shared
|
||||
# gateway wrapper. Kubernetes consumes published gateway images, so only its
|
||||
# local CLI needs to be built when CI has not supplied a prebuilt one.
|
||||
@@ -50,9 +72,19 @@ run_conformance() {
|
||||
e2e_run_openshell_conformance "Kubernetes"
|
||||
}
|
||||
|
||||
# `OPENSHELL_E2E_REPORT_NAME` (default `e2e-kubernetes`) names the report
|
||||
# `results/<name>.{xml,html}` and its heading, so repeated runs do not clobber.
|
||||
run_suite() {
|
||||
local name="${OPENSHELL_E2E_REPORT_NAME:-e2e-kubernetes}"
|
||||
local report="${ROOT}/results/${name}.xml"
|
||||
local status=0
|
||||
"${ROOT}/e2e/with-kube-gateway.sh" \
|
||||
bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}"
|
||||
bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}" || status=$?
|
||||
if [ "${report}" != "${JUNIT_XML}" ]; then
|
||||
mv -f "${JUNIT_XML}" "${report}" 2>/dev/null || true
|
||||
fi
|
||||
render_html "${report}" "${name}"
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
run_e2e() {
|
||||
@@ -61,11 +93,13 @@ run_e2e() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
cargo test --manifest-path "${ROOT}/e2e/rust/Cargo.toml" \
|
||||
# Pin `--target-dir` so the profile `junit.path` resolves regardless of `CARGO_TARGET_DIR`.
|
||||
cargo nextest run --profile e2e-kubernetes \
|
||||
--config-file "${ROOT}/.config/nextest.toml" \
|
||||
--target-dir "${ROOT}/e2e/rust/target" \
|
||||
--manifest-path "${ROOT}/e2e/rust/Cargo.toml" \
|
||||
--features "${E2E_FEATURES}" \
|
||||
--no-fail-fast \
|
||||
${test_filter[@]+"${test_filter[@]}"} \
|
||||
-- --nocapture
|
||||
${test_filter[@]+"${test_filter[@]}"}
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "${RUN_WITH_GATEWAY_COMMAND}" ]; then
|
||||
@@ -73,12 +107,13 @@ if [ "${1:-}" = "${RUN_WITH_GATEWAY_COMMAND}" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Credential-driver mode: run once per storage backend, each with its own report.
|
||||
if [ "${OPENSHELL_E2E_CREDENTIAL_DRIVERS:-0}" = "1" ] \
|
||||
&& [ -z "${OPENSHELL_E2E_CREDENTIAL_DRIVER:-}" ]; then
|
||||
OPENSHELL_E2E_CREDENTIAL_DRIVER=kubernetes-secrets run_suite
|
||||
OPENSHELL_E2E_CREDENTIAL_DRIVER=vault run_suite
|
||||
exit 0
|
||||
OPENSHELL_E2E_CREDENTIAL_DRIVER=kubernetes-secrets \
|
||||
OPENSHELL_E2E_REPORT_NAME=e2e-kubernetes-secrets run_suite
|
||||
OPENSHELL_E2E_CREDENTIAL_DRIVER=vault \
|
||||
OPENSHELL_E2E_REPORT_NAME=e2e-kubernetes-vault run_suite
|
||||
else
|
||||
run_suite
|
||||
fi
|
||||
|
||||
exec "${ROOT}/e2e/with-kube-gateway.sh" \
|
||||
bash "${BASH_SOURCE[0]}" "${RUN_WITH_GATEWAY_COMMAND}"
|
||||
|
||||
+3
-1
@@ -13,7 +13,7 @@ OpenShell uses [Fern](https://buildwithfern.com/) to validate, preview, and publ
|
||||
| `fern/assets/` | Logos and other shared assets. |
|
||||
| `fern/main.css` | Site-wide styles. |
|
||||
|
||||
In a normal source checkout, `fern/docs.yml` points the `latest` version at `docs/index.yml`. Release automation builds the multi-version configuration on the generated `docs-website` branch.
|
||||
In a normal source checkout, `fern/docs.yml` points the `dev` version at `docs/index.yml`. Release automation builds the multi-version configuration on the generated `docs-website` branch and maps the source documentation to the channel being published.
|
||||
|
||||
## Local development
|
||||
|
||||
@@ -54,6 +54,8 @@ The sync and publish workflows share the `docs-website` concurrency group. This
|
||||
|
||||
The `dev` snapshot also owns the shared Fern configuration, components, assets, and CSS on `docs-website`. The `latest` snapshot copies its documentation and navigation but does not replace those shared files. This keeps the site configuration aligned with `main` while preserving the released content.
|
||||
|
||||
A `dev` sync copies the top-level `announcement` from the source `fern/docs.yml`. This announcement is the global fallback, and removing it from the source removes it from `docs-website`. Each snapshot sync copies the source version announcement only to the channel being updated. A version announcement overrides the global announcement for that version, so Release Dev cannot change the `latest` announcement and Release Tag cannot change the `dev` announcement.
|
||||
|
||||
## Manual maintenance and publishing
|
||||
|
||||
Maintainers can run `.github/workflows/sync-docs.yml` manually to add, refresh, or remove a historical version snapshot. The workflow preserves snapshots that were not selected. Production publishing is disabled by default for a manual sync.
|
||||
|
||||
+5
-4
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Fern site config — merged with skills/convert-to-fern/assets/theme/nvidia/docs-theme.yml pattern
|
||||
# Production: https://docs.nvidia.com/openshell/ — preview + custom-domain share path prefix `openshell`.
|
||||
# Internal MDX links use /latest/...; basepath-aware resolves them under /openshell/ on production.
|
||||
# Source checkouts and pull request previews expose the current documentation at /dev.
|
||||
instances:
|
||||
- url: openshell.docs.buildwithfern.com/openshell
|
||||
custom-domain: docs.nvidia.com/openshell
|
||||
@@ -38,7 +38,7 @@ logo:
|
||||
dark: ./assets/NVIDIA_dark.svg
|
||||
light: ./assets/NVIDIA_light.svg
|
||||
height: 20
|
||||
href: /openshell/latest
|
||||
href: /openshell
|
||||
right-text: OpenShell
|
||||
|
||||
favicon: ./assets/NVIDIA_symbol.svg
|
||||
@@ -56,9 +56,10 @@ experimental:
|
||||
- ./components
|
||||
|
||||
versions:
|
||||
- display-name: Latest
|
||||
- display-name: Dev
|
||||
path: ../docs/index.yml
|
||||
slug: latest
|
||||
slug: dev
|
||||
availability: beta
|
||||
announcement:
|
||||
message: '<span style="display: block; padding: 0.375rem 0; text-align: left;"><strong>OpenShell 0.1.0 is coming soon.</strong> <a href="https://github.com/NVIDIA/OpenShell/milestone/10" target="_blank" rel="noreferrer">Track progress in the 0.1.0 milestone</a>, <a href="https://docs.nvidia.com/openshell/dev/index.html" target="_blank" rel="noreferrer">read the prerelease documentation</a>, or <a href="https://docs.nvidia.com/openshell/dev/about/installation#install-a-prerelease" target="_blank" rel="noreferrer">install a prerelease</a>.</span>'
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
};
|
||||
build-artifacts-test-archives = {
|
||||
type = "app";
|
||||
program = "${artifacts.conformanceCliArchive}/bin/build-openshell-conformance-test-archive";
|
||||
program = "${artifacts.testArchives}/bin/build-artifacts-test-archives";
|
||||
};
|
||||
build-artifacts-helm = {
|
||||
type = "app";
|
||||
|
||||
@@ -136,11 +136,6 @@ checksum = "sha256:f73a5c39f96bb6ebb89cc7915cf182260d4cbf30765322c5e793d0fe8bd80
|
||||
url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-aarch64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060468"
|
||||
|
||||
[tools."github:mozilla/sccache"."platforms.linux-x64"]
|
||||
checksum = "sha256:aec995a83ad3dff3d14b6314e08858b7b73d35ca85a5bcf3d3a9ec07dee35588"
|
||||
url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-x86_64-unknown-linux-musl.tar.gz"
|
||||
url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060682"
|
||||
|
||||
[tools."github:mozilla/sccache"."platforms.macos-arm64"]
|
||||
checksum = "sha256:ded590cae2c72042c61178632906bef62d635fa20d45f8b22110a2241f430960"
|
||||
url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.16.0-aarch64-apple-darwin.tar.gz"
|
||||
@@ -169,9 +164,12 @@ url = "https://github.com/mozilla/sccache/releases/download/v0.16.0/sccache-v0.1
|
||||
url_api = "https://api.github.com/repos/mozilla/sccache/releases/assets/452060682"
|
||||
|
||||
[[tools."github:nextest-rs/nextest"]]
|
||||
version = "cargo-nextest-0.9.143"
|
||||
version = "0.9.143"
|
||||
backend = "github:nextest-rs/nextest"
|
||||
|
||||
[tools."github:nextest-rs/nextest".options]
|
||||
version_prefix = "cargo-nextest-"
|
||||
|
||||
[tools."github:nextest-rs/nextest"."platforms.linux-arm64"]
|
||||
checksum = "sha256:2a64b3566a92508550a7ab29c3e8db25472ca37730ecb4d22100b6aa440c2a68"
|
||||
url = "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.143/cargo-nextest-0.9.143-aarch64-unknown-linux-gnu.tar.gz"
|
||||
@@ -183,6 +181,7 @@ checksum = "sha256:66786b9abe23920d022a182d1416b1bbc8130dd4872a9553d76985a1708dc
|
||||
url = "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-0.9.143/cargo-nextest-0.9.143-x86_64-unknown-linux-gnu.tar.gz"
|
||||
url_api = "https://api.github.com/repos/nextest-rs/nextest/releases/assets/501882467"
|
||||
provenance = "github-attestations"
|
||||
provenance_verified = true
|
||||
|
||||
[tools."github:nextest-rs/nextest"."platforms.macos-arm64"]
|
||||
checksum = "sha256:4830d430411148d17602a75cc880bfb4dc8dac153dea59a48a2ef4cc93577f07"
|
||||
|
||||
@@ -44,8 +44,8 @@ k3d = { version = "5.8.3", os = ["macos"] }
|
||||
"github:EmbarkStudios/cargo-deny" = { version = "0.20.2", version_prefix = "" }
|
||||
zig = "0.14.1"
|
||||
"github:rust-secure-code/cargo-auditable" = "0.7.5"
|
||||
"github:nextest-rs/nextest" = { version = "0.9.143", version_prefix = "cargo-nextest-" }
|
||||
"github:rust-cross/cargo-zigbuild" = "0.22.3"
|
||||
"github:nextest-rs/nextest" = { version = "cargo-nextest-0.9.143", os = ["windows"] }
|
||||
"npm:markdownlint-cli2" = "0.22.0"
|
||||
|
||||
[tools."github:mozilla/sccache"]
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
<!--
|
||||
Convert a nextest JUnit report into a standalone HTML report (XSLT 1.0).
|
||||
|
||||
Usage:
|
||||
xsltproc junit-to-html.xsl report.xml > report.html
|
||||
xsltproc -stringparam title "my suite report" junit-to-html.xsl report.xml > report.html
|
||||
|
||||
Renders a summary plus a per-suite table with pass/fail/skip rows. The report
|
||||
heading is the `title` parameter (default "Test report").
|
||||
-->
|
||||
<xsl:stylesheet version="1.0"
|
||||
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
||||
<xsl:output method="html" indent="yes" encoding="UTF-8"
|
||||
doctype-system="about:legacy-compat"/>
|
||||
|
||||
<!-- Report heading; override via the xsltproc `title` string parameter. -->
|
||||
<xsl:param name="title" select="'Test report'"/>
|
||||
|
||||
<xsl:template match="/testsuites">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title><xsl:value-of select="$title"/></title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f7f9; --card: #fff; --ink: #1f2328; --muted: #656d76;
|
||||
--border: #d7dbe0; --pass: #1a7f37; --fail: #cf222e; --skip: #9a6700;
|
||||
--pass-bg: #eafbe7; --fail-bg: #fbecec; --skip-bg: #fdf6e3;
|
||||
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
margin: 0; padding: 2rem clamp(1rem, 4vw, 3rem); color: var(--ink);
|
||||
background: var(--bg); line-height: 1.45;
|
||||
}
|
||||
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
|
||||
h2 {
|
||||
font-family: var(--mono); font-size: 0.95rem; font-weight: 600;
|
||||
margin: 2rem 0 0; color: var(--ink);
|
||||
}
|
||||
h2 .count { color: var(--muted); font-weight: 400; }
|
||||
|
||||
.summary { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0 0 0.5rem; }
|
||||
.summary .card {
|
||||
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 0.5rem 0.9rem; min-width: 5.5rem;
|
||||
}
|
||||
.summary .card .label {
|
||||
display: block; font-size: 0.7rem; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; color: var(--muted);
|
||||
}
|
||||
.summary .card .value { font-size: 1.35rem; font-weight: 700; }
|
||||
.card.c-fail .value { color: var(--fail); }
|
||||
.card.c-skip .value { color: var(--skip); }
|
||||
.card.c-fail.zero .value, .card.c-skip.zero .value { color: var(--muted); }
|
||||
|
||||
table {
|
||||
border-collapse: collapse; width: 100%; margin-top: 0.6rem; table-layout: fixed;
|
||||
background: var(--card); border: 1px solid var(--border);
|
||||
border-radius: 8px; overflow: hidden;
|
||||
}
|
||||
th, td {
|
||||
text-align: left; padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--border); vertical-align: top;
|
||||
}
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
td:first-child { font-family: var(--mono); font-size: 0.85rem; overflow-wrap: anywhere; }
|
||||
th {
|
||||
position: sticky; top: 0; background: #eef1f4; z-index: 1;
|
||||
font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.04em;
|
||||
color: var(--muted);
|
||||
}
|
||||
tbody tr:nth-child(even) { background: #fafbfc; }
|
||||
tbody tr:hover { background: #eef4ff; }
|
||||
tr.failed, tr.failed:nth-child(even) { background: var(--fail-bg); }
|
||||
tr.skipped, tr.skipped:nth-child(even) { background: var(--skip-bg); }
|
||||
td.time { font-family: var(--mono); font-size: 0.85rem; color: var(--muted); }
|
||||
|
||||
.badge {
|
||||
display: inline-block; padding: 0.1rem 0.5rem; border-radius: 999px;
|
||||
font-size: 0.72rem; font-weight: 700; letter-spacing: 0.03em;
|
||||
}
|
||||
.status-pass { background: var(--pass-bg); color: var(--pass); }
|
||||
.status-fail { background: var(--fail-bg); color: var(--fail); }
|
||||
.status-skip { background: var(--skip-bg); color: var(--skip); }
|
||||
pre {
|
||||
margin: 0.4rem 0 0; padding: 0.5rem 0.6rem; white-space: pre-wrap;
|
||||
color: var(--fail); background: #fff; border: 1px solid var(--border);
|
||||
border-radius: 6px; font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><xsl:value-of select="$title"/></h1>
|
||||
<div class="summary">
|
||||
<div class="card">
|
||||
<span class="label">Total</span>
|
||||
<span class="value"><xsl:value-of select="@tests"/></span>
|
||||
</div>
|
||||
<div class="card c-fail">
|
||||
<xsl:if test="@failures = 0"><xsl:attribute name="class">card c-fail zero</xsl:attribute></xsl:if>
|
||||
<span class="label">Failures</span>
|
||||
<span class="value"><xsl:value-of select="@failures"/></span>
|
||||
</div>
|
||||
<div class="card c-fail">
|
||||
<xsl:if test="@errors = 0"><xsl:attribute name="class">card c-fail zero</xsl:attribute></xsl:if>
|
||||
<span class="label">Errors</span>
|
||||
<span class="value"><xsl:value-of select="@errors"/></span>
|
||||
</div>
|
||||
<div class="card c-skip">
|
||||
<xsl:if test="sum(testsuite/@skipped) = 0"><xsl:attribute name="class">card c-skip zero</xsl:attribute></xsl:if>
|
||||
<span class="label">Skipped</span>
|
||||
<span class="value"><xsl:value-of select="sum(testsuite/@skipped)"/></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="label">Time</span>
|
||||
<span class="value"><xsl:value-of select="@time"/>s</span>
|
||||
</div>
|
||||
</div>
|
||||
<xsl:for-each select="testsuite">
|
||||
<h2><xsl:value-of select="@name"/>
|
||||
<xsl:text> </xsl:text>
|
||||
<span class="count">(<xsl:value-of select="count(testcase)"/> tests, <xsl:value-of select="@time"/>s)</span>
|
||||
</h2>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col/>
|
||||
<col style="width: 6rem;"/>
|
||||
<col style="width: 6rem;"/>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr><th>Test</th><th>Status</th><th>Time</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<xsl:for-each select="testcase">
|
||||
<xsl:choose>
|
||||
<xsl:when test="failure or error">
|
||||
<tr class="failed">
|
||||
<td><xsl:value-of select="@name"/>
|
||||
<pre><xsl:value-of select="failure | error"/></pre>
|
||||
</td>
|
||||
<td><span class="badge status-fail">FAIL</span></td>
|
||||
<td class="time"><xsl:value-of select="@time"/>s</td>
|
||||
</tr>
|
||||
</xsl:when>
|
||||
<xsl:when test="skipped">
|
||||
<tr class="skipped">
|
||||
<td><xsl:value-of select="@name"/></td>
|
||||
<td><span class="badge status-skip">SKIP</span></td>
|
||||
<td class="time"><xsl:value-of select="@time"/>s</td>
|
||||
</tr>
|
||||
</xsl:when>
|
||||
<xsl:otherwise>
|
||||
<tr>
|
||||
<td><xsl:value-of select="@name"/></td>
|
||||
<td><span class="badge status-pass">PASS</span></td>
|
||||
<td class="time"><xsl:value-of select="@time"/>s</td>
|
||||
</tr>
|
||||
</xsl:otherwise>
|
||||
</xsl:choose>
|
||||
</xsl:for-each>
|
||||
</tbody>
|
||||
</table>
|
||||
</xsl:for-each>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
||||
@@ -36,6 +36,7 @@ class VersionEntry:
|
||||
display_name: str
|
||||
path: str
|
||||
availability: str | None = None
|
||||
announcement: YamlMapping | None = None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -312,6 +313,7 @@ def parse_versions(raw_versions: object) -> list[VersionEntry]:
|
||||
display_name = entry.get("display-name")
|
||||
path = entry.get("path")
|
||||
availability = entry.get("availability")
|
||||
announcement = entry.get("announcement")
|
||||
if (
|
||||
isinstance(slug, str)
|
||||
and isinstance(display_name, str)
|
||||
@@ -325,6 +327,9 @@ def parse_versions(raw_versions: object) -> list[VersionEntry]:
|
||||
availability=availability
|
||||
if isinstance(availability, str)
|
||||
else None,
|
||||
announcement=cast("YamlMapping", announcement)
|
||||
if isinstance(announcement, dict)
|
||||
else None,
|
||||
)
|
||||
)
|
||||
return entries
|
||||
@@ -349,8 +354,8 @@ def ordered_entries(
|
||||
return [by_slug[slug] for slug in order]
|
||||
|
||||
|
||||
def render_versions(entries: list[VersionEntry]) -> list[dict[str, str]]:
|
||||
rendered: list[dict[str, str]] = []
|
||||
def render_versions(entries: list[VersionEntry]) -> list[YamlMapping]:
|
||||
rendered: list[YamlMapping] = []
|
||||
for entry in entries:
|
||||
item = {
|
||||
"display-name": entry.display_name,
|
||||
@@ -359,10 +364,36 @@ def render_versions(entries: list[VersionEntry]) -> list[dict[str, str]]:
|
||||
}
|
||||
if entry.availability is not None:
|
||||
item["availability"] = entry.availability
|
||||
if entry.announcement is not None:
|
||||
item["announcement"] = entry.announcement
|
||||
rendered.append(item)
|
||||
return rendered
|
||||
|
||||
|
||||
def sync_global_announcement(source_docs_yml: Path, target_docs_yml: Path) -> None:
|
||||
source_data = read_yaml(source_docs_yml)
|
||||
source_announcement = source_data.get("announcement")
|
||||
if source_announcement is not None and not isinstance(source_announcement, dict):
|
||||
raise ValueError("docs.yml announcement must be a mapping")
|
||||
|
||||
target_data = read_yaml(target_docs_yml)
|
||||
if source_announcement is None:
|
||||
target_data.pop("announcement", None)
|
||||
else:
|
||||
target_data["announcement"] = source_announcement
|
||||
write_yaml(target_docs_yml, target_data)
|
||||
|
||||
|
||||
def source_version_announcement(docs_yml: Path, slug: str) -> YamlMapping | None:
|
||||
entries = parse_versions(read_yaml(docs_yml).get("versions"))
|
||||
for entry in entries:
|
||||
if entry.slug == slug:
|
||||
return entry.announcement
|
||||
if len(entries) == 1:
|
||||
return entries[0].announcement
|
||||
return None
|
||||
|
||||
|
||||
def component_dirs(fern_dir: Path) -> list[str]:
|
||||
dirs: list[str] = []
|
||||
preferred = ["pages-latest", "pages-dev"]
|
||||
@@ -408,6 +439,7 @@ def write_snapshot(
|
||||
copy_if_exists(
|
||||
source_fern / "fern.config.json", target_fern / "fern.config.json"
|
||||
)
|
||||
sync_global_announcement(source_fern / "docs.yml", target_fern / "docs.yml")
|
||||
|
||||
versions_dir = target_fern / "versions"
|
||||
versions_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -483,6 +515,9 @@ def sync_docs(args: argparse.Namespace) -> None:
|
||||
display_name=slug,
|
||||
path=f"./versions/{slug}.yml",
|
||||
availability=stable_availability,
|
||||
announcement=source_version_announcement(
|
||||
source_fern / "docs.yml", slug
|
||||
),
|
||||
),
|
||||
refresh_shared=False,
|
||||
)
|
||||
@@ -509,6 +544,9 @@ def sync_docs(args: argparse.Namespace) -> None:
|
||||
display_name=display_override or f"Latest ({slug})",
|
||||
path="./versions/latest.yml",
|
||||
availability=stable_availability,
|
||||
announcement=source_version_announcement(
|
||||
source_fern / "docs.yml", "latest"
|
||||
),
|
||||
),
|
||||
refresh_shared=False,
|
||||
)
|
||||
@@ -548,6 +586,7 @@ def sync_docs(args: argparse.Namespace) -> None:
|
||||
display_name=display_name,
|
||||
path=f"./versions/{slug}.yml",
|
||||
availability=availability,
|
||||
announcement=source_version_announcement(source_fern / "docs.yml", slug),
|
||||
),
|
||||
refresh_shared=channel == "dev",
|
||||
)
|
||||
|
||||
@@ -147,13 +147,14 @@ def test_resolve_availability() -> None:
|
||||
sdw.resolve_availability("dev", "alpha")
|
||||
|
||||
|
||||
def test_parse_and_render_versions_preserves_availability() -> None:
|
||||
def test_parse_and_render_versions_preserves_version_settings() -> None:
|
||||
raw_versions = [
|
||||
{
|
||||
"display-name": "v0.0.36",
|
||||
"path": "./versions/v0.0.36.yml",
|
||||
"slug": "v0.0.36",
|
||||
"availability": "deprecated",
|
||||
"announcement": {"message": "Upgrade to the latest version."},
|
||||
}
|
||||
]
|
||||
|
||||
@@ -165,11 +166,104 @@ def test_parse_and_render_versions_preserves_availability() -> None:
|
||||
"v0.0.36",
|
||||
"./versions/v0.0.36.yml",
|
||||
"deprecated",
|
||||
{"message": "Upgrade to the latest version."},
|
||||
)
|
||||
]
|
||||
assert sdw.render_versions(entries) == raw_versions
|
||||
|
||||
|
||||
def test_sync_global_announcement_applies_source_config(tmp_path: Path) -> None:
|
||||
source_docs_yml = tmp_path / "source.yml"
|
||||
target_docs_yml = tmp_path / "target.yml"
|
||||
source_docs_yml.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"announcement": {"message": "Current global announcement."},
|
||||
"versions": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
target_docs_yml.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"announcement": {"message": "Stale global announcement."},
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Latest (v0.0.116)",
|
||||
"path": "./versions/latest.yml",
|
||||
"slug": "latest",
|
||||
},
|
||||
{
|
||||
"display-name": "Dev",
|
||||
"path": "./versions/dev.yml",
|
||||
"slug": "dev",
|
||||
"announcement": {"message": "Development docs."},
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
sdw.sync_global_announcement(source_docs_yml, target_docs_yml)
|
||||
|
||||
target_data = read_yaml(target_docs_yml)
|
||||
assert target_data["announcement"] == {"message": "Current global announcement."}
|
||||
assert target_data["versions"] == [
|
||||
{
|
||||
"display-name": "Latest (v0.0.116)",
|
||||
"path": "./versions/latest.yml",
|
||||
"slug": "latest",
|
||||
},
|
||||
{
|
||||
"display-name": "Dev",
|
||||
"path": "./versions/dev.yml",
|
||||
"slug": "dev",
|
||||
"announcement": {"message": "Development docs."},
|
||||
},
|
||||
]
|
||||
|
||||
source_docs_yml.write_text(
|
||||
yaml.safe_dump({"versions": []}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
sdw.sync_global_announcement(source_docs_yml, target_docs_yml)
|
||||
|
||||
target_data = read_yaml(target_docs_yml)
|
||||
assert "announcement" not in target_data
|
||||
versions = target_data["versions"]
|
||||
assert "announcement" not in versions[0]
|
||||
assert versions[1]["announcement"] == {"message": "Development docs."}
|
||||
|
||||
|
||||
def test_source_version_announcement_maps_single_source_version_to_channel(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
docs_yml = tmp_path / "docs.yml"
|
||||
docs_yml.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Dev",
|
||||
"path": "../docs/index.yml",
|
||||
"slug": "dev",
|
||||
"announcement": {"message": "Snapshot announcement."},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
expected = {"message": "Snapshot announcement."}
|
||||
assert sdw.source_version_announcement(docs_yml, "latest") == expected
|
||||
assert sdw.source_version_announcement(docs_yml, "dev") == expected
|
||||
assert sdw.source_version_announcement(docs_yml, "v0.0.116") == expected
|
||||
|
||||
|
||||
def test_ordered_entries_pins_latest_then_dev() -> None:
|
||||
existing = [
|
||||
sdw.VersionEntry("v0.0.36", "v0.0.36", "./versions/v0.0.36.yml"),
|
||||
@@ -212,6 +306,8 @@ def _make_source_tree(root: Path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
fern = root / "fern"
|
||||
fern.mkdir(parents=True)
|
||||
(fern / "docs.yml").write_text(yaml.safe_dump({"versions": []}), encoding="utf-8")
|
||||
(fern / "assets").mkdir(parents=True)
|
||||
(fern / "assets" / "logo.svg").write_text("<svg/>", encoding="utf-8")
|
||||
(fern / "components").mkdir(parents=True)
|
||||
@@ -228,11 +324,39 @@ def _make_docs_website_tree(root: Path) -> None:
|
||||
(fern / "docs.yml").write_text(yaml.safe_dump({"versions": []}), encoding="utf-8")
|
||||
|
||||
|
||||
def test_sync_docs_creates_planned_latest_and_dev_selector(tmp_path: Path) -> None:
|
||||
def test_sync_docs_scopes_version_announcements_to_updated_channel(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
source = tmp_path / "source"
|
||||
website = tmp_path / "docs-website"
|
||||
_make_source_tree(source)
|
||||
_make_docs_website_tree(website)
|
||||
(source / "fern" / "docs.yml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Latest",
|
||||
"path": "../docs/index.yml",
|
||||
"slug": "latest",
|
||||
"announcement": {
|
||||
"message": "Version 0.0.116 is the final alpha release."
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(website / "fern" / "docs.yml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"announcement": {"message": "Stale global announcement."},
|
||||
"versions": [],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
sdw.sync_docs(
|
||||
Namespace(
|
||||
@@ -248,6 +372,21 @@ def test_sync_docs_creates_planned_latest_and_dev_selector(tmp_path: Path) -> No
|
||||
availability="",
|
||||
)
|
||||
)
|
||||
(source / "fern" / "docs.yml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Dev",
|
||||
"path": "../docs/index.yml",
|
||||
"slug": "dev",
|
||||
"announcement": {"message": "OpenShell 0.1.0 is coming soon."},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sdw.sync_docs(
|
||||
Namespace(
|
||||
operation="sync",
|
||||
@@ -276,16 +415,53 @@ def test_sync_docs_creates_planned_latest_and_dev_selector(tmp_path: Path) -> No
|
||||
"display-name": "Latest (v0.0.116)",
|
||||
"path": "./versions/latest.yml",
|
||||
"slug": "latest",
|
||||
"announcement": {"message": "Version 0.0.116 is the final alpha release."},
|
||||
},
|
||||
{
|
||||
"display-name": "Dev",
|
||||
"path": "./versions/dev.yml",
|
||||
"slug": "dev",
|
||||
"availability": "beta",
|
||||
"announcement": {"message": "OpenShell 0.1.0 is coming soon."},
|
||||
},
|
||||
]
|
||||
assert "announcement" not in docs_yml
|
||||
assert "./components" in docs_yml["experimental"]["mdx-components"]
|
||||
|
||||
(source / "fern" / "docs.yml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Dev",
|
||||
"path": "../docs/index.yml",
|
||||
"slug": "dev",
|
||||
"announcement": {"message": "OpenShell 0.1.0 is released."},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sdw.sync_docs(
|
||||
Namespace(
|
||||
operation="sync",
|
||||
source_root=source,
|
||||
docs_website_root=website,
|
||||
channel="latest",
|
||||
source_ref="release-0.1.0-sha",
|
||||
source_sha="release-0.1.0-sha",
|
||||
release_version="0.1.0",
|
||||
version_slug="",
|
||||
display_name="Latest (v0.1.0)",
|
||||
availability="",
|
||||
)
|
||||
)
|
||||
|
||||
versions = read_yaml(fern / "docs.yml")["versions"]
|
||||
assert versions[0]["announcement"] == {"message": "OpenShell 0.1.0 is released."}
|
||||
assert versions[1]["announcement"] == {"message": "OpenShell 0.1.0 is coming soon."}
|
||||
|
||||
|
||||
def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source"
|
||||
@@ -341,6 +517,71 @@ def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None:
|
||||
]
|
||||
|
||||
|
||||
def test_latest_sync_updates_legacy_snapshot_announcement(tmp_path: Path) -> None:
|
||||
source = tmp_path / "source"
|
||||
website = tmp_path / "docs-website"
|
||||
_make_source_tree(source)
|
||||
_make_docs_website_tree(website)
|
||||
(source / "fern" / "docs.yml").write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Latest",
|
||||
"path": "../docs/index.yml",
|
||||
"slug": "latest",
|
||||
"announcement": {
|
||||
"message": "Version 0.0.116 is the final alpha release."
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
docs_yml_path = website / "fern" / "docs.yml"
|
||||
docs_yml_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"display-name": "Latest (v0.0.116)",
|
||||
"path": "./versions/latest.yml",
|
||||
"slug": "latest",
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
sdw.sync_docs(
|
||||
Namespace(
|
||||
operation="sync",
|
||||
source_root=source,
|
||||
docs_website_root=website,
|
||||
channel="latest",
|
||||
source_ref="docs/v0.0.116-announcement",
|
||||
source_sha="announcement-sha",
|
||||
release_version="0.0.116",
|
||||
version_slug="",
|
||||
display_name="Latest (v0.0.116)",
|
||||
availability="",
|
||||
)
|
||||
)
|
||||
|
||||
latest = read_yaml(docs_yml_path)["versions"][0]
|
||||
assert latest["announcement"] == {
|
||||
"message": "Version 0.0.116 is the final alpha release."
|
||||
}
|
||||
snapshots = read_yaml(website / "fern" / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
|
||||
assert snapshots["latest"] == {
|
||||
"source-ref": "docs/v0.0.116-announcement",
|
||||
"source-sha": "announcement-sha",
|
||||
"version": "0.0.116",
|
||||
}
|
||||
|
||||
|
||||
def test_stable_sync_creates_immutable_version_and_promotes_latest(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -31,6 +31,10 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
|
||||
# Source pinned dependency versions.
|
||||
source "${ROOT}/crates/openshell-driver-vm/runtime/pins.env" 2>/dev/null || true
|
||||
|
||||
BUILD_DIR="${ROOT}/target/libkrun-build"
|
||||
OUTPUT_DIR="${BUILD_DIR}"
|
||||
BREW_PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)"
|
||||
@@ -149,11 +153,12 @@ echo " Built: libkrunfw.${ABI_VERSION}.dylib ($(du -sh "${OUTPUT_DIR}/libkrun
|
||||
|
||||
# ── Clone libkrun ───────────────────────────────────────────────────────
|
||||
|
||||
LIBKRUN_REF="${LIBKRUN_REF:-e5922f6}"
|
||||
# LIBKRUN_REF is sourced from pins.env; env-var override still works.
|
||||
LIBKRUN_REF="${LIBKRUN_REF:-728df8125077d0db44265f6e997c72b81b65c015}"
|
||||
|
||||
if [ ! -d libkrun ]; then
|
||||
echo "==> Cloning libkrun..."
|
||||
git clone https://github.com/containers/libkrun.git
|
||||
git clone https://github.com/libkrun/libkrun.git
|
||||
fi
|
||||
|
||||
echo "==> Checking out ${LIBKRUN_REF}..."
|
||||
|
||||
@@ -134,7 +134,7 @@ ensure_python3_with_pyelftools_for_libkrunfw
|
||||
|
||||
if [ ! -d libkrunfw ]; then
|
||||
echo " Cloning libkrunfw (pinned: ${LIBKRUNFW_REF:-HEAD})..."
|
||||
git clone https://github.com/containers/libkrunfw.git
|
||||
git clone https://github.com/libkrun/libkrunfw.git
|
||||
fi
|
||||
|
||||
cd libkrunfw
|
||||
@@ -405,11 +405,12 @@ echo "==> Building libkrun..."
|
||||
ensure_cargo_for_libkrun
|
||||
ensure_libclang_for_libkrun
|
||||
|
||||
LIBKRUN_REF="${LIBKRUN_REF:-v1.17.4}"
|
||||
# LIBKRUN_REF is sourced from pins.env (line 32); env-var override still works.
|
||||
LIBKRUN_REF="${LIBKRUN_REF:-728df8125077d0db44265f6e997c72b81b65c015}"
|
||||
|
||||
if [ ! -d libkrun ]; then
|
||||
echo " Cloning libkrun..."
|
||||
git clone https://github.com/containers/libkrun.git
|
||||
git clone https://github.com/libkrun/libkrun.git
|
||||
fi
|
||||
|
||||
cd libkrun
|
||||
@@ -420,11 +421,6 @@ if [ -n "${LIBKRUN_REF:-}" ]; then
|
||||
git checkout "${LIBKRUN_REF}" 2>/dev/null || git checkout "origin/${LIBKRUN_REF}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -f init/Makefile ] || grep -q 'init/init' Makefile 2>/dev/null; then
|
||||
echo " Building init/init binary..."
|
||||
make init/init
|
||||
fi
|
||||
|
||||
echo " Building libkrun with NET=1 BLK=1..."
|
||||
cargo build --release --features blk --features net --target-dir="$(pwd)/target"
|
||||
|
||||
|
||||
@@ -146,12 +146,6 @@ run = [
|
||||
"CONTAINER_RUNTIME=podman e2e/with-keycloak.sh env OPENSHELL_E2E_OIDC_GATEWAY=1 e2e/with-podman-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-oidc-pkce --test oidc_pkce",
|
||||
]
|
||||
|
||||
["e2e:provider-refresh-keycloak"]
|
||||
description = "Run provider refresh recovery e2e against Keycloak and a Podman gateway"
|
||||
run = [
|
||||
"CONTAINER_RUNTIME=podman e2e/with-keycloak.sh e2e/with-podman-gateway.sh cargo test --manifest-path e2e/rust/Cargo.toml --features e2e-provider-refresh-keycloak --test provider_refresh_keycloak -- --nocapture",
|
||||
]
|
||||
|
||||
["e2e:oidc-pkce:docker"]
|
||||
description = "Run Linux browser PKCE and RBAC e2e tests against Keycloak and a Docker gateway"
|
||||
run = [
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
- name: Run Keycloak provider refresh tests
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
roles:
|
||||
- keycloak
|
||||
tasks:
|
||||
- name: Wait for SSH
|
||||
ansible.builtin.wait_for_connection:
|
||||
|
||||
- name: Create Keycloak provider refresh test directory
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: /var/lib/openshell-provider-refresh/tests
|
||||
state: directory
|
||||
owner: tmachine
|
||||
group: tmachine
|
||||
mode: "0700"
|
||||
|
||||
- name: Install Keycloak provider refresh test bundle
|
||||
become: true
|
||||
ansible.builtin.copy:
|
||||
src: "{{ provider_refresh_keycloak_test_bundle }}"
|
||||
dest: /var/lib/openshell-provider-refresh/tests/bundle.tar
|
||||
owner: tmachine
|
||||
group: tmachine
|
||||
mode: "0600"
|
||||
|
||||
- name: Extract Keycloak provider refresh test bundle
|
||||
become: true
|
||||
ansible.builtin.unarchive:
|
||||
src: /var/lib/openshell-provider-refresh/tests/bundle.tar
|
||||
dest: /var/lib/openshell-provider-refresh/tests
|
||||
owner: tmachine
|
||||
group: tmachine
|
||||
remote_src: true
|
||||
|
||||
- name: Check Keycloak provider refresh test archive
|
||||
ansible.builtin.stat:
|
||||
path: /var/lib/openshell-provider-refresh/tests/tests.tar.zst
|
||||
register: provider_refresh_keycloak_test_archive
|
||||
|
||||
- name: Require Keycloak provider refresh test archive
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- provider_refresh_keycloak_test_archive.stat.isreg | default(false)
|
||||
fail_msg: Keycloak provider refresh test bundle did not contain tests.tar.zst
|
||||
|
||||
- name: Run Keycloak provider refresh archive
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- cargo-nextest
|
||||
- nextest
|
||||
- run
|
||||
- --archive-file
|
||||
- /var/lib/openshell-provider-refresh/tests/tests.tar.zst
|
||||
- --workspace-remap
|
||||
- /var/lib/openshell-provider-refresh/tests
|
||||
- --no-capture
|
||||
environment:
|
||||
OPENSHELL_BIN: /usr/local/bin/openshell
|
||||
OPENSHELL_E2E_OIDC_ISSUER: http://127.0.0.1:8180/realms/openshell
|
||||
OPENSHELL_E2E_OIDC_USERNAME: admin@test
|
||||
OPENSHELL_E2E_OIDC_PASSWORD: admin
|
||||
register: provider_refresh_result
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Show Keycloak provider refresh result
|
||||
ansible.builtin.debug:
|
||||
var: provider_refresh_result
|
||||
when: provider_refresh_result.rc != 0
|
||||
|
||||
- name: Read OpenShell gateway logs
|
||||
become: true
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- journalctl
|
||||
- --unit
|
||||
- openshell-gateway.service
|
||||
- --no-pager
|
||||
- --lines
|
||||
- "500"
|
||||
register: openshell_gateway_logs
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: provider_refresh_result.rc != 0
|
||||
|
||||
- name: Show OpenShell gateway logs
|
||||
ansible.builtin.debug:
|
||||
var: openshell_gateway_logs.stdout_lines
|
||||
when: provider_refresh_result.rc != 0
|
||||
|
||||
- name: Require Keycloak provider refresh success
|
||||
ansible.builtin.assert:
|
||||
that: provider_refresh_result.rc == 0
|
||||
fail_msg: Keycloak provider refresh test failed
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
- name: Verify SELinux
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
|
||||
tasks:
|
||||
- name: Read the current SELinux mode
|
||||
ansible.builtin.command:
|
||||
cmd: getenforce
|
||||
register: selinux_mode
|
||||
changed_when: false
|
||||
|
||||
- name: Verify enforcing mode
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- selinux_mode.stdout == "Enforcing"
|
||||
fail_msg: SELinux is not enforcing.
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
# Keep these values aligned with scripts/keycloak-dev.sh. The role provisions
|
||||
# an isolated guest fixture, while the script manages a developer-host fixture.
|
||||
keycloak_container_name: openshell-keycloak
|
||||
keycloak_image: quay.io/keycloak/keycloak:24.0
|
||||
keycloak_port: 8180
|
||||
keycloak_realm: openshell
|
||||
keycloak_admin_username: admin
|
||||
keycloak_admin_password: admin
|
||||
keycloak_state_directory: /var/lib/openshell-keycloak
|
||||
keycloak_health_retries: 45
|
||||
keycloak_health_delay: 2
|
||||
@@ -0,0 +1,126 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
- name: Wait for SSH
|
||||
ansible.builtin.wait_for_connection:
|
||||
|
||||
- name: Detect tmachine container runtime
|
||||
ansible.builtin.include_role:
|
||||
name: tmachine_container_runtime
|
||||
|
||||
- name: Set Keycloak runtime values
|
||||
ansible.builtin.set_fact:
|
||||
keycloak_runtime_user: >-
|
||||
{{ 'tmachine' if tmachine_container_runtime_is_rootless else 'root' }}
|
||||
keycloak_runtime_home: >-
|
||||
{{ '/home/tmachine' if tmachine_container_runtime_is_rootless else '/root' }}
|
||||
keycloak_runtime_state_directory: >-
|
||||
{{ '/home/tmachine/.local/share/openshell-keycloak'
|
||||
if tmachine_container_runtime_is_rootless else keycloak_state_directory }}
|
||||
|
||||
- name: Require Keycloak container runtime
|
||||
become: true
|
||||
become_user: "{{ keycloak_runtime_user }}"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ tmachine_container_runtime_name }}"
|
||||
- --version
|
||||
environment:
|
||||
HOME: "{{ keycloak_runtime_home }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Create Keycloak state directory
|
||||
become: true
|
||||
ansible.builtin.file:
|
||||
path: "{{ keycloak_runtime_state_directory }}"
|
||||
state: directory
|
||||
owner: "{{ keycloak_runtime_user }}"
|
||||
group: "{{ keycloak_runtime_user }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Install Keycloak realm fixture
|
||||
become: true
|
||||
ansible.builtin.copy:
|
||||
src: "{{ keycloak_realm_file }}"
|
||||
dest: "{{ keycloak_runtime_state_directory }}/realm.json"
|
||||
owner: "{{ keycloak_runtime_user }}"
|
||||
group: "{{ keycloak_runtime_user }}"
|
||||
mode: "0644"
|
||||
|
||||
- name: Remove an existing Keycloak fixture
|
||||
become: true
|
||||
become_user: "{{ keycloak_runtime_user }}"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ tmachine_container_runtime_name }}"
|
||||
- rm
|
||||
- --force
|
||||
- "{{ keycloak_container_name }}"
|
||||
environment:
|
||||
HOME: "{{ keycloak_runtime_home }}"
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Start Keycloak fixture
|
||||
become: true
|
||||
become_user: "{{ keycloak_runtime_user }}"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ tmachine_container_runtime_name }}"
|
||||
- run
|
||||
- --detach
|
||||
- --name
|
||||
- "{{ keycloak_container_name }}"
|
||||
- --publish
|
||||
- "127.0.0.1:{{ keycloak_port }}:8080"
|
||||
- --env
|
||||
- "KEYCLOAK_ADMIN={{ keycloak_admin_username }}"
|
||||
- --env
|
||||
- "KEYCLOAK_ADMIN_PASSWORD={{ keycloak_admin_password }}"
|
||||
- --volume
|
||||
- "{{ keycloak_runtime_state_directory }}/realm.json:/opt/keycloak/data/import/realm.json:ro,z"
|
||||
- "{{ keycloak_image }}"
|
||||
- start-dev
|
||||
- --import-realm
|
||||
environment:
|
||||
HOME: "{{ keycloak_runtime_home }}"
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for Keycloak discovery endpoint
|
||||
ansible.builtin.uri:
|
||||
url: "http://127.0.0.1:{{ keycloak_port }}/realms/{{ keycloak_realm }}/.well-known/openid-configuration"
|
||||
status_code: 200
|
||||
return_content: false
|
||||
register: keycloak_discovery
|
||||
retries: "{{ keycloak_health_retries }}"
|
||||
delay: "{{ keycloak_health_delay }}"
|
||||
until: keycloak_discovery.status | default(0) == 200
|
||||
failed_when: false
|
||||
|
||||
- name: Read Keycloak logs after failed health check
|
||||
become: true
|
||||
become_user: "{{ keycloak_runtime_user }}"
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ tmachine_container_runtime_name }}"
|
||||
- logs
|
||||
- --tail
|
||||
- "30"
|
||||
- "{{ keycloak_container_name }}"
|
||||
environment:
|
||||
HOME: "{{ keycloak_runtime_home }}"
|
||||
register: keycloak_logs
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
when: keycloak_discovery.status | default(0) != 200
|
||||
|
||||
- name: Show Keycloak logs after failed health check
|
||||
ansible.builtin.debug:
|
||||
var: keycloak_logs.stdout_lines
|
||||
when: keycloak_discovery.status | default(0) != 200
|
||||
|
||||
- name: Require Keycloak discovery endpoint
|
||||
ansible.builtin.assert:
|
||||
that: keycloak_discovery.status | default(0) == 200
|
||||
fail_msg: Keycloak did not become healthy within the configured timeout
|
||||
@@ -2,50 +2,21 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
- name: Resolve tmachine UID
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- id
|
||||
- -u
|
||||
- tmachine
|
||||
changed_when: false
|
||||
register: openshell_tmachine_uid
|
||||
- name: Detect tmachine container runtime
|
||||
ansible.builtin.include_role:
|
||||
name: tmachine_container_runtime
|
||||
|
||||
- name: Check Docker socket
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: /var/run/docker.sock
|
||||
register: openshell_docker_socket
|
||||
|
||||
- name: Check rootful Podman socket
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: /run/podman/podman.sock
|
||||
register: openshell_rootful_podman_socket
|
||||
|
||||
- name: Check rootless Podman socket
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: "/run/user/{{ openshell_tmachine_uid.stdout }}/podman/podman.sock"
|
||||
register: openshell_rootless_podman_socket
|
||||
|
||||
- name: Require exactly one container runtime socket
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(openshell_docker_socket.stat.exists | int)
|
||||
+ (openshell_rootful_podman_socket.stat.exists | int)
|
||||
+ (openshell_rootless_podman_socket.stat.exists | int) == 1
|
||||
fail_msg: Expected exactly one Docker or Podman socket
|
||||
|
||||
- name: Select container runtime
|
||||
- name: Set OpenShell gateway runtime values
|
||||
ansible.builtin.set_fact:
|
||||
openshell_gateway_driver: "{{ 'docker' if openshell_docker_socket.stat.exists else 'podman' }}"
|
||||
openshell_gateway_user: "{{ 'root' if openshell_rootful_podman_socket.stat.exists else 'tmachine' }}"
|
||||
openshell_gateway_home: "{{ '/root' if openshell_rootful_podman_socket.stat.exists else '/home/tmachine' }}"
|
||||
openshell_gateway_uid: "{{ '0' if openshell_rootful_podman_socket.stat.exists else openshell_tmachine_uid.stdout }}"
|
||||
openshell_gateway_bind_address: "{{ '127.0.0.1:17670' if openshell_docker_socket.stat.exists else '0.0.0.0:17670' }}"
|
||||
openshell_runtime_socket: >-
|
||||
{{ '/var/run/docker.sock' if openshell_docker_socket.stat.exists
|
||||
else '/run/podman/podman.sock' if openshell_rootful_podman_socket.stat.exists
|
||||
else '/run/user/' ~ openshell_tmachine_uid.stdout ~ '/podman/podman.sock' }}
|
||||
openshell_gateway_driver: "{{ tmachine_container_runtime_name }}"
|
||||
openshell_gateway_user: >-
|
||||
{{ 'root' if tmachine_container_runtime_name == 'podman'
|
||||
and not tmachine_container_runtime_is_rootless else 'tmachine' }}
|
||||
openshell_gateway_home: >-
|
||||
{{ '/root' if tmachine_container_runtime_name == 'podman'
|
||||
and not tmachine_container_runtime_is_rootless else '/home/tmachine' }}
|
||||
openshell_gateway_uid: >-
|
||||
{{ '0' if tmachine_container_runtime_name == 'podman'
|
||||
and not tmachine_container_runtime_is_rootless else tmachine_container_runtime_tmachine_uid.stdout }}
|
||||
openshell_gateway_bind_address: "{{ '127.0.0.1:17670' if tmachine_container_runtime_name == 'docker' else '0.0.0.0:17670' }}"
|
||||
openshell_runtime_socket: "{{ tmachine_container_runtime_socket }}"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
---
|
||||
- name: Resolve tmachine UID
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- id
|
||||
- -u
|
||||
- tmachine
|
||||
changed_when: false
|
||||
register: tmachine_container_runtime_tmachine_uid
|
||||
|
||||
- name: Check Docker socket
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: /var/run/docker.sock
|
||||
register: tmachine_container_runtime_docker_socket
|
||||
|
||||
- name: Check rootful Podman socket
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: /run/podman/podman.sock
|
||||
register: tmachine_container_runtime_rootful_podman_socket
|
||||
|
||||
- name: Check rootless Podman socket
|
||||
become: true
|
||||
ansible.builtin.stat:
|
||||
path: "/run/user/{{ tmachine_container_runtime_tmachine_uid.stdout }}/podman/podman.sock"
|
||||
register: tmachine_container_runtime_rootless_podman_socket
|
||||
|
||||
- name: Require exactly one container runtime socket
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- >-
|
||||
(tmachine_container_runtime_docker_socket.stat.exists | int)
|
||||
+ (tmachine_container_runtime_rootful_podman_socket.stat.exists | int)
|
||||
+ (tmachine_container_runtime_rootless_podman_socket.stat.exists | int) == 1
|
||||
fail_msg: Expected exactly one Docker or Podman socket
|
||||
|
||||
- name: Select tmachine container runtime
|
||||
ansible.builtin.set_fact:
|
||||
tmachine_container_runtime_name: >-
|
||||
{{ 'docker' if tmachine_container_runtime_docker_socket.stat.exists else 'podman' }}
|
||||
tmachine_container_runtime_is_rootless: >-
|
||||
{{ tmachine_container_runtime_rootless_podman_socket.stat.exists }}
|
||||
tmachine_container_runtime_socket: >-
|
||||
{{ '/var/run/docker.sock' if tmachine_container_runtime_docker_socket.stat.exists
|
||||
else '/run/podman/podman.sock' if tmachine_container_runtime_rootful_podman_socket.stat.exists
|
||||
else '/run/user/' ~ tmachine_container_runtime_tmachine_uid.stdout ~ '/podman/podman.sock' }}
|
||||
+23
-3
@@ -84,9 +84,17 @@ let
|
||||
target = muslToolchain.target;
|
||||
output = "artifacts/test-archives/${muslToolchain.target}/openshell-conformance-tests.tar";
|
||||
};
|
||||
providerRefreshKeycloakArchive = mkTestArchive {
|
||||
name = "provider-refresh-keycloak";
|
||||
workspacePath = "tests/suites/features";
|
||||
manifestPath = "tests/suites/features/Cargo.toml";
|
||||
package = "openshell-test-feature-provider-refresh-keycloak";
|
||||
target = muslToolchain.target;
|
||||
output = "artifacts/test-archives/${muslToolchain.target}/provider-refresh-keycloak-tests.tar";
|
||||
};
|
||||
in
|
||||
rec {
|
||||
inherit conformanceCliArchive;
|
||||
inherit conformanceCliArchive providerRefreshKeycloakArchive;
|
||||
|
||||
binaries = pkgs.writeShellApplication {
|
||||
name = "build-artifacts-binaries";
|
||||
@@ -126,6 +134,18 @@ rec {
|
||||
'';
|
||||
};
|
||||
|
||||
testArchives = pkgs.writeShellApplication {
|
||||
name = "build-artifacts-test-archives";
|
||||
runtimeInputs = [
|
||||
conformanceCliArchive
|
||||
providerRefreshKeycloakArchive
|
||||
];
|
||||
text = ''
|
||||
build-openshell-conformance-test-archive
|
||||
build-provider-refresh-keycloak-test-archive
|
||||
'';
|
||||
};
|
||||
|
||||
images = pkgs.writeShellApplication {
|
||||
name = "build-artifacts-images";
|
||||
runtimeInputs = [
|
||||
@@ -204,13 +224,13 @@ rec {
|
||||
name = "build-artifacts";
|
||||
runtimeInputs = [
|
||||
binaries
|
||||
conformanceCliArchive
|
||||
testArchives
|
||||
images
|
||||
helm
|
||||
];
|
||||
text = ''
|
||||
build-artifacts-binaries
|
||||
build-openshell-conformance-test-archive
|
||||
build-artifacts-test-archives
|
||||
build-artifacts-images
|
||||
build-artifacts-helm
|
||||
'';
|
||||
|
||||
+27
-39
@@ -31,7 +31,7 @@ let
|
||||
}
|
||||
];
|
||||
|
||||
scenarios = [
|
||||
environments = [
|
||||
{
|
||||
name = "ubuntu-docker-rootful";
|
||||
machine = "ubuntu";
|
||||
@@ -42,19 +42,6 @@ let
|
||||
"ansible/playbooks/docker.yaml"
|
||||
];
|
||||
};
|
||||
install = {
|
||||
use_galaxy = false;
|
||||
playbooks = [
|
||||
"ansible/playbooks/openshell.yaml"
|
||||
"ansible/playbooks/gateway.yaml"
|
||||
];
|
||||
inputs = {
|
||||
openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell";
|
||||
openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway";
|
||||
openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar";
|
||||
openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar";
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "fedora-podman-rootful";
|
||||
@@ -63,22 +50,10 @@ let
|
||||
use_galaxy = false;
|
||||
playbooks = [
|
||||
"ansible/playbooks/nextest.yaml"
|
||||
"ansible/playbooks/selinux.yaml"
|
||||
"ansible/playbooks/podman-rootful.yaml"
|
||||
];
|
||||
};
|
||||
install = {
|
||||
use_galaxy = false;
|
||||
playbooks = [
|
||||
"ansible/playbooks/openshell.yaml"
|
||||
"ansible/playbooks/gateway.yaml"
|
||||
];
|
||||
inputs = {
|
||||
openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell";
|
||||
openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway";
|
||||
openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar";
|
||||
openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar";
|
||||
};
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "fedora-podman-rootless";
|
||||
@@ -87,21 +62,26 @@ let
|
||||
use_galaxy = false;
|
||||
playbooks = [
|
||||
"ansible/playbooks/nextest.yaml"
|
||||
"ansible/playbooks/selinux.yaml"
|
||||
"ansible/playbooks/podman-rootless.yaml"
|
||||
];
|
||||
};
|
||||
install = {
|
||||
use_galaxy = false;
|
||||
playbooks = [
|
||||
"ansible/playbooks/openshell.yaml"
|
||||
"ansible/playbooks/gateway.yaml"
|
||||
];
|
||||
inputs = {
|
||||
openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell";
|
||||
openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway";
|
||||
openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar";
|
||||
openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar";
|
||||
};
|
||||
}
|
||||
];
|
||||
|
||||
installers = [
|
||||
{
|
||||
name = "binaries";
|
||||
use_galaxy = false;
|
||||
playbooks = [
|
||||
"ansible/playbooks/openshell.yaml"
|
||||
"ansible/playbooks/gateway.yaml"
|
||||
];
|
||||
inputs = {
|
||||
openshell_cli_binary = "../artifacts/binaries/${muslTarget}/openshell";
|
||||
openshell_gateway_binary = "../artifacts/binaries/${gnuTarget}/openshell-gateway";
|
||||
openshell_supervisor_image = "../artifacts/images/openshell-supervisor-tmachine.tar";
|
||||
openshell_sandbox_image = "../artifacts/images/openshell-sandbox-tmachine.tar";
|
||||
};
|
||||
}
|
||||
];
|
||||
@@ -114,6 +94,14 @@ let
|
||||
openshell_conformance_test_bundle = "../artifacts/test-archives/${muslTarget}/openshell-conformance-tests.tar";
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "provider-refresh";
|
||||
playbooks = [ "ansible/playbooks/features/provider-refresh/keycloak.yaml" ];
|
||||
inputs = {
|
||||
keycloak_realm_file = "../scripts/keycloak-realm.json";
|
||||
provider_refresh_keycloak_test_bundle = "../artifacts/test-archives/${muslTarget}/provider-refresh-keycloak-tests.tar";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
Generated
+1868
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["provider-refresh/keycloak"]
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "openshell-test-feature-provider-refresh-keycloak"
|
||||
version = "0.0.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
openshell-conformance = { path = "../../../../../crates/openshell-conformance" }
|
||||
openshell-e2e = { path = "../../../../../e2e/rust" }
|
||||
serde_json = "1"
|
||||
tempfile = "3"
|
||||
tokio = { version = "1.43", features = ["macros", "process", "io-util", "rt"] }
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
#![cfg(target_os = "linux")]
|
||||
|
||||
//! End-to-end coverage for provider OAuth refresh recovery against Keycloak.
|
||||
//! Provider OAuth refresh recovery against Keycloak.
|
||||
//!
|
||||
//! OpenShell itself uses the local gateway's mTLS authentication. Keycloak is
|
||||
//! only the provider token issuer: the test refreshes a valid grant, revokes
|
||||
@@ -10,7 +10,8 @@ use serde::Deserialize;
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Config {
|
||||
pub machines: Vec<Machine>,
|
||||
pub scenarios: Vec<Scenario>,
|
||||
pub environments: Vec<Environment>,
|
||||
pub installers: Vec<Installer>,
|
||||
pub testsuites: Vec<Testsuite>,
|
||||
}
|
||||
|
||||
@@ -21,11 +22,10 @@ pub struct Machine {
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Scenario {
|
||||
pub struct Environment {
|
||||
pub name: String,
|
||||
pub machine: String,
|
||||
pub setup: Setup,
|
||||
pub install: Install,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
@@ -35,7 +35,8 @@ pub struct Setup {
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Install {
|
||||
pub struct Installer {
|
||||
pub name: String,
|
||||
pub use_galaxy: bool,
|
||||
pub playbooks: Vec<PathBuf>,
|
||||
pub inputs: BTreeMap<String, PathBuf>,
|
||||
|
||||
+46
-22
@@ -5,7 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use config::{Config, Machine, Scenario};
|
||||
use config::{Config, Environment, Installer, Machine};
|
||||
|
||||
mod ansible;
|
||||
mod config;
|
||||
@@ -23,9 +23,18 @@ struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
Setup { scenario: String },
|
||||
Install { scenario: String },
|
||||
Test { scenario: String, testsuite: String },
|
||||
Setup {
|
||||
environment: String,
|
||||
},
|
||||
Install {
|
||||
environment: String,
|
||||
installer: String,
|
||||
},
|
||||
Test {
|
||||
environment: String,
|
||||
installer: String,
|
||||
testsuite: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
@@ -34,49 +43,64 @@ async fn main() -> Result<()> {
|
||||
let config = Config::load(&cli.config)?;
|
||||
|
||||
match cli.command {
|
||||
Command::Setup { scenario } => {
|
||||
let (machine, scenario) = find_scenario(&config, &scenario)?;
|
||||
qemu::setup(&machine, &scenario).await?;
|
||||
Command::Setup { environment } => {
|
||||
let (machine, environment) = find_environment(&config, &environment)?;
|
||||
qemu::setup(&machine, &environment).await?;
|
||||
}
|
||||
Command::Install { scenario } => {
|
||||
let (machine, scenario) = find_scenario(&config, &scenario)?;
|
||||
qemu::install(&machine, &scenario).await?;
|
||||
Command::Install {
|
||||
environment,
|
||||
installer,
|
||||
} => {
|
||||
let (machine, environment) = find_environment(&config, &environment)?;
|
||||
let installer = find_installer(&config, &installer)?;
|
||||
qemu::install(&machine, &environment, &installer).await?;
|
||||
}
|
||||
Command::Test {
|
||||
scenario,
|
||||
environment,
|
||||
installer,
|
||||
testsuite,
|
||||
} => {
|
||||
let (machine, scenario) = find_scenario(&config, &scenario)?;
|
||||
let (machine, environment) = find_environment(&config, &environment)?;
|
||||
let installer = find_installer(&config, &installer)?;
|
||||
let testsuite = config
|
||||
.testsuites
|
||||
.iter()
|
||||
.find(|candidate| candidate.name == testsuite)
|
||||
.with_context(|| format!("testsuite {testsuite:?} is not defined"))?;
|
||||
qemu::test(&machine, &scenario, testsuite).await?;
|
||||
qemu::test(&machine, &environment, &installer, testsuite).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_scenario(config: &Config, name: &str) -> Result<(Machine, Scenario)> {
|
||||
let scenario = config
|
||||
.scenarios
|
||||
fn find_installer(config: &Config, name: &str) -> Result<Installer> {
|
||||
config
|
||||
.installers
|
||||
.iter()
|
||||
.find(|scenario| scenario.name == name)
|
||||
.with_context(|| format!("scenario {name:?} is not defined"))?
|
||||
.find(|installer| installer.name == name)
|
||||
.with_context(|| format!("installer {name:?} is not defined"))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn find_environment(config: &Config, name: &str) -> Result<(Machine, Environment)> {
|
||||
let environment = config
|
||||
.environments
|
||||
.iter()
|
||||
.find(|environment| environment.name == name)
|
||||
.with_context(|| format!("environment {name:?} is not defined"))?
|
||||
.clone();
|
||||
let machine = config
|
||||
.machines
|
||||
.iter()
|
||||
.find(|machine| machine.name == scenario.machine)
|
||||
.find(|machine| machine.name == environment.machine)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"machine {:?} referenced by scenario {:?} is not defined",
|
||||
scenario.machine, scenario.name
|
||||
"machine {:?} referenced by environment {:?} is not defined",
|
||||
environment.machine, environment.name
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
|
||||
Ok((machine, scenario))
|
||||
Ok((machine, environment))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{Context, Result};
|
||||
use blake3::Hasher;
|
||||
|
||||
use crate::config::{Machine, Scenario};
|
||||
use crate::config::{Environment, Installer, Machine};
|
||||
|
||||
use super::ansible_hash::hash_sources;
|
||||
use super::layer::{cached_layer, hash_file, hash_files, hash_inputs};
|
||||
@@ -14,31 +14,34 @@ use super::setup::setup;
|
||||
|
||||
const INSTALL_CACHE_VERSION: &[u8] = b"tmachine-install-blake3-v1";
|
||||
|
||||
pub async fn install(machine: &Machine, scenario: &Scenario) -> Result<PathBuf> {
|
||||
let setup_disk = setup(machine, scenario).await?;
|
||||
if scenario.install.playbooks.is_empty() && scenario.install.inputs.is_empty() {
|
||||
pub async fn install(
|
||||
machine: &Machine,
|
||||
environment: &Environment,
|
||||
installer: &Installer,
|
||||
) -> Result<PathBuf> {
|
||||
let setup_disk = setup(machine, environment).await?;
|
||||
if installer.playbooks.is_empty() && installer.inputs.is_empty() {
|
||||
return Ok(setup_disk);
|
||||
}
|
||||
|
||||
let hash = install_hash(&setup_disk, scenario)?;
|
||||
let hash = install_hash(&setup_disk, installer)?;
|
||||
cached_layer(
|
||||
&setup_disk,
|
||||
&hash,
|
||||
scenario.install.use_galaxy,
|
||||
&scenario.install.playbooks,
|
||||
&scenario.install.inputs,
|
||||
installer.use_galaxy,
|
||||
&installer.playbooks,
|
||||
&installer.inputs,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn install_hash(setup_disk: &Path, scenario: &Scenario) -> Result<String> {
|
||||
fn install_hash(setup_disk: &Path, installer: &Installer) -> Result<String> {
|
||||
let mut hasher = Hasher::new();
|
||||
hasher.update(INSTALL_CACHE_VERSION);
|
||||
hash_file(&mut hasher, setup_disk).context("failed to hash setup disk")?;
|
||||
hasher.update(&[u8::from(scenario.install.use_galaxy)]);
|
||||
hasher.update(&[u8::from(installer.use_galaxy)]);
|
||||
hash_sources(&mut hasher).context("failed to hash Ansible sources")?;
|
||||
hash_files(&mut hasher, &scenario.install.playbooks)
|
||||
.context("failed to hash install playbooks")?;
|
||||
hash_inputs(&mut hasher, &scenario.install.inputs)?;
|
||||
hash_files(&mut hasher, &installer.playbooks).context("failed to hash install playbooks")?;
|
||||
hash_inputs(&mut hasher, &installer.inputs)?;
|
||||
Ok(hasher.finalize().to_hex().to_string())
|
||||
}
|
||||
|
||||
@@ -7,36 +7,37 @@ use std::path::PathBuf;
|
||||
use anyhow::{Context, Result};
|
||||
use blake3::Hasher;
|
||||
|
||||
use crate::config::{Machine, Scenario};
|
||||
use crate::config::{Environment, Machine};
|
||||
|
||||
use super::ansible_hash::hash_sources;
|
||||
use super::layer::{cached_layer, hash_file, hash_files};
|
||||
|
||||
const SETUP_CACHE_VERSION: &[u8] = b"tmachine-disk-blake3-v1";
|
||||
|
||||
pub async fn setup(machine: &Machine, scenario: &Scenario) -> Result<PathBuf> {
|
||||
if scenario.setup.playbooks.is_empty() {
|
||||
pub async fn setup(machine: &Machine, environment: &Environment) -> Result<PathBuf> {
|
||||
if environment.setup.playbooks.is_empty() {
|
||||
return Ok(machine.base_image.clone());
|
||||
}
|
||||
|
||||
let hash = setup_hash(machine, scenario)?;
|
||||
let hash = setup_hash(machine, environment)?;
|
||||
cached_layer(
|
||||
&machine.base_image,
|
||||
&hash,
|
||||
scenario.setup.use_galaxy,
|
||||
&scenario.setup.playbooks,
|
||||
environment.setup.use_galaxy,
|
||||
&environment.setup.playbooks,
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn setup_hash(machine: &Machine, scenario: &Scenario) -> Result<String> {
|
||||
fn setup_hash(machine: &Machine, environment: &Environment) -> Result<String> {
|
||||
let mut hasher = Hasher::new();
|
||||
hasher.update(SETUP_CACHE_VERSION);
|
||||
hash_file(&mut hasher, &machine.base_image)
|
||||
.with_context(|| format!("failed to hash base image for machine {:?}", machine.name))?;
|
||||
hasher.update(&[u8::from(scenario.setup.use_galaxy)]);
|
||||
hasher.update(&[u8::from(environment.setup.use_galaxy)]);
|
||||
hash_sources(&mut hasher).context("failed to hash Ansible sources")?;
|
||||
hash_files(&mut hasher, &scenario.setup.playbooks).context("failed to hash setup playbooks")?;
|
||||
hash_files(&mut hasher, &environment.setup.playbooks)
|
||||
.context("failed to hash setup playbooks")?;
|
||||
Ok(hasher.finalize().to_hex().to_string())
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::config::{Machine, Scenario, Testsuite};
|
||||
use crate::config::{Environment, Installer, Machine, Testsuite};
|
||||
use anyhow::Result;
|
||||
|
||||
use super::img::QemuImage;
|
||||
@@ -11,8 +11,13 @@ use super::install::install;
|
||||
use super::layer::run_playbooks;
|
||||
use super::vm::QemuVm;
|
||||
|
||||
pub async fn test(machine: &Machine, scenario: &Scenario, testsuite: &Testsuite) -> Result<()> {
|
||||
let install_disk = install(machine, scenario).await?;
|
||||
pub async fn test(
|
||||
machine: &Machine,
|
||||
environment: &Environment,
|
||||
installer: &Installer,
|
||||
testsuite: &Testsuite,
|
||||
) -> Result<()> {
|
||||
let install_disk = install(machine, environment, installer).await?;
|
||||
let test_dir = tempdir().unwrap();
|
||||
let test_disk = test_dir.path().join("test.qcow2");
|
||||
let image = QemuImage::create(&install_disk, test_disk).await;
|
||||
|
||||
Reference in New Issue
Block a user