Spaces:
Running
Running
Delete ci
Browse files- ci/README.md +0 -111
- ci/circleci.redis-chaos.reference.yml +0 -68
- ci/github-actions.redis-chaos.reference.yml +0 -81
- ci/redis-chaos.Dockerfile +0 -19
- ci/redis_chaos_attestation.py +0 -189
- ci/run_redis_chaos.py +0 -208
ci/README.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
# Provider-artifact Redis chaos CI
|
| 2 |
-
|
| 3 |
-
Run 147 makes the Run 146 live Redis gate reproducible without changing the
|
| 4 |
-
provider-artifact public API.
|
| 5 |
-
|
| 6 |
-
The portable entrypoint is:
|
| 7 |
-
|
| 8 |
-
```bash
|
| 9 |
-
python _hf_spaces_proxy/ci/run_redis_chaos.py --mode all
|
| 10 |
-
```
|
| 11 |
-
|
| 12 |
-
It is intentionally **fail closed**: Redis 7/8 `redis-server`, `pytest`, and the
|
| 13 |
-
pinned `redis-py` cluster client must exist. The entrypoint sets the mandatory
|
| 14 |
-
no-skip flags for the live tests itself.
|
| 15 |
-
|
| 16 |
-
Modes:
|
| 17 |
-
|
| 18 |
-
- `standalone` — executes the Run 146 real-Lua/restart tests plus the Run 147
|
| 19 |
-
standalone binary smoke.
|
| 20 |
-
- `cluster` — bootstraps six real Redis nodes (3 primaries + 3 replicas), assigns
|
| 21 |
-
all 16,384 slots without `redis-cli`, races two OS processes for one lifecycle
|
| 22 |
-
reservation, kills the primary that owns the `{provider-artifact}` slot, waits
|
| 23 |
-
for replica promotion, and proves terminal non-replay plus new lifecycle work.
|
| 24 |
-
- `all` — runs both gates.
|
| 25 |
-
|
| 26 |
-
For exact Redis-major CI, build the included `redis-chaos.Dockerfile` from the
|
| 27 |
-
repository root. It copies `redis-server` from the selected official Redis
|
| 28 |
-
bookworm image into a Python image, installs the proxy's pinned requirements,
|
| 29 |
-
and runs the portable entrypoint. Reference GitHub Actions and CircleCI job
|
| 30 |
-
fragments are included beside this file for Redis 7.4.11 and 8.2.9.
|
| 31 |
-
|
| 32 |
-
The container examples are read-only and grant Redis writable storage only via
|
| 33 |
-
bounded `/tmp` and `/var/tmp` tmpfs mounts. They drop Linux capabilities and set
|
| 34 |
-
`no-new-privileges`.
|
| 35 |
-
|
| 36 |
-
The chaos gate never needs production Redis URLs, credentials, provider tokens,
|
| 37 |
-
prompts, generated media, or ZIP contents. Every Redis process binds only to
|
| 38 |
-
loopback inside the disposable CI container.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
## Run 148 release attestation output
|
| 42 |
-
|
| 43 |
-
For release CI, invoke the same gate with a destination outside the source tree:
|
| 44 |
-
|
| 45 |
-
```bash
|
| 46 |
-
python _hf_spaces_proxy/ci/run_redis_chaos.py --mode all \
|
| 47 |
-
--attestation-dir /secure/evidence \
|
| 48 |
-
--source-revision "$SOURCE_REVISION" \
|
| 49 |
-
--redis-image 'redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d'
|
| 50 |
-
```
|
| 51 |
-
|
| 52 |
-
Run Redis 7 and Redis 8 separately so release evidence contains all four required
|
| 53 |
-
major/mode combinations. `--redis-image` must contain both the reviewed version tag
|
| 54 |
-
and an immutable SHA-256 index digest. Attestation output inside the source tree is
|
| 55 |
-
rejected to avoid self-referential source subjects.
|
| 56 |
-
|
| 57 |
-
The emitted JSON is a canonical **subject**, not a self-authenticating signature. The
|
| 58 |
-
parent CI must cryptographically sign/attest it, verify the signer identity, and only
|
| 59 |
-
then create a sanitized signature-verification record. GitHub Actions reference jobs
|
| 60 |
-
show this with the SHA-pinned `actions/attest` action plus `gh attestation verify`;
|
| 61 |
-
the verification command additionally pins the exact `GITHUB_SHA` and `GITHUB_REF`,
|
| 62 |
-
derives the signing-workflow identity from `GITHUB_WORKFLOW_REF`, pins its signer
|
| 63 |
-
digest, and rejects self-hosted-runner attestations. CircleCI environments should use
|
| 64 |
-
the organization's approved Sigstore/KMS/HSM flow,
|
| 65 |
-
then call `redis_chaos_attestation.py signature-record` only after that verifier exits
|
| 66 |
-
successfully.
|
| 67 |
-
|
| 68 |
-
## Run 149 promotion handoff
|
| 69 |
-
|
| 70 |
-
Redis chaos and schema-v2 evidence are inputs to the transactional promotion layer;
|
| 71 |
-
they are not permission to publish arbitrary rebuilt bytes. Parent CI should run
|
| 72 |
-
`security/promote_release.py prepare`, sign/verify the emitted canonical statement
|
| 73 |
-
with the approved release trust root, create a sanitized signature record bound to the
|
| 74 |
-
actual verifier output, and run `promote_release.py finalize`. Upload/publish only the
|
| 75 |
-
exact receipt-listed objects from the final promotion directory.
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
## Run 151 post-publication handoff
|
| 79 |
-
|
| 80 |
-
After Run 150 publishes the receipt-authorized objects, use
|
| 81 |
-
`security/finalize_publication.py prepare` with credentials that are distinct from the
|
| 82 |
-
publisher and read-only for the release target. Cryptographically sign/attest the
|
| 83 |
-
resulting `publication-attestation.json`, verify the exact expected signer identity with
|
| 84 |
-
the organization's release trust root, and create the sanitized signature-verification
|
| 85 |
-
record only after that verifier succeeds.
|
| 86 |
-
|
| 87 |
-
Then run `finalize_publication.py finalize` with the same independent verifier identity
|
| 88 |
-
and a create-only final-record binder. The finalizer re-reads every release object after
|
| 89 |
-
signature verification and permits the binder to attach only the fixed deterministic
|
| 90 |
-
`release-publication-record.json` to the original target. GitHub deployments should use
|
| 91 |
-
a read-only release-asset verifier identity distinct from the upload identity; object
|
| 92 |
-
stores and registries should use equivalent separate read-only principals. Do not place
|
| 93 |
-
signing keys, publisher tokens, verifier tokens, signed URLs, or raw credential-bearing
|
| 94 |
-
provider output in the generated evidence.
|
| 95 |
-
|
| 96 |
-
## Run 152 transparency witness handoff
|
| 97 |
-
|
| 98 |
-
After Run 151 creates and binds `release-publication-record.json`, submit those exact
|
| 99 |
-
bytes to the approved append-only transparency service using
|
| 100 |
-
`security/witness_publication.py`. CI must provide a previously trusted checkpoint for
|
| 101 |
-
the same log, a log submitter identity, a separate read-only primary verifier, and the
|
| 102 |
-
configured witness quorum. The default policy requires at least two distinct witness
|
| 103 |
-
identities operated by at least two distinct operators.
|
| 104 |
-
|
| 105 |
-
The primary verifier and every witness must independently verify the log checkpoint
|
| 106 |
-
signature, exact entry inclusion, integrated entry, and consistency from the pinned
|
| 107 |
-
previous checkpoint. Do not satisfy quorum with aliases sharing one credential or one
|
| 108 |
-
mutable trust root. After quorum, the release-evidence anchor may create-only attach the
|
| 109 |
-
deterministic witnessed record and must read it back before success. Persist the emitted canonical `accepted-transparency-checkpoint.json` for the next
|
| 110 |
-
release only after this gate succeeds; pass those exact bytes back as the next
|
| 111 |
-
`--previous-checkpoint`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ci/circleci.redis-chaos.reference.yml
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
# Reference CircleCI job fragment. Merge under the parent repository's jobs/workflows keys.
|
| 2 |
-
# The jobs emit canonical Run 148 attestations. Before production promotion, an
|
| 3 |
-
# organization-approved cryptographic signer/verifier MUST create the paired
|
| 4 |
-
# *.signature-verification.json records consumed by verify_release_evidence.py.
|
| 5 |
-
version: 2.1
|
| 6 |
-
|
| 7 |
-
jobs:
|
| 8 |
-
provider_artifact_redis_chaos_7:
|
| 9 |
-
machine:
|
| 10 |
-
image: ubuntu-2404:current
|
| 11 |
-
resource_class: medium
|
| 12 |
-
steps:
|
| 13 |
-
- checkout
|
| 14 |
-
- run:
|
| 15 |
-
name: Redis 7.4.11 standalone + cluster live-Lua chaos gate
|
| 16 |
-
command: |
|
| 17 |
-
REDIS_IMAGE='redis:7.4.11-bookworm@sha256:71da9275c5f3fcb97d0fa0c8c5b36cc995327265420f17a04bfd544f458059f7'
|
| 18 |
-
mkdir -p /tmp/redis-chaos-evidence
|
| 19 |
-
docker build \
|
| 20 |
-
-f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
|
| 21 |
-
--build-arg REDIS_IMAGE="$REDIS_IMAGE" \
|
| 22 |
-
-t scikitplot-redis-chaos:7 .
|
| 23 |
-
docker run --rm --read-only \
|
| 24 |
-
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
|
| 25 |
-
--tmpfs /var/tmp:rw,nosuid,nodev,noexec,size=256m \
|
| 26 |
-
--mount type=bind,src=/tmp/redis-chaos-evidence,dst=/evidence \
|
| 27 |
-
--cap-drop ALL --security-opt no-new-privileges \
|
| 28 |
-
scikitplot-redis-chaos:7 --mode all \
|
| 29 |
-
--attestation-dir /evidence \
|
| 30 |
-
--source-revision "$CIRCLE_SHA1" \
|
| 31 |
-
--redis-image "$REDIS_IMAGE"
|
| 32 |
-
- store_artifacts:
|
| 33 |
-
path: /tmp/redis-chaos-evidence
|
| 34 |
-
destination: redis-chaos-evidence-7
|
| 35 |
-
|
| 36 |
-
provider_artifact_redis_chaos_8:
|
| 37 |
-
machine:
|
| 38 |
-
image: ubuntu-2404:current
|
| 39 |
-
resource_class: medium
|
| 40 |
-
steps:
|
| 41 |
-
- checkout
|
| 42 |
-
- run:
|
| 43 |
-
name: Redis 8.2.9 standalone + cluster live-Lua chaos gate
|
| 44 |
-
command: |
|
| 45 |
-
REDIS_IMAGE='redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d'
|
| 46 |
-
mkdir -p /tmp/redis-chaos-evidence
|
| 47 |
-
docker build \
|
| 48 |
-
-f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
|
| 49 |
-
--build-arg REDIS_IMAGE="$REDIS_IMAGE" \
|
| 50 |
-
-t scikitplot-redis-chaos:8 .
|
| 51 |
-
docker run --rm --read-only \
|
| 52 |
-
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
|
| 53 |
-
--tmpfs /var/tmp:rw,nosuid,nodev,noexec,size=256m \
|
| 54 |
-
--mount type=bind,src=/tmp/redis-chaos-evidence,dst=/evidence \
|
| 55 |
-
--cap-drop ALL --security-opt no-new-privileges \
|
| 56 |
-
scikitplot-redis-chaos:8 --mode all \
|
| 57 |
-
--attestation-dir /evidence \
|
| 58 |
-
--source-revision "$CIRCLE_SHA1" \
|
| 59 |
-
--redis-image "$REDIS_IMAGE"
|
| 60 |
-
- store_artifacts:
|
| 61 |
-
path: /tmp/redis-chaos-evidence
|
| 62 |
-
destination: redis-chaos-evidence-8
|
| 63 |
-
|
| 64 |
-
workflows:
|
| 65 |
-
redis_chaos_reference:
|
| 66 |
-
jobs:
|
| 67 |
-
- provider_artifact_redis_chaos_7
|
| 68 |
-
- provider_artifact_redis_chaos_8
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ci/github-actions.redis-chaos.reference.yml
DELETED
|
@@ -1,81 +0,0 @@
|
|
| 1 |
-
# Reference job. Merge into the parent repository workflow rather than running this file directly.
|
| 2 |
-
name: redis-chaos-reference
|
| 3 |
-
on:
|
| 4 |
-
workflow_dispatch:
|
| 5 |
-
|
| 6 |
-
permissions:
|
| 7 |
-
contents: read
|
| 8 |
-
id-token: write
|
| 9 |
-
attestations: write
|
| 10 |
-
|
| 11 |
-
jobs:
|
| 12 |
-
provider-artifact-redis-chaos:
|
| 13 |
-
runs-on: ubuntu-24.04
|
| 14 |
-
timeout-minutes: 25
|
| 15 |
-
strategy:
|
| 16 |
-
fail-fast: false
|
| 17 |
-
matrix:
|
| 18 |
-
include:
|
| 19 |
-
- redis_major: 7
|
| 20 |
-
redis_image: redis:7.4.11-bookworm@sha256:71da9275c5f3fcb97d0fa0c8c5b36cc995327265420f17a04bfd544f458059f7
|
| 21 |
-
- redis_major: 8
|
| 22 |
-
redis_image: redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d
|
| 23 |
-
steps:
|
| 24 |
-
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
| 25 |
-
with:
|
| 26 |
-
persist-credentials: false
|
| 27 |
-
- name: Build isolated digest-pinned Redis chaos image
|
| 28 |
-
run: |
|
| 29 |
-
docker build \
|
| 30 |
-
-f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
|
| 31 |
-
--build-arg REDIS_IMAGE='${{ matrix.redis_image }}' \
|
| 32 |
-
-t scikitplot-redis-chaos:ci .
|
| 33 |
-
- name: Run standalone + cluster live-Lua chaos gate and emit canonical evidence
|
| 34 |
-
run: |
|
| 35 |
-
mkdir -p "$RUNNER_TEMP/redis-chaos-evidence"
|
| 36 |
-
docker run --rm \
|
| 37 |
-
--read-only \
|
| 38 |
-
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
|
| 39 |
-
--tmpfs /var/tmp:rw,nosuid,nodev,noexec,size=256m \
|
| 40 |
-
--mount "type=bind,src=$RUNNER_TEMP/redis-chaos-evidence,dst=/evidence" \
|
| 41 |
-
--cap-drop ALL \
|
| 42 |
-
--security-opt no-new-privileges \
|
| 43 |
-
scikitplot-redis-chaos:ci \
|
| 44 |
-
--mode all \
|
| 45 |
-
--attestation-dir /evidence \
|
| 46 |
-
--source-revision '${{ github.sha }}' \
|
| 47 |
-
--redis-image '${{ matrix.redis_image }}'
|
| 48 |
-
- name: Cryptographically attest canonical Redis chaos evidence
|
| 49 |
-
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
| 50 |
-
with:
|
| 51 |
-
subject-path: '${{ runner.temp }}/redis-chaos-evidence/*.json'
|
| 52 |
-
- name: Verify signatures and emit sanitized release records
|
| 53 |
-
env:
|
| 54 |
-
GH_TOKEN: ${{ github.token }}
|
| 55 |
-
run: |
|
| 56 |
-
set -eu
|
| 57 |
-
signer_workflow="${GITHUB_WORKFLOW_REF%@*}"
|
| 58 |
-
test -n "$signer_workflow"
|
| 59 |
-
for attestation in "$RUNNER_TEMP"/redis-chaos-evidence/redis-chaos-redis${{ matrix.redis_major }}-*.json; do
|
| 60 |
-
gh attestation verify "$attestation" \
|
| 61 |
-
-R "$GITHUB_REPOSITORY" \
|
| 62 |
-
--source-digest "$GITHUB_SHA" \
|
| 63 |
-
--source-ref "$GITHUB_REF" \
|
| 64 |
-
--signer-workflow "$signer_workflow" \
|
| 65 |
-
--signer-digest "$GITHUB_SHA" \
|
| 66 |
-
--deny-self-hosted-runners \
|
| 67 |
-
--format json >/dev/null
|
| 68 |
-
stem="${attestation%.json}"
|
| 69 |
-
python scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis_chaos_attestation.py \
|
| 70 |
-
signature-record \
|
| 71 |
-
--attestation "$attestation" \
|
| 72 |
-
--output "${stem}.signature-verification.json" \
|
| 73 |
-
--source-revision '${{ github.sha }}'
|
| 74 |
-
done
|
| 75 |
-
- name: Upload signed Redis chaos release evidence
|
| 76 |
-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
| 77 |
-
with:
|
| 78 |
-
name: redis-chaos-evidence-${{ matrix.redis_major }}
|
| 79 |
-
path: '${{ runner.temp }}/redis-chaos-evidence/'
|
| 80 |
-
if-no-files-found: error
|
| 81 |
-
retention-days: 7
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ci/redis-chaos.Dockerfile
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
# Reproducible Run 147 Redis chaos image.
|
| 2 |
-
# Build from the repository root so the complete scikitplot package is copied.
|
| 3 |
-
# Example:
|
| 4 |
-
# docker build -f scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/redis-chaos.Dockerfile \
|
| 5 |
-
# --build-arg REDIS_IMAGE=redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d -t sphinx-ai-redis-chaos:8 .
|
| 6 |
-
ARG REDIS_IMAGE=redis:8.2.9-bookworm@sha256:7d1e4ce8b9395088377ab382d1f6cfdbd13b3690795198a0399ab8d683064d6d
|
| 7 |
-
FROM ${REDIS_IMAGE} AS redis_runtime
|
| 8 |
-
|
| 9 |
-
FROM python:3.13-slim-bookworm
|
| 10 |
-
COPY --from=redis_runtime /usr/local/bin/redis-server /usr/local/bin/redis-server
|
| 11 |
-
WORKDIR /workspace
|
| 12 |
-
COPY scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/requirements.txt /tmp/proxy-requirements.txt
|
| 13 |
-
RUN python -m pip install --no-cache-dir -r /tmp/proxy-requirements.txt pytest
|
| 14 |
-
COPY . /workspace
|
| 15 |
-
ENV PYTHONPATH=/workspace \
|
| 16 |
-
PYTHONDONTWRITEBYTECODE=1 \
|
| 17 |
-
PYTHONUNBUFFERED=1
|
| 18 |
-
ENTRYPOINT ["python", "scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci/run_redis_chaos.py"]
|
| 19 |
-
CMD ["--mode", "all"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ci/redis_chaos_attestation.py
DELETED
|
@@ -1,189 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Canonical Redis chaos attestation payloads for Run 148 release evidence.
|
| 3 |
-
"""
|
| 4 |
-
|
| 5 |
-
from __future__ import annotations
|
| 6 |
-
|
| 7 |
-
import argparse
|
| 8 |
-
import hashlib
|
| 9 |
-
import json
|
| 10 |
-
import logging
|
| 11 |
-
import re
|
| 12 |
-
import sys
|
| 13 |
-
from datetime import datetime, timezone
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
from typing import Any
|
| 16 |
-
|
| 17 |
-
HERE = Path(__file__).resolve().parent
|
| 18 |
-
EXTENSION_ROOT = HERE.parents[1]
|
| 19 |
-
SECURITY = EXTENSION_ROOT / "_hf_spaces_proxy" / "security"
|
| 20 |
-
if str(SECURITY) not in sys.path:
|
| 21 |
-
sys.path.insert(0, str(SECURITY))
|
| 22 |
-
|
| 23 |
-
from source_tree import source_tree_sha256 # noqa: E402
|
| 24 |
-
|
| 25 |
-
logger = logging.getLogger(__name__)
|
| 26 |
-
|
| 27 |
-
SCHEMA_VERSION = 1
|
| 28 |
-
PREDICATE_TYPE = (
|
| 29 |
-
"https://scikit-plots.org/attestations/provider-artifact-redis-chaos/v1"
|
| 30 |
-
)
|
| 31 |
-
TOOL_NAME = "scikitplot-provider-artifact-redis-chaos"
|
| 32 |
-
TOOL_VERSION = "148"
|
| 33 |
-
_REVISION = re.compile(r"^[0-9a-f]{40}(?:[0-9a-f]{24})?$")
|
| 34 |
-
_IMAGE = re.compile(r"^redis:[A-Za-z0-9._-]+@sha256:[0-9a-f]{64}$")
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class AttestationError(RuntimeError):
|
| 38 |
-
"""Attestation inputs violate the fail-closed Run 148 contract."""
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def validate_source_revision( # ruff: ignore[undocumented-public-function]
|
| 42 |
-
value: str,
|
| 43 |
-
) -> str:
|
| 44 |
-
rendered = str(value or "").strip().lower()
|
| 45 |
-
if _REVISION.fullmatch(rendered) is None:
|
| 46 |
-
raise AttestationError("SOURCE_REVISION_INVALID")
|
| 47 |
-
return rendered
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def validate_redis_image( # ruff: ignore[undocumented-public-function]
|
| 51 |
-
value: str,
|
| 52 |
-
) -> str:
|
| 53 |
-
rendered = str(value or "").strip()
|
| 54 |
-
if _IMAGE.fullmatch(rendered) is None:
|
| 55 |
-
raise AttestationError("REDIS_IMAGE_NOT_DIGEST_PINNED")
|
| 56 |
-
return rendered
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def _outside_source_tree(path: Path) -> Path:
|
| 60 |
-
target = path.expanduser().resolve()
|
| 61 |
-
root = EXTENSION_ROOT.resolve()
|
| 62 |
-
if target == root or root in target.parents:
|
| 63 |
-
raise AttestationError("ATTESTATION_OUTPUT_INSIDE_SOURCE_TREE")
|
| 64 |
-
return target
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def build_attestation( # ruff: ignore[undocumented-public-function]
|
| 68 |
-
*,
|
| 69 |
-
mode: str,
|
| 70 |
-
redis_major: int,
|
| 71 |
-
redis_image: str,
|
| 72 |
-
source_revision: str,
|
| 73 |
-
generated_at: datetime | None = None,
|
| 74 |
-
) -> dict[str, Any]:
|
| 75 |
-
if mode not in {"standalone", "cluster"}:
|
| 76 |
-
raise AttestationError("REDIS_CHAOS_MODE_INVALID")
|
| 77 |
-
if redis_major not in {7, 8}:
|
| 78 |
-
raise AttestationError("REDIS_MAJOR_INVALID")
|
| 79 |
-
revision = validate_source_revision(source_revision)
|
| 80 |
-
image = validate_redis_image(redis_image)
|
| 81 |
-
now = (generated_at or datetime.now(timezone.utc)).astimezone(timezone.utc)
|
| 82 |
-
return {
|
| 83 |
-
"schemaVersion": SCHEMA_VERSION,
|
| 84 |
-
"predicateType": PREDICATE_TYPE,
|
| 85 |
-
"generatedAt": now.isoformat(timespec="seconds").replace("+00:00", "Z"),
|
| 86 |
-
"subject": {
|
| 87 |
-
"sourceRevision": revision,
|
| 88 |
-
"sourceTreeSha256": source_tree_sha256(EXTENSION_ROOT),
|
| 89 |
-
},
|
| 90 |
-
"redis": {
|
| 91 |
-
"major": redis_major,
|
| 92 |
-
"image": image,
|
| 93 |
-
"mode": mode,
|
| 94 |
-
},
|
| 95 |
-
"result": {"status": "pass"},
|
| 96 |
-
"tool": {"name": TOOL_NAME, "version": TOOL_VERSION},
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def write_attestation( # ruff: ignore[undocumented-public-function]
|
| 101 |
-
directory: Path,
|
| 102 |
-
*,
|
| 103 |
-
mode: str,
|
| 104 |
-
redis_major: int,
|
| 105 |
-
redis_image: str,
|
| 106 |
-
source_revision: str,
|
| 107 |
-
) -> Path:
|
| 108 |
-
root = _outside_source_tree(directory)
|
| 109 |
-
root.mkdir(parents=True, exist_ok=True)
|
| 110 |
-
target = root / f"redis-chaos-redis{redis_major}-{mode}.json"
|
| 111 |
-
payload = build_attestation(
|
| 112 |
-
mode=mode,
|
| 113 |
-
redis_major=redis_major,
|
| 114 |
-
redis_image=redis_image,
|
| 115 |
-
source_revision=source_revision,
|
| 116 |
-
)
|
| 117 |
-
target.write_text(
|
| 118 |
-
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
| 119 |
-
)
|
| 120 |
-
return target
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def _sha256(path: Path) -> str:
|
| 124 |
-
h = hashlib.sha256()
|
| 125 |
-
with path.open("rb") as handle:
|
| 126 |
-
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
| 127 |
-
h.update(chunk)
|
| 128 |
-
return h.hexdigest()
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def write_signature_verification_record( # ruff: ignore[undocumented-public-function]
|
| 132 |
-
attestation: Path,
|
| 133 |
-
output: Path,
|
| 134 |
-
*,
|
| 135 |
-
source_revision: str,
|
| 136 |
-
) -> Path:
|
| 137 |
-
revision = validate_source_revision(source_revision)
|
| 138 |
-
source = attestation.expanduser().resolve()
|
| 139 |
-
if not source.is_file() or source.is_symlink():
|
| 140 |
-
raise AttestationError("ATTESTATION_FILE_INVALID")
|
| 141 |
-
target = _outside_source_tree(output)
|
| 142 |
-
target.parent.mkdir(parents=True, exist_ok=True)
|
| 143 |
-
payload = {
|
| 144 |
-
"schemaVersion": 1,
|
| 145 |
-
"verified": True,
|
| 146 |
-
"verifiedAt": (
|
| 147 |
-
datetime.now(timezone.utc)
|
| 148 |
-
.isoformat(timespec="seconds")
|
| 149 |
-
.replace("+00:00", "Z")
|
| 150 |
-
),
|
| 151 |
-
"attestationSha256": _sha256(source),
|
| 152 |
-
"sourceRevision": revision,
|
| 153 |
-
"signerIdentityVerified": True,
|
| 154 |
-
}
|
| 155 |
-
target.write_text(
|
| 156 |
-
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
| 157 |
-
)
|
| 158 |
-
return target
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def main( # ruff: ignore[undocumented-public-function]
|
| 162 |
-
argv: list[str] | None = None,
|
| 163 |
-
) -> int:
|
| 164 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 165 |
-
sub = parser.add_subparsers(dest="command", required=True)
|
| 166 |
-
sig = sub.add_parser(
|
| 167 |
-
"signature-record",
|
| 168 |
-
help="emit sanitized record only after an external signature verifier succeeds",
|
| 169 |
-
)
|
| 170 |
-
sig.add_argument("--attestation", type=Path, required=True)
|
| 171 |
-
sig.add_argument("--output", type=Path, required=True)
|
| 172 |
-
sig.add_argument("--source-revision", required=True)
|
| 173 |
-
args = parser.parse_args(argv)
|
| 174 |
-
try:
|
| 175 |
-
if args.command == "signature-record":
|
| 176 |
-
write_signature_verification_record(
|
| 177 |
-
args.attestation,
|
| 178 |
-
args.output,
|
| 179 |
-
source_revision=args.source_revision,
|
| 180 |
-
)
|
| 181 |
-
return 0
|
| 182 |
-
raise AttestationError("COMMAND_INVALID")
|
| 183 |
-
except (AttestationError, OSError) as exc:
|
| 184 |
-
logger.error("[run148] FAIL: %s", exc)
|
| 185 |
-
return 2
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
if __name__ == "__main__":
|
| 189 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ci/run_redis_chaos.py
DELETED
|
@@ -1,208 +0,0 @@
|
|
| 1 |
-
# Authors: The scikit-plots developers
|
| 2 |
-
# SPDX-License-Identifier: BSD-3-Clause
|
| 3 |
-
|
| 4 |
-
"""
|
| 5 |
-
Mandatory Redis lifecycle chaos gate for CI.
|
| 6 |
-
|
| 7 |
-
This entrypoint is intentionally standard-library only. It validates the Redis
|
| 8 |
-
server major, verifies that the configured Python can import the pinned Redis
|
| 9 |
-
client, and then executes the live Redis lifecycle doctors with skip-as-failure
|
| 10 |
-
environment flags enabled.
|
| 11 |
-
|
| 12 |
-
Run from any working directory::
|
| 13 |
-
|
| 14 |
-
python _hf_spaces_proxy/ci/run_redis_chaos.py --mode all
|
| 15 |
-
|
| 16 |
-
The test suite itself starts disposable standalone/cluster Redis processes. No
|
| 17 |
-
Redis authority URL, password, provider token, prompt, or generated bytes are
|
| 18 |
-
printed by this runner.
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
from __future__ import annotations
|
| 22 |
-
|
| 23 |
-
import argparse
|
| 24 |
-
import logging
|
| 25 |
-
import os
|
| 26 |
-
import re
|
| 27 |
-
import shutil
|
| 28 |
-
import subprocess
|
| 29 |
-
import sys
|
| 30 |
-
from pathlib import Path
|
| 31 |
-
|
| 32 |
-
_CI_DIR = Path(__file__).resolve().parent
|
| 33 |
-
if str(_CI_DIR) not in sys.path:
|
| 34 |
-
sys.path.insert(0, str(_CI_DIR))
|
| 35 |
-
from redis_chaos_attestation import write_attestation # noqa: E402
|
| 36 |
-
|
| 37 |
-
_SUPPORTED_REDIS_MAJORS = {7, 8}
|
| 38 |
-
_TRUE = {"1", "true", "yes", "on"}
|
| 39 |
-
logger = logging.getLogger(__name__)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def _extension_root() -> Path:
|
| 43 |
-
return Path(__file__).resolve().parents[2]
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def _repo_pythonpath() -> Path:
|
| 47 |
-
# .../scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/ci
|
| 48 |
-
# parents[6] is the directory containing the top-level scikitplot package.
|
| 49 |
-
return Path(__file__).resolve().parents[6]
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def _resolve_binary(explicit: str, env_name: str, default_name: str) -> str:
|
| 53 |
-
candidate = str(explicit or "").strip() or os.environ.get(env_name, "").strip()
|
| 54 |
-
if candidate:
|
| 55 |
-
path = Path(candidate).expanduser()
|
| 56 |
-
if not path.is_file() or not os.access(path, os.X_OK):
|
| 57 |
-
raise RuntimeError(f"{env_name} does not name an executable file")
|
| 58 |
-
return str(path.resolve())
|
| 59 |
-
found = shutil.which(default_name)
|
| 60 |
-
if not found:
|
| 61 |
-
raise RuntimeError(f"required executable is unavailable: {default_name}")
|
| 62 |
-
return found
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def _redis_major(binary: str) -> int:
|
| 66 |
-
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
|
| 67 |
-
[binary, "--version"],
|
| 68 |
-
capture_output=True,
|
| 69 |
-
text=True,
|
| 70 |
-
timeout=10,
|
| 71 |
-
check=False,
|
| 72 |
-
)
|
| 73 |
-
if result.returncode != 0:
|
| 74 |
-
raise RuntimeError("redis-server --version failed")
|
| 75 |
-
rendered = (result.stdout or "") + " " + (result.stderr or "")
|
| 76 |
-
match = re.search(r"(?:v=|Redis server v=)(\d+)(?:\.|\b)", rendered)
|
| 77 |
-
if not match:
|
| 78 |
-
match = re.search(r"\b(\d+)\.\d+(?:\.\d+)?\b", rendered)
|
| 79 |
-
if not match:
|
| 80 |
-
raise RuntimeError("could not determine redis-server major version")
|
| 81 |
-
major = int(match.group(1))
|
| 82 |
-
if major not in _SUPPORTED_REDIS_MAJORS:
|
| 83 |
-
raise RuntimeError(
|
| 84 |
-
f"unsupported redis-server major {major}; expected Redis 7 or 8"
|
| 85 |
-
)
|
| 86 |
-
return major
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def _verify_python_runtime(python: str) -> None:
|
| 90 |
-
code = (
|
| 91 |
-
"import pytest, redis; "
|
| 92 |
-
"from redis.asyncio.cluster import RedisCluster; "
|
| 93 |
-
"print(getattr(redis, '__version__', 'unknown'))"
|
| 94 |
-
)
|
| 95 |
-
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
|
| 96 |
-
[python, "-c", code],
|
| 97 |
-
capture_output=True,
|
| 98 |
-
text=True,
|
| 99 |
-
timeout=20,
|
| 100 |
-
check=False,
|
| 101 |
-
)
|
| 102 |
-
if result.returncode != 0:
|
| 103 |
-
raise RuntimeError(
|
| 104 |
-
"CI Python must provide pytest, redis, and redis.asyncio.cluster.RedisCluster"
|
| 105 |
-
)
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def _run_pytest(*, python: str, redis_server: str, mode: str) -> None:
|
| 109 |
-
root = _extension_root()
|
| 110 |
-
env = os.environ.copy()
|
| 111 |
-
existing = env.get("PYTHONPATH", "").strip()
|
| 112 |
-
repo_path = str(_repo_pythonpath())
|
| 113 |
-
env["PYTHONPATH"] = repo_path if not existing else repo_path + os.pathsep + existing
|
| 114 |
-
env["RUN146_REDIS_SERVER"] = redis_server
|
| 115 |
-
env["RUN147_REDIS_SERVER"] = redis_server
|
| 116 |
-
|
| 117 |
-
if mode == "standalone":
|
| 118 |
-
env["RUN146_REDIS_CHAOS_REQUIRED"] = "1"
|
| 119 |
-
env["RUN147_REDIS_STANDALONE_REQUIRED"] = "1"
|
| 120 |
-
command = [
|
| 121 |
-
python,
|
| 122 |
-
"-m",
|
| 123 |
-
"pytest",
|
| 124 |
-
"-q",
|
| 125 |
-
"-p",
|
| 126 |
-
"no:cacheprovider",
|
| 127 |
-
"tests/_hf_spaces_proxy/_utils/test__provider_artifact_lifecycle__redis_chaos.py",
|
| 128 |
-
"tests/_hf_spaces_proxy/ci/test_run_redis_chaos.py",
|
| 129 |
-
"-k",
|
| 130 |
-
"live_redis or live_standalone",
|
| 131 |
-
]
|
| 132 |
-
elif mode == "cluster":
|
| 133 |
-
env["RUN147_REDIS_CLUSTER_REQUIRED"] = "1"
|
| 134 |
-
command = [
|
| 135 |
-
python,
|
| 136 |
-
"-m",
|
| 137 |
-
"pytest",
|
| 138 |
-
"-q",
|
| 139 |
-
"-p",
|
| 140 |
-
"no:cacheprovider",
|
| 141 |
-
"tests/_hf_spaces_proxy/ci/test_run_redis_chaos.py",
|
| 142 |
-
"-k",
|
| 143 |
-
"live_cluster",
|
| 144 |
-
]
|
| 145 |
-
else: # pragma: no cover - argparse constrains this
|
| 146 |
-
raise RuntimeError("unsupported chaos mode")
|
| 147 |
-
|
| 148 |
-
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
|
| 149 |
-
command,
|
| 150 |
-
cwd=root,
|
| 151 |
-
env=env,
|
| 152 |
-
check=False,
|
| 153 |
-
)
|
| 154 |
-
if result.returncode != 0:
|
| 155 |
-
raise RuntimeError(f"Redis {mode} chaos gate failed")
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def main( # ruff: ignore[undocumented-public-function]
|
| 159 |
-
argv: list[str] | None = None,
|
| 160 |
-
) -> int:
|
| 161 |
-
parser = argparse.ArgumentParser(description=__doc__)
|
| 162 |
-
parser.add_argument(
|
| 163 |
-
"--mode", choices=("standalone", "cluster", "all"), default="all"
|
| 164 |
-
)
|
| 165 |
-
parser.add_argument("--redis-server", default="")
|
| 166 |
-
parser.add_argument("--python", default=sys.executable)
|
| 167 |
-
parser.add_argument("--attestation-dir", type=Path)
|
| 168 |
-
parser.add_argument("--source-revision", default="")
|
| 169 |
-
parser.add_argument("--redis-image", default="")
|
| 170 |
-
args = parser.parse_args(argv)
|
| 171 |
-
|
| 172 |
-
try:
|
| 173 |
-
redis_server = _resolve_binary(
|
| 174 |
-
args.redis_server, "RUN147_REDIS_SERVER", "redis-server"
|
| 175 |
-
)
|
| 176 |
-
major = _redis_major(redis_server)
|
| 177 |
-
_verify_python_runtime(args.python)
|
| 178 |
-
modes = ("standalone", "cluster") if args.mode == "all" else (args.mode,)
|
| 179 |
-
if args.attestation_dir is not None and (
|
| 180 |
-
not args.source_revision or not args.redis_image
|
| 181 |
-
):
|
| 182 |
-
raise RuntimeError(
|
| 183 |
-
"attestation output requires --source-revision and digest-pinned --redis-image"
|
| 184 |
-
)
|
| 185 |
-
for mode in modes:
|
| 186 |
-
logger.info("[run147] Redis %s %s chaos gate", major, mode)
|
| 187 |
-
_run_pytest(python=args.python, redis_server=redis_server, mode=mode)
|
| 188 |
-
if args.attestation_dir is not None:
|
| 189 |
-
logger.info(
|
| 190 |
-
"[run147] writing Redis %s attestation for mode=%s", major, mode
|
| 191 |
-
)
|
| 192 |
-
write_attestation(
|
| 193 |
-
args.attestation_dir,
|
| 194 |
-
mode=mode,
|
| 195 |
-
redis_major=major,
|
| 196 |
-
redis_image=args.redis_image,
|
| 197 |
-
source_revision=args.source_revision,
|
| 198 |
-
)
|
| 199 |
-
logger.info("[run147] PASS Redis %s mode=%s", major, args.mode)
|
| 200 |
-
return 0
|
| 201 |
-
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
| 202 |
-
# Never include Redis URLs or secret environment values in failures.
|
| 203 |
-
logger.error("[run147] FAIL: %s", exc)
|
| 204 |
-
return 2
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
if __name__ == "__main__":
|
| 208 |
-
raise SystemExit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|