Compare commits
408
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d498fbbb68 | ||
|
|
ec8114a6a4 | ||
|
|
6115366656 | ||
|
|
983e1b84cc | ||
|
|
937c3db160 | ||
|
|
a528c0111d | ||
|
|
c673aff0fe | ||
|
|
a4b6b561d7 | ||
|
|
23f5318dfc | ||
|
|
3df4a97bba | ||
|
|
a26d38b749 | ||
|
|
a29d646027 | ||
|
|
cf9d2ffe19 | ||
|
|
647022a27e | ||
|
|
7d042d0a1c | ||
|
|
8d7a7b4d1b | ||
|
|
a1ec187919 | ||
|
|
a5773bf710 | ||
|
|
0a8928b5aa | ||
|
|
8f61c46ff0 | ||
|
|
4cc09c268a | ||
|
|
6bb7b994ed | ||
|
|
facfd10316 | ||
|
|
3aea8f41b8 | ||
|
|
553a368714 | ||
|
|
249b13d503 | ||
|
|
c22ba346af | ||
|
|
b276592daf | ||
|
|
7da90fc53b | ||
|
|
eec70a9b8c | ||
|
|
7eda26d177 | ||
|
|
3c94727101 | ||
|
|
561024738a | ||
|
|
9a117cca70 | ||
|
|
e340b3cd41 | ||
|
|
2803b8b484 | ||
|
|
47aedc2ac8 | ||
|
|
7e5121a642 | ||
|
|
ce3850b0b0 | ||
|
|
1682dfc08f | ||
|
|
70c56eadec | ||
|
|
c716fde17b | ||
|
|
aaca9e1ec5 | ||
|
|
c8cb04d859 | ||
|
|
543f2b97dd | ||
|
|
30364abf21 | ||
|
|
6486dba289 | ||
|
|
57c46004b1 | ||
|
|
3abb1770c2 | ||
|
|
08ee09af1e | ||
|
|
72fa368357 | ||
|
|
86bae93e98 | ||
|
|
c0b7d3a747 | ||
|
|
6e5e30afd4 | ||
|
|
9f0600d6c1 | ||
|
|
f2346d5d09 | ||
|
|
b4302ab6f7 | ||
|
|
75cdb94e27 | ||
|
|
bce8a48cad | ||
|
|
de66236383 | ||
|
|
f1131abf79 | ||
|
|
fc3f424208 | ||
|
|
75228c815d | ||
|
|
485af03367 | ||
|
|
2acb669c31 | ||
|
|
79908b8a54 | ||
|
|
9037ddab6e | ||
|
|
5d92b99926 | ||
|
|
fff02656b7 | ||
|
|
5f1b9bf8d4 | ||
|
|
7280fcc3cf | ||
|
|
ea767e23ae | ||
|
|
3de8e29198 | ||
|
|
c7e5ddb0cb | ||
|
|
683cc882be | ||
|
|
9c1b469f0d | ||
|
|
4995665fe5 | ||
|
|
5a861df968 | ||
|
|
01ec6f7d8b | ||
|
|
17673e8fa5 | ||
|
|
9a63a82a49 | ||
|
|
061694f83a | ||
|
|
ed5b2d5b80 | ||
|
|
68d19353aa | ||
|
|
eaf5db5e91 | ||
|
|
598dc766fa | ||
|
|
ab42fa8004 | ||
|
|
ee1c11542e | ||
|
|
0259c30bb5 | ||
|
|
1e7470d476 | ||
|
|
9840c50e9f | ||
|
|
2bafab8b22 | ||
|
|
5ad2aa4c6a | ||
|
|
85b8b6122f | ||
|
|
8a6c5e95ee | ||
|
|
771a952f02 | ||
|
|
9fab7757b0 | ||
|
|
3884a6a7f7 | ||
|
|
4234719e53 | ||
|
|
032506535f | ||
|
|
5675c1d77a | ||
|
|
29137263fb | ||
|
|
8d229f1720 | ||
|
|
9a7946c863 | ||
|
|
10ffd29257 | ||
|
|
933932b091 | ||
|
|
dbbd05ea97 | ||
|
|
dc4749aca6 | ||
|
|
ce7ad1ba70 | ||
|
|
71f45ca5ea | ||
|
|
754e74f6d8 | ||
|
|
344c004299 | ||
|
|
07b85bda5e | ||
|
|
8b3589a489 | ||
|
|
c762e30331 | ||
|
|
0807b16aa4 | ||
|
|
0f9e6b6b58 | ||
|
|
9de7e1d320 | ||
|
|
2dd5775296 | ||
|
|
f2517b2ade | ||
|
|
c744fd12c3 | ||
|
|
bbf66bb0f0 | ||
|
|
bfefba9633 | ||
|
|
d6d32bf8e6 | ||
|
|
07776dc6fb | ||
|
|
87d346ee20 | ||
|
|
fa1057ddfd | ||
|
|
009ce1e9f3 | ||
|
|
5f89db4012 | ||
|
|
f50b8dfc83 | ||
|
|
6892080128 | ||
|
|
fe25166478 | ||
|
|
1154544e99 | ||
|
|
ac33d3c7be | ||
|
|
78434048b7 | ||
|
|
fa7ba9149e | ||
|
|
6147c14c9b | ||
|
|
3257e42a02 | ||
|
|
4815194db0 | ||
|
|
adeba21665 | ||
|
|
b295314bde | ||
|
|
f23926636c | ||
|
|
2de7fc7f20 | ||
|
|
a3a5adaa19 | ||
|
|
4a17129204 | ||
|
|
6109921849 | ||
|
|
1f8fc52cb1 | ||
|
|
1ef65f00f9 | ||
|
|
66161e8d41 | ||
|
|
11502717c3 | ||
|
|
512aeb79a0 | ||
|
|
1f2635ea7d | ||
|
|
671ee4ea62 | ||
|
|
42acb862b6 | ||
|
|
17935f4c9e | ||
|
|
1be7b1c144 | ||
|
|
9778ef51a7 | ||
|
|
76dfd7a86c | ||
|
|
cccf9e426c | ||
|
|
5fbf538eb6 | ||
|
|
7d856c276b | ||
|
|
d982f39870 | ||
|
|
3fff152a3d | ||
|
|
52c9ec9211 | ||
|
|
f9a221bad5 | ||
|
|
05e60a6b74 | ||
|
|
9970f225cc | ||
|
|
b0a4ed93d8 | ||
|
|
4dab688b6f | ||
|
|
6e03301b7a | ||
|
|
0987a71f1e | ||
|
|
0caa53bf8c | ||
|
|
0d4960f3ff | ||
|
|
e7e464b4fa | ||
|
|
66e6cdc027 | ||
|
|
d985e3191a | ||
|
|
caf5de678a | ||
|
|
6a5cde2b91 | ||
|
|
4b6c8a9191 | ||
|
|
dc38797ee7 | ||
|
|
b42570be14 | ||
|
|
eaa218f8e0 | ||
|
|
efc638a0f9 | ||
|
|
11269d1c21 | ||
|
|
9a3f45e60d | ||
|
|
f7edfd4eee | ||
|
|
fe1ee45775 | ||
|
|
2f5bf4b76e | ||
|
|
91a8f6a869 | ||
|
|
ccd5119b06 | ||
|
|
63068aae1d | ||
|
|
86d954e103 | ||
|
|
731ab90ce1 | ||
|
|
063b69ef99 | ||
|
|
330d46bb43 | ||
|
|
4cda3ad22f | ||
|
|
f437c5df9d | ||
|
|
c91d9cc7dc | ||
|
|
868553f27d | ||
|
|
125c3715a8 | ||
|
|
25a795f0fd | ||
|
|
55998419ee | ||
|
|
9028711475 | ||
|
|
0ebfed6014 | ||
|
|
83c2088185 | ||
|
|
4e2e4830a1 | ||
|
|
ae3b4df92c | ||
|
|
40b4d8aba3 | ||
|
|
9a9c740e1c | ||
|
|
c08507e1b8 | ||
|
|
9d3874c88f | ||
|
|
091eae36cc | ||
|
|
8bceba0182 | ||
|
|
2e09c9135e | ||
|
|
d9665f3e79 | ||
|
|
a21078f4a9 | ||
|
|
acfd1f1f10 | ||
|
|
aa9b667940 | ||
|
|
4f0a278df1 | ||
|
|
f242ad5a16 | ||
|
|
cd69565c2e | ||
|
|
7fdca9ffdb | ||
|
|
ee5a466940 | ||
|
|
e5a7f04c4e | ||
|
|
40046d9bcb | ||
|
|
8d2d2d6197 | ||
|
|
ae5309c2b6 | ||
|
|
0d9918b401 | ||
|
|
c5c54f5963 | ||
|
|
978d2d3a84 | ||
|
|
d23f5aa29d | ||
|
|
62d646836d | ||
|
|
9b03fff032 | ||
|
|
519069f97f | ||
|
|
b4f144a7fb | ||
|
|
677ead4fd3 | ||
|
|
7ae03938c3 | ||
|
|
d2d7f3dea5 | ||
|
|
56dc1c632d | ||
|
|
e33a6776e5 | ||
|
|
9fe9f2ed36 | ||
|
|
10a0ac3311 | ||
|
|
bc4035e723 | ||
|
|
7c4a2ea12c | ||
|
|
346a63abaa | ||
|
|
303eb99f49 | ||
|
|
8ba4b1a290 | ||
|
|
cda63ba245 | ||
|
|
ad71cff465 | ||
|
|
e7d059ceed | ||
|
|
e8d99b83ff | ||
|
|
09e550df79 | ||
|
|
9328f04f4a | ||
|
|
44e237e60e | ||
|
|
56c716a4ea | ||
|
|
37f7f5293f | ||
|
|
99c9b25487 | ||
|
|
b590e24de8 | ||
|
|
b16eb4e88a | ||
|
|
9eae7cf25e | ||
|
|
4d654e106c | ||
|
|
9747846787 | ||
|
|
61d2185bb6 | ||
|
|
b92c1b970d | ||
|
|
8f313720f0 | ||
|
|
2f40412d51 | ||
|
|
1d919544af | ||
|
|
c22125201f | ||
|
|
0ebe59ea2f | ||
|
|
59692fafa3 | ||
|
|
3f7e623eb6 | ||
|
|
3ecdce7b07 | ||
|
|
3d1d82f582 | ||
|
|
f2145a747f | ||
|
|
cfae2b2579 | ||
|
|
fdf5cec841 | ||
|
|
2aaf55a53c | ||
|
|
9bd896633b | ||
|
|
1b490c475e | ||
|
|
7b13d46352 | ||
|
|
816cd1728b | ||
|
|
8c85cd22bb | ||
|
|
1016f568b8 | ||
|
|
ab7a334987 | ||
|
|
2e7bdc94ac | ||
|
|
025b03e678 | ||
|
|
830d74a5ae | ||
|
|
90a5aafa1c | ||
|
|
f7e7849460 | ||
|
|
38d5b172f4 | ||
|
|
e9679e18c0 | ||
|
|
d58cb38c79 | ||
|
|
2a55328d11 | ||
|
|
1d224cf2d3 | ||
|
|
9c6f00dbce | ||
|
|
3c9cb9b7da | ||
|
|
3aba6d5082 | ||
|
|
87f3b9ec00 | ||
|
|
e96c8a2a13 | ||
|
|
d0b4e31167 | ||
|
|
3db2217c32 | ||
|
|
eae138f7fd | ||
|
|
533bf32577 | ||
|
|
ab07ac0389 | ||
|
|
5a4a5795b2 | ||
|
|
2ed6993b58 | ||
|
|
532eb3d4f9 | ||
|
|
79a40904e4 | ||
|
|
01c8238311 | ||
|
|
c1f1daa6fe | ||
|
|
ea0a690862 | ||
|
|
050bb85d27 | ||
|
|
c16055d7e4 | ||
|
|
085c4431a2 | ||
|
|
e61cc7c0c4 | ||
|
|
5c00205f62 | ||
|
|
2d5554fadf | ||
|
|
db4364d336 | ||
|
|
4dd02a96e9 | ||
|
|
1da481daa1 | ||
|
|
96c2ade7ff | ||
|
|
d236fe6cea | ||
|
|
e2586ddd26 | ||
|
|
2ba5a1491a | ||
|
|
75b8d39cc1 | ||
|
|
ec311f4802 | ||
|
|
aed3edaf6a | ||
|
|
1c9e59a6ea | ||
|
|
ef546a435b | ||
|
|
ff68b8f0f1 | ||
|
|
5d5b08e084 | ||
|
|
52a3a3014d | ||
|
|
62f4eed221 | ||
|
|
f94f4a6d22 | ||
|
|
72bb0132bf | ||
|
|
929fdb1157 | ||
|
|
9db35be2ec | ||
|
|
f5a1be34c6 | ||
|
|
dc9577613f | ||
|
|
4efc0d9fa4 | ||
|
|
b70e8033f3 | ||
|
|
4492b4f0d2 | ||
|
|
71b061cac7 | ||
|
|
a7e024002f | ||
|
|
6c8b2047d9 | ||
|
|
cdd51f6dc7 | ||
|
|
baf605464c | ||
|
|
18e59f3490 | ||
|
|
5b132bcb54 | ||
|
|
e9d84a5cf7 | ||
|
|
e4e7afd580 | ||
|
|
7e6de4146e | ||
|
|
48ca6f41fa | ||
|
|
12d122ad83 | ||
|
|
e36d910f2f | ||
|
|
98f5e481d1 | ||
|
|
9b38447dbe | ||
|
|
f06216b931 | ||
|
|
a12a4fb154 | ||
|
|
ca822c508d | ||
|
|
8880ddd2bd | ||
|
|
802f3459e9 | ||
|
|
3f159b8ece | ||
|
|
60e653b63c | ||
|
|
419ea1854f | ||
|
|
3d908bab6c | ||
|
|
4fd00607df | ||
|
|
07fbda3f9a | ||
|
|
9a45883278 | ||
|
|
33a3f85136 | ||
|
|
31e034ab67 | ||
|
|
7eab4840ef | ||
|
|
cbf0fbf5b5 | ||
|
|
56063bbbf1 | ||
|
|
f5c60f0bca | ||
|
|
bb81d7f037 | ||
|
|
68d089874d | ||
|
|
7c8a278cd9 | ||
|
|
2bcf512737 | ||
|
|
7b91c83f5b | ||
|
|
bdd977d8a9 | ||
|
|
f87163acdc | ||
|
|
de16ba3e36 | ||
|
|
1512bd2716 | ||
|
|
d33eb53003 | ||
|
|
c977e37ab8 | ||
|
|
8a6ababba6 | ||
|
|
388aacf71c | ||
|
|
230f9f3bd0 | ||
|
|
ab8318e102 | ||
|
|
e800197cdd | ||
|
|
2f694bac8d | ||
|
|
124a4e3738 | ||
|
|
adbcfb9974 | ||
|
|
a3ccd07254 | ||
|
|
d8ff9e4d02 | ||
|
|
6af0890571 | ||
|
|
c50cf02f7d | ||
|
|
14d663143e | ||
|
|
f70ce8e8bb | ||
|
|
935c2320a7 | ||
|
|
f0d60493ae | ||
|
|
8cbba00407 | ||
|
|
99e5b4c89a | ||
|
|
3d1ee8ebdb | ||
|
|
da68fa1de8 | ||
|
|
4fb0e7c281 | ||
|
|
8c16ae3385 |
@@ -0,0 +1,74 @@
|
||||
name: Dist Tests
|
||||
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches:
|
||||
# - master
|
||||
# tags:
|
||||
# - 'v*.*.*'
|
||||
# paths-ignore:
|
||||
# - '**/*.md'
|
||||
# - '.gitignore'
|
||||
# - 'docker/**'
|
||||
# - '!docker/job.yaml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Branch (master)
|
||||
required: false
|
||||
type: string
|
||||
source:
|
||||
description: Repository with tests (current)
|
||||
required: false
|
||||
type: string
|
||||
nameprefix:
|
||||
description: Runner prefix (cs-codex-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
namespace:
|
||||
description: Runner namespace (cs-codex-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
|
||||
NAMEPREFIX: cs-codex-dist-tests
|
||||
NAMESPACE: cs-codex-dist-tests
|
||||
JOB_MANIFEST: docker/job.yaml
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
KUBE_VERSION: v1.26.1
|
||||
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Variables
|
||||
run: |
|
||||
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.nameprefix }}" ]] && echo "NAMEPREFIX=${{ inputs.nameprefix }}" >>"$GITHUB_ENV" || echo "NAMEPREFIX=${{ env.NAMEPREFIX }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
|
||||
echo "RUNID=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
|
||||
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
|
||||
uses: azure/setup-kubectl@v3
|
||||
with:
|
||||
version: ${{ env.KUBE_VERSION }}
|
||||
|
||||
- name: Kubectl - Kubeconfig
|
||||
run: |
|
||||
mkdir -p "${HOME}"/.kube
|
||||
echo "${{ env.KUBE_CONFIG }}" | base64 -d > "${HOME}"/.kube/config
|
||||
|
||||
- name: Kubectl - Create Job
|
||||
run: |
|
||||
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
|
||||
@@ -0,0 +1,177 @@
|
||||
name: Docker - Reusable
|
||||
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
docker_file:
|
||||
default: docker/Dockerfile
|
||||
description: Dockerfile
|
||||
required: false
|
||||
type: string
|
||||
docker_repo:
|
||||
default: codexstorage/cs-codex-dist-tests
|
||||
description: DockerHub repository
|
||||
required: false
|
||||
type: string
|
||||
tag_latest:
|
||||
default: true
|
||||
description: Set latest tag for Docker images
|
||||
required: false
|
||||
type: boolean
|
||||
tag_sha:
|
||||
default: true
|
||||
description: Set Git short commit as Docker tag
|
||||
required: false
|
||||
type: boolean
|
||||
tag_suffix:
|
||||
default: ''
|
||||
description: Suffix for Docker images tag
|
||||
required: false
|
||||
type: string
|
||||
|
||||
|
||||
env:
|
||||
DOCKER_FILE: ${{ inputs.docker_file }}
|
||||
DOCKER_REPO: ${{ inputs.docker_repo }}
|
||||
TAG_LATEST: ${{ inputs.tag_latest }}
|
||||
TAG_SHA: ${{ inputs.tag_sha }}
|
||||
TAG_SUFFIX: ${{ inputs.tag_suffix }}
|
||||
|
||||
|
||||
jobs:
|
||||
# Build platform specific image
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
target:
|
||||
- os: linux
|
||||
arch: amd64
|
||||
- os: linux
|
||||
arch: arm64
|
||||
include:
|
||||
- target:
|
||||
os: linux
|
||||
arch: amd64
|
||||
builder: ubuntu-22.04
|
||||
- target:
|
||||
os: linux
|
||||
arch: arm64
|
||||
builder: buildjet-4vcpu-ubuntu-2204-arm
|
||||
|
||||
name: Build ${{ matrix.target.os }}/${{ matrix.target.arch }}
|
||||
runs-on: ${{ matrix.builder }}
|
||||
env:
|
||||
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Build and Push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKER_FILE }}
|
||||
platforms: ${{ env.PLATFORM }}
|
||||
push: true
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
|
||||
|
||||
- name: Docker - Export digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Docker - Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
|
||||
# Publish multi-platform image
|
||||
publish:
|
||||
name: Publish multi-platform image
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Docker - Variables
|
||||
run: |
|
||||
# Adjust custom suffix when set and
|
||||
if [[ -n "${{ env.TAG_SUFFIX }}" ]]; then
|
||||
echo "TAG_SUFFIX=-${{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
|
||||
fi
|
||||
# Disable SHA tags on tagged release
|
||||
if [[ ${{ startsWith(github.ref, 'refs/tags/') }} == "true" ]]; then
|
||||
echo "TAG_SHA=false" >>$GITHUB_ENV
|
||||
fi
|
||||
# Handle latest and latest-custom using raw
|
||||
if [[ ${{ env.TAG_SHA }} == "false" ]]; then
|
||||
echo "TAG_LATEST=false" >>$GITHUB_ENV
|
||||
echo "TAG_RAW=true" >>$GITHUB_ENV
|
||||
if [[ -z "${{ env.TAG_SUFFIX }}" ]]; then
|
||||
echo "TAG_RAW_VALUE=latest" >>$GITHUB_ENV
|
||||
else
|
||||
echo "TAG_RAW_VALUE=latest-{{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "TAG_RAW=false" >>$GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Docker - Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
flavor: |
|
||||
latest=${{ env.TAG_LATEST }}
|
||||
suffix=${{ env.TAG_SUFFIX }},onlatest=true
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,enable=${{ env.TAG_RAW }},value=latest
|
||||
type=sha,enable=${{ env.TAG_SHA }}
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
|
||||
|
||||
- name: Docker - Inspect image
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.DOCKER_REPO }}:${{ steps.meta.outputs.version }}
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Docker
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- docker/Dockerfile
|
||||
- docker/docker-entrypoint.sh
|
||||
- .github/workflows/docker.yml
|
||||
- .github/workflows/docker-reusable.yml
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
uses: ./.github/workflows/docker-reusable.yml
|
||||
secrets: inherit
|
||||
@@ -1,3 +1,4 @@
|
||||
.vs
|
||||
obj
|
||||
bin
|
||||
.vscode
|
||||
@@ -0,0 +1,254 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ArgsUniform
|
||||
{
|
||||
public class ArgsUniform<T>
|
||||
{
|
||||
private readonly Action printAppInfo;
|
||||
private readonly object? defaultsProvider;
|
||||
private readonly IEnv.IEnv env;
|
||||
private readonly string[] args;
|
||||
private const int cliStart = 8;
|
||||
private const int shortStart = 38;
|
||||
private const int envStart = 48;
|
||||
private const int descStart = 80;
|
||||
|
||||
public ArgsUniform(Action printAppInfo, params string[] args)
|
||||
: this(printAppInfo, new IEnv.Env(), args)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgsUniform(Action printAppInfo, object defaultsProvider, params string[] args)
|
||||
: this(printAppInfo, defaultsProvider, new IEnv.Env(), args)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgsUniform(Action printAppInfo, IEnv.IEnv env, params string[] args)
|
||||
: this(printAppInfo, null!, env, args)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgsUniform(Action printAppInfo, object defaultsProvider, IEnv.IEnv env, params string[] args)
|
||||
{
|
||||
this.printAppInfo = printAppInfo;
|
||||
this.defaultsProvider = defaultsProvider;
|
||||
this.env = env;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public T Parse(bool printResult = false)
|
||||
{
|
||||
if (args.Any(a => a == "-h" || a == "--help" || a == "-?"))
|
||||
{
|
||||
printAppInfo();
|
||||
PrintHelp();
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
var result = Activator.CreateInstance<T>();
|
||||
var uniformProperties = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).ToArray();
|
||||
var missingRequired = new List<PropertyInfo>();
|
||||
foreach (var uniformProperty in uniformProperties)
|
||||
{
|
||||
var attr = uniformProperty.GetCustomAttribute<UniformAttribute>();
|
||||
if (attr != null)
|
||||
{
|
||||
if (!UniformAssign(result, attr, uniformProperty) && attr.Required)
|
||||
{
|
||||
{
|
||||
missingRequired.Add(uniformProperty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRequired.Any())
|
||||
{
|
||||
PrintResults(result, uniformProperties);
|
||||
Print("");
|
||||
foreach (var missing in missingRequired)
|
||||
{
|
||||
var attr = missing.GetCustomAttribute<UniformAttribute>()!;
|
||||
var exampleArg = $"--{attr.Arg}=...";
|
||||
var exampleEnvVar = $"{attr.EnvVar}=...";
|
||||
Print($" ! Missing required input. Use argument: '{exampleArg}' or environment variable: '{exampleEnvVar}'.");
|
||||
}
|
||||
|
||||
PrintHelp();
|
||||
throw new ArgumentException("Unable to assemble all required arguments");
|
||||
}
|
||||
|
||||
if (printResult)
|
||||
{
|
||||
PrintResults(result, uniformProperties);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void PrintResults(T result, PropertyInfo[] uniformProperties)
|
||||
{
|
||||
Print("");
|
||||
foreach (var p in uniformProperties)
|
||||
{
|
||||
Print($"\t{p.Name} = {p.GetValue(result)}");
|
||||
}
|
||||
Print("");
|
||||
}
|
||||
|
||||
public void PrintHelp()
|
||||
{
|
||||
Print("");
|
||||
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description");
|
||||
var attrs = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).Select(p => p.GetCustomAttribute<UniformAttribute>()).Where(a => a != null).ToArray();
|
||||
foreach (var attr in attrs)
|
||||
{
|
||||
var a = attr!;
|
||||
var optional = !a.Required ? " *" : "";
|
||||
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional);
|
||||
}
|
||||
Print("");
|
||||
}
|
||||
|
||||
private void Print(string msg)
|
||||
{
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
|
||||
private void PrintAligned(string cli, string s, string env, string desc)
|
||||
{
|
||||
Console.CursorLeft = cliStart;
|
||||
Console.Write(cli);
|
||||
Console.CursorLeft = shortStart;
|
||||
Console.Write(s);
|
||||
Console.CursorLeft = envStart;
|
||||
Console.Write(env);
|
||||
Console.CursorLeft = descStart;
|
||||
Console.Write(desc + Environment.NewLine);
|
||||
}
|
||||
|
||||
private object GetDefaultValue(Type t)
|
||||
{
|
||||
if (t.IsValueType) return Activator.CreateInstance(t)!;
|
||||
return null!;
|
||||
}
|
||||
|
||||
private bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
|
||||
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
|
||||
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
|
||||
{
|
||||
var currentValue = uniformProperty.GetValue(result);
|
||||
var isEmptryString = (currentValue as string) == string.Empty;
|
||||
if (currentValue != GetDefaultValue(uniformProperty.PropertyType) && !isEmptryString) return true;
|
||||
|
||||
if (defaultsProvider == null) return false;
|
||||
|
||||
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
|
||||
if (defaultProperty == null) return false;
|
||||
|
||||
var value = defaultProperty.GetValue(defaultsProvider);
|
||||
if (value != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
|
||||
if (!string.IsNullOrEmpty(e))
|
||||
{
|
||||
return Assign(result, uniformProperty, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
|
||||
{
|
||||
var fromArg = GetFromArgs(attr.Arg);
|
||||
if (fromArg != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, fromArg);
|
||||
}
|
||||
var fromShort = GetFromArgs(attr.ArgShort);
|
||||
if (fromShort != null)
|
||||
{
|
||||
return Assign(result, uniformProperty, fromShort);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool Assign(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (uniformProperty.PropertyType == value.GetType())
|
||||
{
|
||||
uniformProperty.SetValue(result, value);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
|
||||
{
|
||||
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
|
||||
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
var s = value.ToString();
|
||||
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
|
||||
{
|
||||
uniformProperty.SetValue(result, e);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (int.TryParse(value.ToString(), out int i))
|
||||
{
|
||||
uniformProperty.SetValue(result, i);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
|
||||
{
|
||||
if (value != null)
|
||||
{
|
||||
uniformProperty.SetValue(result, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private string? GetFromArgs(string key)
|
||||
{
|
||||
var argKey = $"--{key}=";
|
||||
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
|
||||
if (arg != null)
|
||||
{
|
||||
return arg.Substring(argKey.Length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="I-Env" Version="1.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace ArgsUniform
|
||||
{
|
||||
public class ExampleUser
|
||||
{
|
||||
public class Args
|
||||
{
|
||||
[Uniform("aaa", "a", "AAA", false, "Sets the AAA!")]
|
||||
public string Aaa { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("bbb", "b", "BBB", true, "Sets that BBB")]
|
||||
public string Bbb { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class DefaultsProvider
|
||||
{
|
||||
public string Aaa { get { return "non-static operation"; } }
|
||||
}
|
||||
|
||||
public void Example()
|
||||
{
|
||||
// env var: "AAA=BBB"
|
||||
var args = "--ccc=ddd";
|
||||
|
||||
var uniform = new ArgsUniform<Args>(PrintHelp, new DefaultsProvider(), args);
|
||||
|
||||
var aaa = uniform.Parse();
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Console.WriteLine("Help text!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace ArgsUniform
|
||||
{
|
||||
public class UniformAttribute : Attribute
|
||||
{
|
||||
public UniformAttribute(string arg, string argShort, string envVar, bool required, string description)
|
||||
{
|
||||
Arg = arg;
|
||||
ArgShort = argShort;
|
||||
EnvVar = envVar;
|
||||
Required = required;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public string Arg { get; }
|
||||
public string ArgShort { get; }
|
||||
public string EnvVar { get; }
|
||||
public bool Required { get; }
|
||||
public string Description { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Distributed System Tests for Nim-Codex
|
||||
|
||||
## Contributing tests
|
||||
Do you want to write some tests for Codex using this distributed test setup? Great! Here's what you do.
|
||||
|
||||
1. Create a branch. Name it something descriptive, but start it with `tests/` please. [Example: `tests/data-redundancy`.]
|
||||
1. Checkout your branch, and decide if your tests will be 'short' tests (minutes to hours), or 'long' tests (hours to days), or both! Create a folder for your tests in the matching folders (`/Tests`, `/LongTests`) and don't worry: You can always move your tests later if you like. [Example, short: `/Tests/DataRedundancy/`, long: `/LongTests/DataRedundancy/`]
|
||||
1. Create one or more code files in your new folder, and write some tests! Here are some tips to help you get started. You can always take a look at the example tests found in [`/Tests/BasicTests/ExampleTests.cs`](/Tests/BasicTests/ExampleTests.cs)
|
||||
1. Set up a standard NUnit test fixture.
|
||||
1. Inherrit from `DistTest` or `AutoBootstrapDistTest`.
|
||||
1. When using `DistTest`:
|
||||
1. You must start your own Codex bootstrap node. You can use `SetupCodexBootstrapNode(...)` for this.
|
||||
1. When you start other Codex nodes with `SetupCodexNodes(...)` you can pass the bootstrap node by adding the `.WithBootstrapNode(...)` option.
|
||||
1. When using `AutoBootstrapDistTest`:
|
||||
1. The test-infra creates the bootstrap node for you, and automatically passes it to each Codex node you create in your tests. Handy for keeping your tests clean and to-the-point.
|
||||
1. When using the auto-bootstrap, you have no control over the bootstrap node from your tests. You can't (for example) shut it down during the course of the test. If you need this level of control for your scenario, use the `DistTest` instead.
|
||||
1. You can generate files of random test data by calling `GenerateTestFile(...)`.
|
||||
1. If your test needs a long time to run, add the `[UseLongTimeouts]` function attribute. This will greatly increase maximum time-out values for operations like for example uploading and downloading files.
|
||||
1. You can enable access to the Codex node metrics by adding the option `.EnableMetrics()`. Enabling metrics will make the test-infra download and save all Codex metrics in case of a test failure. (The metrics are stored as CSV, in the same location as the test log file.)
|
||||
1. You can enable access to the blockchain marketplace by adding the option `.EnableMarketplace(...)`.
|
||||
1. Enabling metrics and/or enabling the marketplace takes extra resources from the test-infra and increases the time needed during Codex node setup. Please don't enable these features unless your tests need them.
|
||||
1. Tip: Codex nodes can be named. Use the option `WithName(...)` and make reading your test logs a little nicer!
|
||||
1. Tip: Commit often.
|
||||
1. Once you're happy with your tests, please create a pull-request and ask (another) Codex core contributor to review your changes.
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public class CodexDebugResponse
|
||||
{
|
||||
public string id { get; set; } = string.Empty;
|
||||
public string[] addrs { get; set; } = new string[0];
|
||||
public string repo { get; set; } = string.Empty;
|
||||
public string spr { get; set; } = string.Empty;
|
||||
public CodexDebugVersionResponse codex { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CodexDebugVersionResponse
|
||||
{
|
||||
public string version { get; set; } = string.Empty;
|
||||
public string revision { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<RootNamespace>CodexDistTestCore</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="KubernetesClient" Version="10.1.4" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,75 +0,0 @@
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public class CodexNodeContainer
|
||||
{
|
||||
public CodexNodeContainer(string name, int servicePort, string servicePortName, int apiPort, string containerPortName, int discoveryPort, int listenPort, string dataDir)
|
||||
{
|
||||
Name = name;
|
||||
ServicePort = servicePort;
|
||||
ServicePortName = servicePortName;
|
||||
ApiPort = apiPort;
|
||||
ContainerPortName = containerPortName;
|
||||
DiscoveryPort = discoveryPort;
|
||||
ListenPort = listenPort;
|
||||
DataDir = dataDir;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public int ServicePort { get; }
|
||||
public string ServicePortName { get; }
|
||||
public int ApiPort { get; }
|
||||
public string ContainerPortName { get; }
|
||||
public int DiscoveryPort { get; }
|
||||
public int ListenPort { get; }
|
||||
public string DataDir { get; }
|
||||
}
|
||||
|
||||
public class CodexGroupNumberSource
|
||||
{
|
||||
private readonly NumberSource codexNodeGroupNumberSource = new NumberSource(0);
|
||||
private readonly NumberSource groupContainerNameSource = new NumberSource(1);
|
||||
private readonly NumberSource servicePortSource = new NumberSource(30001);
|
||||
|
||||
public int GetNextCodexNodeGroupNumber()
|
||||
{
|
||||
return codexNodeGroupNumberSource.GetNextNumber();
|
||||
}
|
||||
|
||||
public string GetNextServicePortName()
|
||||
{
|
||||
return $"node{groupContainerNameSource.GetNextNumber()}";
|
||||
}
|
||||
|
||||
public int GetNextServicePort()
|
||||
{
|
||||
return servicePortSource.GetNextNumber();
|
||||
}
|
||||
}
|
||||
|
||||
public class CodexNodeContainerFactory
|
||||
{
|
||||
private readonly NumberSource containerNameSource = new NumberSource(1);
|
||||
private readonly NumberSource codexPortSource = new NumberSource(8080);
|
||||
private readonly CodexGroupNumberSource groupContainerFactory;
|
||||
|
||||
public CodexNodeContainerFactory(CodexGroupNumberSource groupContainerFactory)
|
||||
{
|
||||
this.groupContainerFactory = groupContainerFactory;
|
||||
}
|
||||
|
||||
public CodexNodeContainer CreateNext()
|
||||
{
|
||||
var n = containerNameSource.GetNextNumber();
|
||||
return new CodexNodeContainer(
|
||||
name: $"codex-node{n}",
|
||||
servicePort: groupContainerFactory.GetNextServicePort(),
|
||||
servicePortName: groupContainerFactory.GetNextServicePortName(),
|
||||
apiPort: codexPortSource.GetNextNumber(),
|
||||
containerPortName: $"api-{n}",
|
||||
discoveryPort: codexPortSource.GetNextNumber(),
|
||||
listenPort: codexPortSource.GetNextNumber(),
|
||||
dataDir: $"datadir{n}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
using CodexDistTestCore.Config;
|
||||
using k8s.Models;
|
||||
using System.Collections;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public interface ICodexNodeGroup : IEnumerable<IOnlineCodexNode>
|
||||
{
|
||||
IOfflineCodexNodes BringOffline();
|
||||
IOnlineCodexNode this[int index] { get; }
|
||||
}
|
||||
|
||||
public class CodexNodeGroup : ICodexNodeGroup
|
||||
{
|
||||
private readonly TestLog log;
|
||||
private readonly IK8sManager k8SManager;
|
||||
|
||||
public CodexNodeGroup(TestLog log, int orderNumber, OfflineCodexNodes origin, IK8sManager k8SManager, OnlineCodexNode[] nodes)
|
||||
{
|
||||
this.log = log;
|
||||
OrderNumber = orderNumber;
|
||||
Origin = origin;
|
||||
this.k8SManager = k8SManager;
|
||||
Nodes = nodes;
|
||||
|
||||
foreach (var n in nodes) n.Group = this;
|
||||
}
|
||||
|
||||
public IOnlineCodexNode this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return Nodes[index];
|
||||
}
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes BringOffline()
|
||||
{
|
||||
return k8SManager.BringOffline(this);
|
||||
}
|
||||
|
||||
public int OrderNumber { get; }
|
||||
public OfflineCodexNodes Origin { get; }
|
||||
public OnlineCodexNode[] Nodes { get; }
|
||||
public V1Deployment? Deployment { get; set; }
|
||||
public V1Service? Service { get; set; }
|
||||
public PodInfo? PodInfo { get; set; }
|
||||
|
||||
public CodexNodeContainer[] GetContainers()
|
||||
{
|
||||
return Nodes.Select(n => n.Container).ToArray();
|
||||
}
|
||||
|
||||
public IEnumerator<IOnlineCodexNode> GetEnumerator()
|
||||
{
|
||||
return Nodes.Cast<IOnlineCodexNode>().GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return Nodes.GetEnumerator();
|
||||
}
|
||||
|
||||
public V1ObjectMeta GetServiceMetadata()
|
||||
{
|
||||
return new V1ObjectMeta
|
||||
{
|
||||
Name = "codex-test-entrypoint-" + OrderNumber,
|
||||
NamespaceProperty = K8sCluster.K8sNamespace
|
||||
};
|
||||
}
|
||||
|
||||
public V1ObjectMeta GetDeploymentMetadata()
|
||||
{
|
||||
return new V1ObjectMeta
|
||||
{
|
||||
Name = "codex-test-node-" + OrderNumber,
|
||||
NamespaceProperty = K8sCluster.K8sNamespace
|
||||
};
|
||||
}
|
||||
|
||||
public CodexNodeLog DownloadLog(IOnlineCodexNode node)
|
||||
{
|
||||
var logDownloader = new PodLogDownloader(log, k8SManager);
|
||||
var n = (OnlineCodexNode)node;
|
||||
return logDownloader.DownloadLog(n);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetSelector()
|
||||
{
|
||||
return new Dictionary<string, string> { { "codex-test-node", "dist-test-" + OrderNumber } };
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return $"CodexNodeGroup#{OrderNumber}-{Origin.Describe()}";
|
||||
}
|
||||
}
|
||||
|
||||
public class PodInfo
|
||||
{
|
||||
public PodInfo(string name, string ip)
|
||||
{
|
||||
Name = name;
|
||||
Ip = ip;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Ip { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public class CodexNodeLog
|
||||
{
|
||||
private readonly LogFile logFile;
|
||||
|
||||
public CodexNodeLog(LogFile logFile)
|
||||
{
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
public void AssertLogContains(string expectedString)
|
||||
{
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (line.Contains(expectedString)) return;
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
|
||||
Assert.Fail($"Unable to find string '{expectedString}' in CodexNode log file {logFile.FilenameWithoutPath}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using k8s.Models;
|
||||
|
||||
namespace CodexDistTestCore.Config
|
||||
{
|
||||
public class CodexDockerImage
|
||||
{
|
||||
public string GetImageTag()
|
||||
{
|
||||
return "thatbenbierens/nim-codex:sha-b204837";
|
||||
}
|
||||
|
||||
public string GetExpectedImageRevision()
|
||||
{
|
||||
return "b20483";
|
||||
}
|
||||
|
||||
public List<V1EnvVar> CreateEnvironmentVariables(OfflineCodexNodes node, CodexNodeContainer environment)
|
||||
{
|
||||
var formatter = new EnvFormatter();
|
||||
formatter.Create(node, environment);
|
||||
return formatter.Result;
|
||||
}
|
||||
|
||||
private class EnvFormatter
|
||||
{
|
||||
public List<V1EnvVar> Result { get; } = new List<V1EnvVar>();
|
||||
|
||||
public void Create(OfflineCodexNodes node, CodexNodeContainer environment)
|
||||
{
|
||||
AddVar("API_PORT", environment.ApiPort.ToString());
|
||||
AddVar("DATA_DIR", environment.DataDir);
|
||||
AddVar("DISC_PORT", environment.DiscoveryPort.ToString());
|
||||
AddVar("LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{environment.ListenPort}");
|
||||
|
||||
if (node.BootstrapNode != null)
|
||||
{
|
||||
var debugInfo = node.BootstrapNode.GetDebugInfo();
|
||||
AddVar("BOOTSTRAP_SPR", debugInfo.spr);
|
||||
}
|
||||
if (node.LogLevel != null)
|
||||
{
|
||||
AddVar("LOG_LEVEL", node.LogLevel.ToString()!.ToUpperInvariant());
|
||||
}
|
||||
if (node.StorageQuota != null)
|
||||
{
|
||||
AddVar("STORAGE_QUOTA", node.StorageQuota.SizeInBytes.ToString()!);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddVar(string key, string value)
|
||||
{
|
||||
Result.Add(new V1EnvVar
|
||||
{
|
||||
Name = key,
|
||||
Value = value
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace CodexDistTestCore.Config
|
||||
{
|
||||
public class FileManagerConfig
|
||||
{
|
||||
public const string Folder = "TestDataFiles";
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using k8s;
|
||||
|
||||
namespace CodexDistTestCore.Config
|
||||
{
|
||||
public class K8sCluster
|
||||
{
|
||||
public const string K8sNamespace = "codex-test-namespace";
|
||||
private const string KubeConfigFile = "C:\\kube\\config";
|
||||
private readonly Dictionary<Location, string> K8sNodeLocationMap = new Dictionary<Location, string>
|
||||
{
|
||||
{ Location.BensLaptop, "worker01" },
|
||||
{ Location.BensOldGamingMachine, "worker02" },
|
||||
};
|
||||
|
||||
private KubernetesClientConfiguration? config;
|
||||
|
||||
public KubernetesClientConfiguration GetK8sClientConfig()
|
||||
{
|
||||
if (config != null) return config;
|
||||
config = KubernetesClientConfiguration.BuildConfigFromConfigFile(KubeConfigFile);
|
||||
return config;
|
||||
}
|
||||
|
||||
public string GetIp()
|
||||
{
|
||||
var c = GetK8sClientConfig();
|
||||
|
||||
var host = c.Host.Replace("https://", "");
|
||||
|
||||
return host.Substring(0, host.IndexOf(':'));
|
||||
}
|
||||
|
||||
public string GetNodeLabelForLocation(Location location)
|
||||
{
|
||||
if (location == Location.Unspecified) return string.Empty;
|
||||
return K8sNodeLocationMap[location];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace CodexDistTestCore.Config
|
||||
{
|
||||
public class LogConfig
|
||||
{
|
||||
public const string LogRoot = "D:/CodexTestLogs";
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
using CodexDistTestCore.Config;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
[SetUpFixture]
|
||||
public abstract class DistTest
|
||||
{
|
||||
private TestLog log = null!;
|
||||
private FileManager fileManager = null!;
|
||||
private K8sManager k8sManager = null!;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
// Previous test run may have been interrupted.
|
||||
// Begin by cleaning everything up.
|
||||
log = new TestLog();
|
||||
fileManager = new FileManager(log);
|
||||
k8sManager = new K8sManager(log, fileManager);
|
||||
|
||||
try
|
||||
{
|
||||
k8sManager.DeleteAllResources();
|
||||
fileManager.DeleteAllTestFiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalTestFailure.HasFailed = true;
|
||||
log.Error($"Global setup cleanup failed with: {ex}");
|
||||
throw;
|
||||
}
|
||||
log.Log("Global setup cleanup successful");
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUpDistTest()
|
||||
{
|
||||
if (GlobalTestFailure.HasFailed)
|
||||
{
|
||||
Assert.Inconclusive("Skip test: Previous test failed during clean up.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var dockerImage = new CodexDockerImage();
|
||||
log = new TestLog();
|
||||
log.Log($"Using docker image '{dockerImage.GetImageTag()}'");
|
||||
|
||||
fileManager = new FileManager(log);
|
||||
k8sManager = new K8sManager(log, fileManager);
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDownDistTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
log.EndTest();
|
||||
IncludeLogsOnTestFailure();
|
||||
k8sManager.DeleteAllResources();
|
||||
fileManager.DeleteAllTestFiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error("Cleanup failed: " + ex.Message);
|
||||
GlobalTestFailure.HasFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public TestFile GenerateTestFile(ByteSize size)
|
||||
{
|
||||
return fileManager.GenerateTestFile(size);
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes SetupCodexNodes(int numberOfNodes)
|
||||
{
|
||||
return new OfflineCodexNodes(k8sManager, numberOfNodes);
|
||||
}
|
||||
|
||||
private void IncludeLogsOnTestFailure()
|
||||
{
|
||||
var result = TestContext.CurrentContext.Result;
|
||||
if (result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
|
||||
{
|
||||
if (IsDownloadingLogsEnabled())
|
||||
{
|
||||
log.Log("Downloading all CodexNode logs because of test failure...");
|
||||
k8sManager.ForEachOnlineGroup(DownloadLogs);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Log("Skipping download of all CodexNode logs due to [DontDownloadLogsOnFailure] attribute.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DownloadLogs(CodexNodeGroup group)
|
||||
{
|
||||
foreach (var node in group)
|
||||
{
|
||||
var downloader = new PodLogDownloader(log, k8sManager);
|
||||
var n = (OnlineCodexNode)node;
|
||||
downloader.DownloadLog(n);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsDownloadingLogsEnabled()
|
||||
{
|
||||
var testProperties = TestContext.CurrentContext.Test.Properties;
|
||||
return !testProperties.ContainsKey(PodLogDownloader.DontDownloadLogsOnFailureKey);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GlobalTestFailure
|
||||
{
|
||||
public static bool HasFailed { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using CodexDistTestCore.Config;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public interface IFileManager
|
||||
{
|
||||
TestFile CreateEmptyTestFile();
|
||||
TestFile GenerateTestFile(ByteSize size);
|
||||
void DeleteAllTestFiles();
|
||||
}
|
||||
|
||||
public class FileManager : IFileManager
|
||||
{
|
||||
public const int ChunkSize = 1024 * 1024;
|
||||
private readonly Random random = new Random();
|
||||
private readonly List<TestFile> activeFiles = new List<TestFile>();
|
||||
private readonly TestLog log;
|
||||
|
||||
public FileManager(TestLog log)
|
||||
{
|
||||
if (!Directory.Exists(FileManagerConfig.Folder)) Directory.CreateDirectory(FileManagerConfig.Folder);
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public TestFile CreateEmptyTestFile()
|
||||
{
|
||||
var result = new TestFile(Path.Combine(FileManagerConfig.Folder, Guid.NewGuid().ToString() + "_test.bin"));
|
||||
File.Create(result.Filename).Close();
|
||||
activeFiles.Add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public TestFile GenerateTestFile(ByteSize size)
|
||||
{
|
||||
var result = CreateEmptyTestFile();
|
||||
GenerateFileBytes(result, size);
|
||||
log.Log($"Generated {size.SizeInBytes} bytes of content for file '{result.Filename}'.");
|
||||
return result;
|
||||
}
|
||||
|
||||
public void DeleteAllTestFiles()
|
||||
{
|
||||
foreach (var file in activeFiles) File.Delete(file.Filename);
|
||||
activeFiles.Clear();
|
||||
}
|
||||
|
||||
private void GenerateFileBytes(TestFile result, ByteSize size)
|
||||
{
|
||||
long bytesLeft = size.SizeInBytes;
|
||||
while (bytesLeft > 0)
|
||||
{
|
||||
var length = Math.Min(bytesLeft, ChunkSize);
|
||||
AppendRandomBytesToFile(result, length);
|
||||
bytesLeft -= length;
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendRandomBytesToFile(TestFile result, long length)
|
||||
{
|
||||
var bytes = new byte[length];
|
||||
random.NextBytes(bytes);
|
||||
using var stream = new FileStream(result.Filename, FileMode.Append);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestFile
|
||||
{
|
||||
public TestFile(string filename)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
|
||||
public string Filename { get; }
|
||||
|
||||
public long GetFileSize()
|
||||
{
|
||||
var info = new FileInfo(Filename);
|
||||
return info.Length;
|
||||
}
|
||||
|
||||
public void AssertIsEqual(TestFile? other)
|
||||
{
|
||||
if (other == null) Assert.Fail("TestFile is null.");
|
||||
if (other == this || other!.Filename == Filename) Assert.Fail("TestFile is compared to itself.");
|
||||
|
||||
using var stream1 = new FileStream(Filename, FileMode.Open, FileAccess.Read);
|
||||
using var stream2 = new FileStream(other.Filename, FileMode.Open, FileAccess.Read);
|
||||
|
||||
var bytes1 = new byte[FileManager.ChunkSize];
|
||||
var bytes2 = new byte[FileManager.ChunkSize];
|
||||
|
||||
var read1 = 0;
|
||||
var read2 = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
read1 = stream1.Read(bytes1, 0, FileManager.ChunkSize);
|
||||
read2 = stream2.Read(bytes2, 0, FileManager.ChunkSize);
|
||||
|
||||
if (read1 == 0 && read2 == 0) return;
|
||||
Assert.That(read1, Is.EqualTo(read2), "Files are not of equal length.");
|
||||
CollectionAssert.AreEqual(bytes1, bytes2, "Files are not binary-equal.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public class Http
|
||||
{
|
||||
private readonly string ip;
|
||||
private readonly int port;
|
||||
private readonly string baseUrl;
|
||||
|
||||
public Http(string ip, int port, string baseUrl)
|
||||
{
|
||||
this.ip = ip;
|
||||
this.port = port;
|
||||
this.baseUrl = baseUrl;
|
||||
|
||||
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
|
||||
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
|
||||
}
|
||||
|
||||
public string HttpGetString(string route)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
using var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
var result = Utils.Wait(client.GetAsync(url));
|
||||
return Utils.Wait(result.Content.ReadAsStringAsync());
|
||||
});
|
||||
}
|
||||
|
||||
public T HttpGetJson<T>(string route)
|
||||
{
|
||||
return JsonConvert.DeserializeObject<T>(HttpGetString(route))!;
|
||||
}
|
||||
|
||||
public string HttpPostStream(string route, Stream stream)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
using var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
|
||||
var content = new StreamContent(stream);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
var response = Utils.Wait(client.PostAsync(url, content));
|
||||
|
||||
return Utils.Wait(response.Content.ReadAsStringAsync());
|
||||
});
|
||||
}
|
||||
|
||||
public Stream HttpGetStream(string route)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
|
||||
return Utils.Wait(client.GetStreamAsync(url));
|
||||
});
|
||||
}
|
||||
|
||||
private string GetUrl()
|
||||
{
|
||||
return $"http://{ip}:{port}{baseUrl}";
|
||||
}
|
||||
|
||||
private static T Retry<T>(Func<T> operation)
|
||||
{
|
||||
var retryCounter = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return operation();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Timing.HttpCallRetryDelay();
|
||||
retryCounter++;
|
||||
if (retryCounter > Timing.HttpCallRetryCount())
|
||||
{
|
||||
Assert.Fail(exception.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient GetClient()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
client.Timeout = Timing.HttpCallTimeout();
|
||||
return client;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public interface IK8sManager
|
||||
{
|
||||
ICodexNodeGroup BringOnline(OfflineCodexNodes node);
|
||||
IOfflineCodexNodes BringOffline(ICodexNodeGroup node);
|
||||
void FetchPodLog(OnlineCodexNode node, IPodLogHandler logHandler);
|
||||
}
|
||||
|
||||
public class K8sManager : IK8sManager
|
||||
{
|
||||
private readonly CodexGroupNumberSource codexGroupNumberSource = new CodexGroupNumberSource();
|
||||
private readonly List<CodexNodeGroup> onlineCodexNodeGroups = new List<CodexNodeGroup>();
|
||||
private readonly KnownK8sPods knownPods = new KnownK8sPods();
|
||||
private readonly TestLog log;
|
||||
private readonly IFileManager fileManager;
|
||||
|
||||
public K8sManager(TestLog log, IFileManager fileManager)
|
||||
{
|
||||
this.log = log;
|
||||
this.fileManager = fileManager;
|
||||
}
|
||||
|
||||
public ICodexNodeGroup BringOnline(OfflineCodexNodes offline)
|
||||
{
|
||||
var online = CreateOnlineCodexNodes(offline);
|
||||
|
||||
K8s(k => k.BringOnline(online, offline));
|
||||
|
||||
log.Log($"{online.Describe()} online.");
|
||||
|
||||
return online;
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes BringOffline(ICodexNodeGroup node)
|
||||
{
|
||||
var online = GetAndRemoveActiveNodeFor(node);
|
||||
|
||||
K8s(k => k.BringOffline(online));
|
||||
|
||||
log.Log($"{online.Describe()} offline.");
|
||||
|
||||
return online.Origin;
|
||||
}
|
||||
|
||||
public void DeleteAllResources()
|
||||
{
|
||||
K8s(k => k.DeleteAllResources());
|
||||
}
|
||||
|
||||
public void ForEachOnlineGroup(Action<CodexNodeGroup> action)
|
||||
{
|
||||
foreach (var group in onlineCodexNodeGroups) action(group);
|
||||
}
|
||||
|
||||
public void FetchPodLog(OnlineCodexNode node, IPodLogHandler logHandler)
|
||||
{
|
||||
K8s(k => k.FetchPodLog(node, logHandler));
|
||||
}
|
||||
|
||||
private CodexNodeGroup CreateOnlineCodexNodes(OfflineCodexNodes offline)
|
||||
{
|
||||
var containers = CreateContainers(offline.NumberOfNodes);
|
||||
var online = containers.Select(c => new OnlineCodexNode(log, fileManager, c)).ToArray();
|
||||
var result = new CodexNodeGroup(log, codexGroupNumberSource.GetNextCodexNodeGroupNumber(), offline, this, online);
|
||||
onlineCodexNodeGroups.Add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private CodexNodeContainer[] CreateContainers(int number)
|
||||
{
|
||||
var factory = new CodexNodeContainerFactory(codexGroupNumberSource);
|
||||
var containers = new List<CodexNodeContainer>();
|
||||
for (var i = 0; i < number; i++) containers.Add(factory.CreateNext());
|
||||
return containers.ToArray();
|
||||
}
|
||||
|
||||
private CodexNodeGroup GetAndRemoveActiveNodeFor(ICodexNodeGroup node)
|
||||
{
|
||||
var n = (CodexNodeGroup)node;
|
||||
onlineCodexNodeGroups.Remove(n);
|
||||
return n;
|
||||
}
|
||||
|
||||
private void K8s(Action<K8sOperations> action)
|
||||
{
|
||||
var k8s = new K8sOperations(knownPods);
|
||||
action(k8s);
|
||||
k8s.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
using CodexDistTestCore.Config;
|
||||
using k8s;
|
||||
using k8s.Models;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public class K8sOperations
|
||||
{
|
||||
private readonly CodexDockerImage dockerImage = new CodexDockerImage();
|
||||
private readonly K8sCluster k8sCluster = new K8sCluster();
|
||||
private readonly Kubernetes client;
|
||||
private readonly KnownK8sPods knownPods;
|
||||
|
||||
public K8sOperations(KnownK8sPods knownPods)
|
||||
{
|
||||
this.knownPods = knownPods;
|
||||
|
||||
client = new Kubernetes(k8sCluster.GetK8sClientConfig());
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
public void BringOnline(CodexNodeGroup online, OfflineCodexNodes offline)
|
||||
{
|
||||
EnsureTestNamespace();
|
||||
|
||||
CreateDeployment(online, offline);
|
||||
CreateService(online);
|
||||
|
||||
WaitUntilOnline(online);
|
||||
FetchPodInfo(online);
|
||||
}
|
||||
|
||||
public void BringOffline(CodexNodeGroup online)
|
||||
{
|
||||
var deploymentName = online.Deployment.Name();
|
||||
DeleteDeployment(online);
|
||||
DeleteService(online);
|
||||
WaitUntilOffline(deploymentName);
|
||||
}
|
||||
|
||||
public void DeleteAllResources()
|
||||
{
|
||||
DeleteNamespace();
|
||||
|
||||
WaitUntilZeroPods();
|
||||
WaitUntilNamespaceDeleted();
|
||||
}
|
||||
|
||||
public void FetchPodLog(OnlineCodexNode node, IPodLogHandler logHandler)
|
||||
{
|
||||
var stream = client.ReadNamespacedPodLog(node.Group.PodInfo!.Name, K8sNamespace, node.Container.Name);
|
||||
logHandler.Log(stream);
|
||||
}
|
||||
|
||||
private void FetchPodInfo(CodexNodeGroup online)
|
||||
{
|
||||
var pods = client.ListNamespacedPod(K8sNamespace).Items;
|
||||
|
||||
var newPods = pods.Where(p => !knownPods.Contains(p.Name())).ToArray();
|
||||
Assert.That(newPods.Length, Is.EqualTo(1), "Expected only 1 pod to be created. Test infra failure.");
|
||||
|
||||
var newPod = newPods.Single();
|
||||
online.PodInfo = new PodInfo(newPod.Name(), newPod.Status.PodIP);
|
||||
|
||||
Assert.That(!string.IsNullOrEmpty(online.PodInfo.Name), "Invalid pod name received. Test infra failure.");
|
||||
Assert.That(!string.IsNullOrEmpty(online.PodInfo.Ip), "Invalid pod IP received. Test infra failure.");
|
||||
|
||||
knownPods.Add(newPod.Name());
|
||||
}
|
||||
|
||||
#region Waiting
|
||||
|
||||
private void WaitUntilOnline(CodexNodeGroup online)
|
||||
{
|
||||
WaitUntil(() =>
|
||||
{
|
||||
online.Deployment = client.ReadNamespacedDeployment(online.Deployment.Name(), K8sNamespace);
|
||||
return online.Deployment?.Status.AvailableReplicas != null && online.Deployment.Status.AvailableReplicas > 0;
|
||||
});
|
||||
}
|
||||
|
||||
private void WaitUntilOffline(string deploymentName)
|
||||
{
|
||||
WaitUntil(() =>
|
||||
{
|
||||
var deployment = client.ReadNamespacedDeployment(deploymentName, K8sNamespace);
|
||||
return deployment == null || deployment.Status.AvailableReplicas == 0;
|
||||
});
|
||||
}
|
||||
|
||||
private void WaitUntilZeroPods()
|
||||
{
|
||||
WaitUntil(() => !client.ListNamespacedPod(K8sNamespace).Items.Any());
|
||||
}
|
||||
|
||||
private void WaitUntilNamespaceDeleted()
|
||||
{
|
||||
WaitUntil(() => !IsTestNamespaceOnline());
|
||||
}
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var state = predicate();
|
||||
while (!state)
|
||||
{
|
||||
if (DateTime.UtcNow - start > Timing.K8sOperationTimeout())
|
||||
{
|
||||
Assert.Fail("K8s operation timed out.");
|
||||
throw new TimeoutException();
|
||||
}
|
||||
|
||||
Timing.WaitForK8sServiceDelay();
|
||||
state = predicate();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Service management
|
||||
|
||||
private void CreateService(CodexNodeGroup online)
|
||||
{
|
||||
var serviceSpec = new V1Service
|
||||
{
|
||||
ApiVersion = "v1",
|
||||
Metadata = online.GetServiceMetadata(),
|
||||
Spec = new V1ServiceSpec
|
||||
{
|
||||
Type = "NodePort",
|
||||
Selector = online.GetSelector(),
|
||||
Ports = CreateServicePorts(online)
|
||||
}
|
||||
};
|
||||
|
||||
online.Service = client.CreateNamespacedService(serviceSpec, K8sNamespace);
|
||||
}
|
||||
|
||||
private List<V1ServicePort> CreateServicePorts(CodexNodeGroup online)
|
||||
{
|
||||
var result = new List<V1ServicePort>();
|
||||
var containers = online.GetContainers();
|
||||
foreach (var container in containers)
|
||||
{
|
||||
result.Add(new V1ServicePort
|
||||
{
|
||||
Name = container.ServicePortName,
|
||||
Protocol = "TCP",
|
||||
Port = container.ApiPort,
|
||||
TargetPort = container.ContainerPortName,
|
||||
NodePort = container.ServicePort
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DeleteService(CodexNodeGroup online)
|
||||
{
|
||||
if (online.Service == null) return;
|
||||
client.DeleteNamespacedService(online.Service.Name(), K8sNamespace);
|
||||
online.Service = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Deployment management
|
||||
|
||||
private void CreateDeployment(CodexNodeGroup online, OfflineCodexNodes offline)
|
||||
{
|
||||
var deploymentSpec = new V1Deployment
|
||||
{
|
||||
ApiVersion = "apps/v1",
|
||||
Metadata = online.GetDeploymentMetadata(),
|
||||
Spec = new V1DeploymentSpec
|
||||
{
|
||||
Replicas = 1,
|
||||
Selector = new V1LabelSelector
|
||||
{
|
||||
MatchLabels = online.GetSelector()
|
||||
},
|
||||
Template = new V1PodTemplateSpec
|
||||
{
|
||||
Metadata = new V1ObjectMeta
|
||||
{
|
||||
Labels = online.GetSelector()
|
||||
},
|
||||
Spec = new V1PodSpec
|
||||
{
|
||||
NodeSelector = CreateNodeSelector(offline),
|
||||
Containers = CreateDeploymentContainers(online, offline)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
online.Deployment = client.CreateNamespacedDeployment(deploymentSpec, K8sNamespace);
|
||||
}
|
||||
|
||||
private IDictionary<string, string> CreateNodeSelector(OfflineCodexNodes offline)
|
||||
{
|
||||
if (offline.Location == Location.Unspecified) return new Dictionary<string, string>();
|
||||
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "codex-test-location", k8sCluster.GetNodeLabelForLocation(offline.Location) }
|
||||
};
|
||||
}
|
||||
|
||||
private List<V1Container> CreateDeploymentContainers(CodexNodeGroup online, OfflineCodexNodes offline)
|
||||
{
|
||||
var result = new List<V1Container>();
|
||||
var containers = online.GetContainers();
|
||||
foreach (var container in containers)
|
||||
{
|
||||
result.Add(new V1Container
|
||||
{
|
||||
Name = container.Name,
|
||||
Image = dockerImage.GetImageTag(),
|
||||
Ports = new List<V1ContainerPort>
|
||||
{
|
||||
new V1ContainerPort
|
||||
{
|
||||
ContainerPort = container.ApiPort,
|
||||
Name = container.ContainerPortName
|
||||
}
|
||||
},
|
||||
Env = dockerImage.CreateEnvironmentVariables(offline, container)
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void DeleteDeployment(CodexNodeGroup online)
|
||||
{
|
||||
if (online.Deployment == null) return;
|
||||
client.DeleteNamespacedDeployment(online.Deployment.Name(), K8sNamespace);
|
||||
online.Deployment = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Namespace management
|
||||
|
||||
private void EnsureTestNamespace()
|
||||
{
|
||||
if (IsTestNamespaceOnline()) return;
|
||||
|
||||
var namespaceSpec = new V1Namespace
|
||||
{
|
||||
ApiVersion = "v1",
|
||||
Metadata = new V1ObjectMeta
|
||||
{
|
||||
Name = K8sNamespace,
|
||||
Labels = new Dictionary<string, string> { { "name", K8sNamespace } }
|
||||
}
|
||||
};
|
||||
client.CreateNamespace(namespaceSpec);
|
||||
}
|
||||
|
||||
private void DeleteNamespace()
|
||||
{
|
||||
if (IsTestNamespaceOnline())
|
||||
{
|
||||
client.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0);
|
||||
}
|
||||
}
|
||||
|
||||
private string K8sNamespace
|
||||
{
|
||||
get { return K8sCluster.K8sNamespace; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool IsTestNamespaceOnline()
|
||||
{
|
||||
return client.ListNamespace().Items.Any(n => n.Metadata.Name == K8sNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public interface IOfflineCodexNodes
|
||||
{
|
||||
IOfflineCodexNodes At(Location location);
|
||||
IOfflineCodexNodes WithLogLevel(CodexLogLevel level);
|
||||
IOfflineCodexNodes WithBootstrapNode(IOnlineCodexNode node);
|
||||
IOfflineCodexNodes WithStorageQuota(ByteSize storageQuota);
|
||||
ICodexNodeGroup BringOnline();
|
||||
}
|
||||
|
||||
public enum CodexLogLevel
|
||||
{
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error
|
||||
}
|
||||
|
||||
public enum Location
|
||||
{
|
||||
Unspecified,
|
||||
BensLaptop,
|
||||
BensOldGamingMachine,
|
||||
}
|
||||
|
||||
public class OfflineCodexNodes : IOfflineCodexNodes
|
||||
{
|
||||
private readonly IK8sManager k8SManager;
|
||||
|
||||
public int NumberOfNodes { get; }
|
||||
public Location Location { get; private set; }
|
||||
public CodexLogLevel? LogLevel { get; private set; }
|
||||
public IOnlineCodexNode? BootstrapNode { get; private set; }
|
||||
public ByteSize? StorageQuota { get; private set; }
|
||||
|
||||
public OfflineCodexNodes(IK8sManager k8SManager, int numberOfNodes)
|
||||
{
|
||||
this.k8SManager = k8SManager;
|
||||
NumberOfNodes = numberOfNodes;
|
||||
Location = Location.Unspecified;
|
||||
}
|
||||
|
||||
public ICodexNodeGroup BringOnline()
|
||||
{
|
||||
return k8SManager.BringOnline(this);
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes At(Location location)
|
||||
{
|
||||
Location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes WithBootstrapNode(IOnlineCodexNode node)
|
||||
{
|
||||
BootstrapNode = node;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes WithLogLevel(CodexLogLevel level)
|
||||
{
|
||||
LogLevel = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IOfflineCodexNodes WithStorageQuota(ByteSize storageQuota)
|
||||
{
|
||||
StorageQuota = storageQuota;
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
var args = string.Join(',', DescribeArgs());
|
||||
return $"{NumberOfNodes} CodexNodes with [{args}]";
|
||||
}
|
||||
|
||||
private IEnumerable<string> DescribeArgs()
|
||||
{
|
||||
if (LogLevel != null) yield return ($"LogLevel={LogLevel}");
|
||||
if (BootstrapNode != null) yield return ("BootstrapNode=set");
|
||||
if (StorageQuota != null) yield return ($"StorageQuote={StorageQuota.SizeInBytes}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
using CodexDistTestCore.Config;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public interface IOnlineCodexNode
|
||||
{
|
||||
CodexDebugResponse GetDebugInfo();
|
||||
ContentId UploadFile(TestFile file);
|
||||
TestFile? DownloadContent(ContentId contentId);
|
||||
void ConnectToPeer(IOnlineCodexNode node);
|
||||
CodexNodeLog DownloadLog();
|
||||
}
|
||||
|
||||
public class OnlineCodexNode : IOnlineCodexNode
|
||||
{
|
||||
private const string SuccessfullyConnectedMessage = "Successfully connected to peer";
|
||||
private const string UploadFailedMessage = "Unable to store block";
|
||||
|
||||
private readonly K8sCluster k8sCluster = new K8sCluster();
|
||||
private readonly TestLog log;
|
||||
private readonly IFileManager fileManager;
|
||||
|
||||
public OnlineCodexNode(TestLog log, IFileManager fileManager, CodexNodeContainer container)
|
||||
{
|
||||
this.log = log;
|
||||
this.fileManager = fileManager;
|
||||
Container = container;
|
||||
}
|
||||
|
||||
public CodexNodeContainer Container { get; }
|
||||
public CodexNodeGroup Group { get; internal set; } = null!;
|
||||
|
||||
public string GetName()
|
||||
{
|
||||
return $"<{Container.Name}>";
|
||||
}
|
||||
|
||||
public CodexDebugResponse GetDebugInfo()
|
||||
{
|
||||
var response = Http().HttpGetJson<CodexDebugResponse>("debug/info");
|
||||
Log($"Got DebugInfo with id: '{response.id}'.");
|
||||
return response;
|
||||
}
|
||||
|
||||
public ContentId UploadFile(TestFile file)
|
||||
{
|
||||
Log($"Uploading file of size {file.GetFileSize()}...");
|
||||
using var fileStream = File.OpenRead(file.Filename);
|
||||
var response = Http().HttpPostStream("upload", fileStream);
|
||||
if (response.StartsWith(UploadFailedMessage))
|
||||
{
|
||||
Assert.Fail("Node failed to store block.");
|
||||
}
|
||||
Log($"Uploaded file. Received contentId: '{response}'.");
|
||||
return new ContentId(response);
|
||||
}
|
||||
|
||||
public TestFile? DownloadContent(ContentId contentId)
|
||||
{
|
||||
Log($"Downloading for contentId: '{contentId.Id}'...");
|
||||
var file = fileManager.CreateEmptyTestFile();
|
||||
DownloadToFile(contentId.Id, file);
|
||||
Log($"Downloaded file of size {file.GetFileSize()} to '{file.Filename}'.");
|
||||
return file;
|
||||
}
|
||||
|
||||
public void ConnectToPeer(IOnlineCodexNode node)
|
||||
{
|
||||
var peer = (OnlineCodexNode)node;
|
||||
|
||||
Log($"Connecting to peer {peer.GetName()}...");
|
||||
var peerInfo = node.GetDebugInfo();
|
||||
var peerId = peerInfo.id;
|
||||
var peerMultiAddress = GetPeerMultiAddress(peer, peerInfo);
|
||||
|
||||
var response = Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
|
||||
|
||||
Assert.That(response, Is.EqualTo(SuccessfullyConnectedMessage), "Unable to connect codex nodes.");
|
||||
Log($"Successfully connected to peer {peer.GetName()}.");
|
||||
}
|
||||
|
||||
public CodexNodeLog DownloadLog()
|
||||
{
|
||||
return Group.DownloadLog(this);
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return $"{Group.Describe()} contains {GetName()}";
|
||||
}
|
||||
|
||||
private string GetPeerMultiAddress(OnlineCodexNode peer, CodexDebugResponse peerInfo)
|
||||
{
|
||||
var multiAddress = peerInfo.addrs.First();
|
||||
// Todo: Is there a case where First address in list is not the way?
|
||||
|
||||
if (Group == peer.Group)
|
||||
{
|
||||
return multiAddress;
|
||||
}
|
||||
|
||||
// The peer we want to connect is in a different pod.
|
||||
// We must replace the default IP with the pod IP in the multiAddress.
|
||||
return multiAddress.Replace("0.0.0.0", peer.Group.PodInfo!.Ip);
|
||||
}
|
||||
|
||||
private void DownloadToFile(string contentId, TestFile file)
|
||||
{
|
||||
using var fileStream = File.OpenWrite(file.Filename);
|
||||
using var downloadStream = Http().HttpGetStream("download/" + contentId);
|
||||
downloadStream.CopyTo(fileStream);
|
||||
}
|
||||
|
||||
private Http Http()
|
||||
{
|
||||
return new Http(ip: k8sCluster.GetIp(), port: Container.ServicePort, baseUrl: "/api/codex/v1");
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
log.Log($"{GetName()}: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentId
|
||||
{
|
||||
public ContentId(string id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public interface IPodLogHandler
|
||||
{
|
||||
void Log(Stream log);
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class DontDownloadLogsOnFailureAttribute : PropertyAttribute
|
||||
{
|
||||
public DontDownloadLogsOnFailureAttribute()
|
||||
: base(Timing.UseLongTimeoutsKey)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class PodLogDownloader
|
||||
{
|
||||
public const string DontDownloadLogsOnFailureKey = "DontDownloadLogsOnFailure";
|
||||
|
||||
private readonly TestLog log;
|
||||
private readonly IK8sManager k8SManager;
|
||||
|
||||
public PodLogDownloader(TestLog log, IK8sManager k8sManager)
|
||||
{
|
||||
this.log = log;
|
||||
k8SManager = k8sManager;
|
||||
}
|
||||
|
||||
public CodexNodeLog DownloadLog(OnlineCodexNode node)
|
||||
{
|
||||
var description = node.Describe();
|
||||
var subFile = log.CreateSubfile();
|
||||
|
||||
log.Log($"Downloading logs for {description} to file {subFile.FilenameWithoutPath}");
|
||||
var handler = new PodLogDownloadHandler(description, subFile);
|
||||
k8SManager.FetchPodLog(node, handler);
|
||||
return handler.CreateCodexNodeLog();
|
||||
}
|
||||
}
|
||||
|
||||
public class PodLogDownloadHandler : IPodLogHandler
|
||||
{
|
||||
private readonly string description;
|
||||
private readonly LogFile log;
|
||||
|
||||
public PodLogDownloadHandler(string description, LogFile log)
|
||||
{
|
||||
this.description = description;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public CodexNodeLog CreateCodexNodeLog()
|
||||
{
|
||||
return new CodexNodeLog(log);
|
||||
}
|
||||
|
||||
public void Log(Stream stream)
|
||||
{
|
||||
log.Write($"{description} -->> {log.FilenameWithoutPath}");
|
||||
log.WriteRaw(description);
|
||||
var reader = new StreamReader(stream);
|
||||
var line = reader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
log.WriteRaw(line);
|
||||
line = reader.ReadLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
using CodexDistTestCore.Config;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public class TestLog
|
||||
{
|
||||
private readonly NumberSource subfileNumberSource = new NumberSource(0);
|
||||
private readonly LogFile file;
|
||||
private readonly DateTime now;
|
||||
|
||||
public TestLog()
|
||||
{
|
||||
now = DateTime.UtcNow;
|
||||
|
||||
var name = GetTestName();
|
||||
file = new LogFile(now, name);
|
||||
|
||||
Log($"Begin: {name}");
|
||||
}
|
||||
|
||||
public void Log(string message)
|
||||
{
|
||||
file.Write(message);
|
||||
}
|
||||
|
||||
public void Error(string message)
|
||||
{
|
||||
Log($"[ERROR] {message}");
|
||||
}
|
||||
|
||||
public void EndTest()
|
||||
{
|
||||
var result = TestContext.CurrentContext.Result;
|
||||
|
||||
Log($"Finished: {GetTestName()} = {result.Outcome.Status}");
|
||||
if (!string.IsNullOrEmpty(result.Message))
|
||||
{
|
||||
Log(result.Message);
|
||||
Log($"{result.StackTrace}");
|
||||
}
|
||||
}
|
||||
|
||||
public LogFile CreateSubfile()
|
||||
{
|
||||
return new LogFile(now, $"{GetTestName()}_{subfileNumberSource.GetNextNumber().ToString().PadLeft(6, '0')}");
|
||||
}
|
||||
|
||||
private static string GetTestName()
|
||||
{
|
||||
var test = TestContext.CurrentContext.Test;
|
||||
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
|
||||
var args = FormatArguments(test);
|
||||
return $"{className}.{test.MethodName}{args}";
|
||||
}
|
||||
|
||||
private static string FormatArguments(TestContext.TestAdapter test)
|
||||
{
|
||||
if (test.Arguments == null || !test.Arguments.Any()) return "";
|
||||
return $"[{string.Join(',', test.Arguments)}]";
|
||||
}
|
||||
}
|
||||
|
||||
public class LogFile
|
||||
{
|
||||
private readonly string filepath;
|
||||
|
||||
public LogFile(DateTime now, string name)
|
||||
{
|
||||
filepath = Path.Join(
|
||||
LogConfig.LogRoot,
|
||||
$"{now.Year}-{Pad(now.Month)}",
|
||||
Pad(now.Day));
|
||||
|
||||
Directory.CreateDirectory(filepath);
|
||||
|
||||
FilenameWithoutPath = $"{Pad(now.Hour)}-{Pad(now.Minute)}-{Pad(now.Second)}Z_{name.Replace('.', '-')}.log";
|
||||
FullFilename = Path.Combine(filepath, FilenameWithoutPath);
|
||||
}
|
||||
|
||||
public string FullFilename { get; }
|
||||
public string FilenameWithoutPath { get; }
|
||||
|
||||
public void Write(string message)
|
||||
{
|
||||
WriteRaw($"{GetTimestamp()} {message}");
|
||||
}
|
||||
|
||||
public void WriteRaw(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.AppendAllLines(FullFilename, new[] { message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Writing to log has failed: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string Pad(int n)
|
||||
{
|
||||
return n.ToString().PadLeft(2, '0');
|
||||
}
|
||||
|
||||
private static string GetTimestamp()
|
||||
{
|
||||
return $"[{DateTime.UtcNow.ToString("u")}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace CodexDistTestCore
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
public static void Sleep(TimeSpan span)
|
||||
{
|
||||
Thread.Sleep(span);
|
||||
}
|
||||
|
||||
public static T Wait<T>(Task<T> task)
|
||||
{
|
||||
task.Wait();
|
||||
return task.Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\DistTestCore\DistTestCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,126 @@
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Marketplace;
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace CodexNetDeployer
|
||||
{
|
||||
public class CodexNodeStarter
|
||||
{
|
||||
private readonly Configuration config;
|
||||
private readonly TestLifecycle lifecycle;
|
||||
private readonly GethStartResult gethResult;
|
||||
private string bootstrapSpr = "";
|
||||
private int validatorsLeft;
|
||||
|
||||
public CodexNodeStarter(Configuration config, TestLifecycle lifecycle, GethStartResult gethResult, int numberOfValidators)
|
||||
{
|
||||
this.config = config;
|
||||
this.lifecycle = lifecycle;
|
||||
this.gethResult = gethResult;
|
||||
validatorsLeft = numberOfValidators;
|
||||
}
|
||||
|
||||
public CodexNodeStartResult? Start(int i)
|
||||
{
|
||||
Console.Write($" - {i} = ");
|
||||
var workflow = lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
var workflowStartup = new StartupConfig();
|
||||
workflowStartup.Add(gethResult);
|
||||
workflowStartup.Add(CreateCodexStartupConfig(bootstrapSpr, i, validatorsLeft));
|
||||
workflowStartup.NameOverride = GetCodexContainerName(i);
|
||||
|
||||
var containers = workflow.Start(1, Location.Unspecified, new CodexContainerRecipe(), workflowStartup);
|
||||
|
||||
var container = containers.Containers.First();
|
||||
var codexAccess = new CodexAccess(lifecycle.Log, container, lifecycle.TimeSet, lifecycle.Configuration.GetAddress(container));
|
||||
var account = gethResult.MarketplaceNetwork.Bootstrap.AllAccounts.Accounts[i];
|
||||
var tokenAddress = gethResult.MarketplaceNetwork.Marketplace.TokenAddress;
|
||||
var marketAccess = new MarketplaceAccess(lifecycle, gethResult.MarketplaceNetwork, account, codexAccess);
|
||||
|
||||
try
|
||||
{
|
||||
var debugInfo = codexAccess.GetDebugInfo();
|
||||
if (!string.IsNullOrWhiteSpace(debugInfo.spr))
|
||||
{
|
||||
Console.Write("Online\t");
|
||||
|
||||
var interaction = gethResult.MarketplaceNetwork.Bootstrap.StartInteraction(lifecycle);
|
||||
interaction.MintTestTokens(new[] { account.Account }, config.InitialTestTokens, tokenAddress);
|
||||
Console.Write("Tokens minted\t");
|
||||
|
||||
var response = marketAccess.MakeStorageAvailable(
|
||||
totalSpace: config.StorageSell!.Value.MB(),
|
||||
minPriceForTotalSpace: config.MinPrice.TestTokens(),
|
||||
maxCollateral: config.MaxCollateral.TestTokens(),
|
||||
maxDuration: TimeSpan.FromSeconds(config.MaxDuration));
|
||||
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
Console.Write("Storage available\tOK" + Environment.NewLine);
|
||||
|
||||
if (string.IsNullOrEmpty(bootstrapSpr)) bootstrapSpr = debugInfo.spr;
|
||||
validatorsLeft--;
|
||||
return new CodexNodeStartResult(workflow, container, codexAccess);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Exception:" + ex.ToString());
|
||||
}
|
||||
|
||||
Console.Write("Unknown failure. Downloading container log." + Environment.NewLine);
|
||||
lifecycle.DownloadLog(container);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetCodexContainerName(int i)
|
||||
{
|
||||
if (i == 0) return "BOOTSTRAP";
|
||||
return "CODEX" + i;
|
||||
}
|
||||
|
||||
private CodexStartupConfig CreateCodexStartupConfig(string bootstrapSpr, int i, int validatorsLeft)
|
||||
{
|
||||
var codexStart = new CodexStartupConfig(config.CodexLogLevel);
|
||||
|
||||
if (!string.IsNullOrEmpty(bootstrapSpr)) codexStart.BootstrapSpr = bootstrapSpr;
|
||||
codexStart.StorageQuota = config.StorageQuota!.Value.MB();
|
||||
var marketplaceConfig = new MarketplaceInitialConfig(100000.Eth(), 0.TestTokens(), validatorsLeft > 0);
|
||||
marketplaceConfig.AccountIndexOverride = i;
|
||||
codexStart.MarketplaceConfig = marketplaceConfig;
|
||||
codexStart.MetricsMode = config.Metrics;
|
||||
|
||||
if (config.BlockTTL != Configuration.SecondsIn1Day)
|
||||
{
|
||||
codexStart.BlockTTL = config.BlockTTL;
|
||||
}
|
||||
if (config.BlockMI != Configuration.TenMinutes)
|
||||
{
|
||||
codexStart.BlockMaintenanceInterval = TimeSpan.FromSeconds(config.BlockMI);
|
||||
}
|
||||
if (config.BlockMN != 1000)
|
||||
{
|
||||
codexStart.BlockMaintenanceNumber = config.BlockMN;
|
||||
}
|
||||
|
||||
return codexStart;
|
||||
}
|
||||
}
|
||||
|
||||
public class CodexNodeStartResult
|
||||
{
|
||||
public CodexNodeStartResult(StartupWorkflow workflow, RunningContainer container, CodexAccess access)
|
||||
{
|
||||
Workflow = workflow;
|
||||
Container = container;
|
||||
Access = access;
|
||||
}
|
||||
|
||||
public StartupWorkflow Workflow { get; }
|
||||
public RunningContainer Container { get; }
|
||||
public CodexAccess Access { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using ArgsUniform;
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Metrics;
|
||||
|
||||
namespace CodexNetDeployer
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
public const int SecondsIn1Day = 24 * 60 * 60;
|
||||
public const int TenMinutes = 10 * 60;
|
||||
|
||||
[Uniform("kube-config", "kc", "KUBECONFIG", false, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
|
||||
public string KubeConfigFile { get; set; } = "null";
|
||||
|
||||
[Uniform("kube-namespace", "kn", "KUBENAMESPACE", true, "Kubernetes namespace to be used for deployment.")]
|
||||
public string KubeNamespace { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("nodes", "n", "NODES", true, "Number of Codex nodes to be created.")]
|
||||
public int? NumberOfCodexNodes { get; set; }
|
||||
|
||||
[Uniform("validators", "v", "VALIDATORS", true, "Number of Codex nodes that will be validating.")]
|
||||
public int? NumberOfValidators { get; set; }
|
||||
|
||||
[Uniform("storage-quota", "sq", "STORAGEQUOTA", true, "Storage quota in megabytes used by each Codex node.")]
|
||||
public int? StorageQuota { get; set; }
|
||||
|
||||
[Uniform("storage-sell", "ss", "STORAGESELL", true, "Number of megabytes of storage quota to make available for selling.")]
|
||||
public int? StorageSell { get; set; }
|
||||
|
||||
[Uniform("log-level", "l", "LOGLEVEL", true, "Log level used by each Codex node. [Trace, Debug*, Info, Warn, Error]")]
|
||||
public CodexLogLevel CodexLogLevel { get; set; } = CodexLogLevel.Debug;
|
||||
|
||||
[Uniform("test-tokens", "tt", "TESTTOKENS", true, "Initial amount of test-tokens minted for each Codex node.")]
|
||||
public int InitialTestTokens { get; set; } = int.MaxValue;
|
||||
|
||||
[Uniform("min-price", "mp", "MINPRICE", true, "Minimum price for the storage space for which contracts will be accepted.")]
|
||||
public int MinPrice { get; set; }
|
||||
|
||||
[Uniform("max-collateral", "mc", "MAXCOLLATERAL", true, "Maximum collateral that will be placed for the total storage space.")]
|
||||
public int MaxCollateral { get; set; }
|
||||
|
||||
[Uniform("max-duration", "md", "MAXDURATION", true, "Maximum duration in seconds for contracts which will be accepted.")]
|
||||
public int MaxDuration { get; set; }
|
||||
|
||||
[Uniform("block-ttl", "bt", "BLOCKTTL", false, "Block timeout in seconds. Default is 24 hours.")]
|
||||
public int BlockTTL { get; set; } = SecondsIn1Day;
|
||||
|
||||
[Uniform("block-mi", "bmi", "BLOCKMI", false, "Block maintenance interval in seconds. Default is 10 minutes.")]
|
||||
public int BlockMI { get; set; } = TenMinutes;
|
||||
|
||||
[Uniform("block-mn", "bmn", "BLOCKMN", false, "Number of blocks maintained per interval. Default is 1000 blocks.")]
|
||||
public int BlockMN { get; set; } = 1000;
|
||||
|
||||
[Uniform("metrics", "m", "METRICS", false, "[None*, Record, Dashboard]. Determines if metrics will be recorded and if a dashboard service will be created.")]
|
||||
public MetricsMode Metrics { get; set; } = MetricsMode.None;
|
||||
|
||||
[Uniform("teststype-podlabel", "ttpl", "TESTSTYPE-PODLABEL", false, "Each kubernetes pod will be created with a label 'teststype' with value 'continuous'. " +
|
||||
"set this option to override the label value.")]
|
||||
public string TestsTypePodLabel { get; set; } = "continuous-tests";
|
||||
|
||||
[Uniform("check-connect", "cc", "CHECKCONNECT", false, "If true, deployer check ensure peer-connectivity between all deployed nodes after deployment.")]
|
||||
public bool CheckPeerConnection { get; set; } = false;
|
||||
|
||||
public List<string> Validate()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
|
||||
ForEachProperty(
|
||||
onString: (n, v) => StringIsSet(n, v, errors),
|
||||
onInt: (n, v) => IntIsOverZero(n, v, errors));
|
||||
|
||||
if (NumberOfValidators > NumberOfCodexNodes)
|
||||
{
|
||||
errors.Add($"{nameof(NumberOfValidators)} ({NumberOfValidators}) may not be greater than {nameof(NumberOfCodexNodes)} ({NumberOfCodexNodes}).");
|
||||
}
|
||||
if (StorageSell.HasValue && StorageQuota.HasValue && StorageSell.Value >= StorageQuota.Value)
|
||||
{
|
||||
errors.Add("StorageSell cannot be greater than or equal to StorageQuota.");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void ForEachProperty(Action<string, string> onString, Action<string, int?> onInt)
|
||||
{
|
||||
var properties = GetType().GetProperties();
|
||||
foreach (var p in properties)
|
||||
{
|
||||
if (p.PropertyType == typeof(string)) onString(p.Name, (string)p.GetValue(this)!);
|
||||
if (p.PropertyType == typeof(int?)) onInt(p.Name, (int?)p.GetValue(this)!);
|
||||
if (p.PropertyType == typeof(int)) onInt(p.Name, (int)p.GetValue(this)!);
|
||||
}
|
||||
}
|
||||
|
||||
private static void IntIsOverZero(string variable, int? value, List<string> errors)
|
||||
{
|
||||
if (value == null || value.Value < 1)
|
||||
{
|
||||
errors.Add($"{variable} must be set and must be greater than 0.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void StringIsSet(string variable, string value, List<string> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
errors.Add($"{variable} must be set.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace CodexNetDeployer
|
||||
{
|
||||
public class Deployer
|
||||
{
|
||||
private readonly Configuration config;
|
||||
private readonly DefaultTimeSet timeset;
|
||||
private readonly PeerConnectivityChecker peerConnectivityChecker;
|
||||
|
||||
public Deployer(Configuration config)
|
||||
{
|
||||
this.config = config;
|
||||
timeset = new DefaultTimeSet();
|
||||
peerConnectivityChecker = new PeerConnectivityChecker();
|
||||
}
|
||||
|
||||
public CodexDeployment Deploy()
|
||||
{
|
||||
Log("Initializing...");
|
||||
var lifecycle = CreateTestLifecycle();
|
||||
|
||||
Log("Preparing configuration...");
|
||||
// We trick the Geth companion node into unlocking all of its accounts, by saying we want to start 999 codex nodes.
|
||||
var setup = new CodexSetup(999, config.CodexLogLevel);
|
||||
setup.WithStorageQuota(config.StorageQuota!.Value.MB()).EnableMarketplace(0.TestTokens());
|
||||
setup.MetricsMode = config.Metrics;
|
||||
|
||||
Log("Creating Geth instance and deploying contracts...");
|
||||
var gethStarter = new GethStarter(lifecycle);
|
||||
var gethResults = gethStarter.BringOnlineMarketplaceFor(setup);
|
||||
|
||||
Log("Geth started. Codex contracts deployed.");
|
||||
Log("Warning: It can take up to 45 minutes for the Geth node to finish unlocking all if its 1000 preconfigured accounts.");
|
||||
|
||||
// It takes a second for the geth node to unlock a single account. Let's assume 3.
|
||||
// We can't start the codex nodes until their accounts are definitely unlocked. So
|
||||
// We wait:
|
||||
Thread.Sleep(TimeSpan.FromSeconds(3.0 * config.NumberOfCodexNodes!.Value));
|
||||
|
||||
Log("Starting Codex nodes...");
|
||||
|
||||
// Each node must have its own IP, so it needs it own pod. Start them 1 at a time.
|
||||
var codexStarter = new CodexNodeStarter(config, lifecycle, gethResults, config.NumberOfValidators!.Value);
|
||||
var startResults = new List<CodexNodeStartResult>();
|
||||
for (var i = 0; i < config.NumberOfCodexNodes; i++)
|
||||
{
|
||||
var result = codexStarter.Start(i);
|
||||
if (result != null) startResults.Add(result);
|
||||
}
|
||||
|
||||
var (prometheusContainer, grafanaStartInfo) = StartMetricsService(lifecycle, setup, startResults.Select(r => r.Container));
|
||||
|
||||
CheckPeerConnectivity(startResults);
|
||||
CheckContainerRestarts(startResults);
|
||||
|
||||
return new CodexDeployment(gethResults, startResults.Select(r => r.Container).ToArray(), prometheusContainer, grafanaStartInfo, CreateMetadata());
|
||||
}
|
||||
|
||||
private TestLifecycle CreateTestLifecycle()
|
||||
{
|
||||
var kubeConfig = GetKubeConfig(config.KubeConfigFile);
|
||||
|
||||
var lifecycleConfig = new DistTestCore.Configuration
|
||||
(
|
||||
kubeConfigFile: kubeConfig,
|
||||
logPath: "null",
|
||||
logDebug: false,
|
||||
dataFilesPath: "notUsed",
|
||||
codexLogLevel: config.CodexLogLevel,
|
||||
k8sNamespacePrefix: config.KubeNamespace
|
||||
);
|
||||
|
||||
var lifecycle = new TestLifecycle(new NullLog(), lifecycleConfig, timeset, string.Empty);
|
||||
DefaultContainerRecipe.TestsType = config.TestsTypePodLabel;
|
||||
DefaultContainerRecipe.ApplicationIds = lifecycle.GetApplicationIds();
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
private (RunningContainer?, GrafanaStartInfo?) StartMetricsService(TestLifecycle lifecycle, CodexSetup setup, IEnumerable<RunningContainer> codexContainers)
|
||||
{
|
||||
if (setup.MetricsMode == DistTestCore.Metrics.MetricsMode.None) return (null, null);
|
||||
|
||||
Log("Starting metrics service...");
|
||||
var runningContainers = new[] { new RunningContainers(null!, null!, codexContainers.ToArray()) };
|
||||
var prometheusContainer = lifecycle.PrometheusStarter.CollectMetricsFor(runningContainers).Containers.Single();
|
||||
|
||||
if (setup.MetricsMode == DistTestCore.Metrics.MetricsMode.Record) return (prometheusContainer, null);
|
||||
|
||||
Log("Starting dashboard service...");
|
||||
var grafanaStartInfo = lifecycle.GrafanaStarter.StartDashboard(prometheusContainer, setup);
|
||||
return (prometheusContainer, grafanaStartInfo);
|
||||
}
|
||||
|
||||
private string? GetKubeConfig(string kubeConfigFile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kubeConfigFile) || kubeConfigFile.ToLowerInvariant() == "null") return null;
|
||||
return kubeConfigFile;
|
||||
}
|
||||
|
||||
private void CheckPeerConnectivity(List<CodexNodeStartResult> codexContainers)
|
||||
{
|
||||
if (!config.CheckPeerConnection) return;
|
||||
|
||||
Log("Starting peer-connectivity check for deployed nodes...");
|
||||
peerConnectivityChecker.CheckConnectivity(codexContainers);
|
||||
Log("Check passed.");
|
||||
}
|
||||
|
||||
private void CheckContainerRestarts(List<CodexNodeStartResult> startResults)
|
||||
{
|
||||
var crashes = new List<RunningContainer>();
|
||||
foreach (var startResult in startResults)
|
||||
{
|
||||
var watcher = startResult.Workflow.CreateCrashWatcher(startResult.Container);
|
||||
if (watcher.HasContainerCrashed()) crashes.Add(startResult.Container);
|
||||
}
|
||||
|
||||
if (!crashes.Any())
|
||||
{
|
||||
Log("Container restart check passed.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"Deployment failed. The following containers have crashed: {string.Join(",", crashes.Select(c => c.Name))}");
|
||||
throw new Exception("Deployment failed: One or more containers crashed.");
|
||||
}
|
||||
}
|
||||
|
||||
private DeploymentMetadata CreateMetadata()
|
||||
{
|
||||
return new DeploymentMetadata(
|
||||
kubeNamespace: config.KubeNamespace,
|
||||
numberOfCodexNodes: config.NumberOfCodexNodes!.Value,
|
||||
numberOfValidators: config.NumberOfValidators!.Value,
|
||||
storageQuotaMB: config.StorageQuota!.Value,
|
||||
codexLogLevel: config.CodexLogLevel,
|
||||
initialTestTokens: config.InitialTestTokens,
|
||||
minPrice: config.MinPrice,
|
||||
maxCollateral: config.MaxCollateral,
|
||||
maxDuration: config.MaxDuration,
|
||||
blockTTL: config.BlockTTL,
|
||||
blockMI: config.BlockMI,
|
||||
blockMN: config.BlockMN);
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
Console.WriteLine(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using DistTestCore.Helpers;
|
||||
using Logging;
|
||||
|
||||
namespace CodexNetDeployer
|
||||
{
|
||||
public class PeerConnectivityChecker
|
||||
{
|
||||
public void CheckConnectivity(List<CodexNodeStartResult> startResults)
|
||||
{
|
||||
var log = new ConsoleLog();
|
||||
var checker = new PeerConnectionTestHelpers(log);
|
||||
var access = startResults.Select(r => r.Access);
|
||||
|
||||
checker.AssertFullyConnected(access);
|
||||
}
|
||||
}
|
||||
|
||||
public class ConsoleLog : BaseLog
|
||||
{
|
||||
public ConsoleLog() : base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetFullName()
|
||||
{
|
||||
return "CONSOLE";
|
||||
}
|
||||
|
||||
public override void Log(string message)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using ArgsUniform;
|
||||
using CodexNetDeployer;
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Marketplace;
|
||||
using DistTestCore.Metrics;
|
||||
using Newtonsoft.Json;
|
||||
using Configuration = CodexNetDeployer.Configuration;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine("CodexNetDeployer" + nl);
|
||||
|
||||
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
|
||||
var config = uniformArgs.Parse(true);
|
||||
|
||||
var errors = config.Validate();
|
||||
if (errors.Any())
|
||||
{
|
||||
Console.WriteLine($"Configuration errors: ({errors.Count})");
|
||||
foreach ( var error in errors ) Console.WriteLine("\t" + error);
|
||||
Console.WriteLine(nl);
|
||||
PrintHelp();
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Using images:" + nl +
|
||||
$"\tCodex image: '{new CodexContainerRecipe().Image}'" + nl +
|
||||
$"\tCodexContracts image: '{new CodexContractsContainerRecipe().Image}'" + nl +
|
||||
$"\tPrometheus image: '{new PrometheusContainerRecipe().Image}'" + nl +
|
||||
$"\tGeth image: '{new GethContainerRecipe().Image}'" + nl +
|
||||
$"\tGrafana image: '{new GrafanaContainerRecipe().Image}'" + nl);
|
||||
|
||||
if (!args.Any(a => a == "-y"))
|
||||
{
|
||||
Console.WriteLine("Does the above config look good? [y/n]");
|
||||
if (Console.ReadLine()!.ToLowerInvariant() != "y") return;
|
||||
Console.WriteLine("I think so too.");
|
||||
}
|
||||
|
||||
var deployer = new Deployer(config);
|
||||
var deployment = deployer.Deploy();
|
||||
|
||||
Console.WriteLine("Writing codex-deployment.json...");
|
||||
|
||||
File.WriteAllText("codex-deployment.json", JsonConvert.SerializeObject(deployment, Formatting.Indented));
|
||||
|
||||
Console.WriteLine("Done!");
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine("CodexNetDeployer allows you to easily deploy multiple Codex nodes in a Kubernetes cluster. " +
|
||||
"The deployer will set up the required supporting services, deploy the Codex on-chain contracts, start and bootstrap the Codex instances. " +
|
||||
"All Kubernetes objects will be created in the namespace provided, allowing you to easily find, modify, and delete them afterwards." + nl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
dotnet run \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--kube-namespace=codex-continuous-tests \
|
||||
--nodes=5 \
|
||||
--validators=3 \
|
||||
--log-level=Trace \
|
||||
--storage-quota=2048 \
|
||||
--storage-sell=1024 \
|
||||
--min-price=1024 \
|
||||
--max-collateral=1024 \
|
||||
--max-duration=3600000 \
|
||||
--block-ttl=180 \
|
||||
--block-mi=120 \
|
||||
--block-mn=10000 \
|
||||
--metrics=Dashboard \
|
||||
--check-connect=1
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\ContinuousTests\ContinuousTests.csproj" />
|
||||
<ProjectReference Include="..\DistTestCore\DistTestCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
using ArgsUniform;
|
||||
using DistTestCore.Codex;
|
||||
|
||||
namespace CodexNetDownloader
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
[Uniform("output-path", "o", "OUTPUT", true, "Path where files will be written.")]
|
||||
public string OutputPath { get; set; } = "output";
|
||||
|
||||
[Uniform("codex-deployment", "c", "CODEXDEPLOYMENT", true, "Path to codex-deployment JSON file.")]
|
||||
public string CodexDeploymentJson { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
|
||||
public string KubeConfigFile { get; set; } = "null";
|
||||
|
||||
public CodexDeployment CodexDeployment { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using ArgsUniform;
|
||||
using ContinuousTests;
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine("CodexNetDownloader" + nl);
|
||||
|
||||
var uniformArgs = new ArgsUniform<CodexNetDownloader.Configuration>(PrintHelp, args);
|
||||
var config = uniformArgs.Parse(true);
|
||||
|
||||
config.CodexDeployment = ParseCodexDeploymentJson(config.CodexDeploymentJson);
|
||||
|
||||
if (!Directory.Exists(config.OutputPath)) Directory.CreateDirectory(config.OutputPath);
|
||||
|
||||
var k8sFactory = new K8sFactory();
|
||||
var lifecycle = k8sFactory.CreateTestLifecycle(config.KubeConfigFile, config.OutputPath, "dataPath", config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), new NullLog());
|
||||
|
||||
foreach (var container in config.CodexDeployment.CodexContainers)
|
||||
{
|
||||
lifecycle.DownloadLog(container);
|
||||
}
|
||||
|
||||
Console.WriteLine("Done!");
|
||||
}
|
||||
|
||||
private static CodexDeployment ParseCodexDeploymentJson(string filename)
|
||||
{
|
||||
var d = JsonConvert.DeserializeObject<CodexDeployment>(File.ReadAllText(filename))!;
|
||||
if (d == null) throw new Exception("Unable to parse " + filename);
|
||||
return d;
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine("CodexNetDownloader lets you download all container logs given a codex-deployment.json file." + nl);
|
||||
|
||||
Console.WriteLine("CodexNetDownloader assumes you are running this tool from *inside* the Kubernetes cluster. " +
|
||||
"If you are not running this from a container inside the cluster, add the argument '--external'." + nl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class CodexAccessFactory
|
||||
{
|
||||
public CodexAccess[] Create(Configuration config, RunningContainer[] containers, BaseLog log, ITimeSet timeSet)
|
||||
{
|
||||
return containers.Select(container =>
|
||||
{
|
||||
var address = container.ClusterExternalAddress;
|
||||
if (config.RunnerLocation == RunnerLocation.InternalToCluster) address = container.ClusterInternalAddress;
|
||||
return new CodexAccess(log, container, timeSet, address);
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using ArgsUniform;
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
[Uniform("log-path", "l", "LOGPATH", true, "Path where log files will be written.")]
|
||||
public string LogPath { get; set; } = "logs";
|
||||
|
||||
[Uniform("data-path", "d", "DATAPATH", true, "Path where temporary data files will be written.")]
|
||||
public string DataPath { get; set; } = "data";
|
||||
|
||||
[Uniform("codex-deployment", "c", "CODEXDEPLOYMENT", true, "Path to codex-deployment JSON file.")]
|
||||
public string CodexDeploymentJson { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("keep", "k", "KEEP", false, "Set to '1' to retain logs of successful tests.")]
|
||||
public bool KeepPassedTestLogs { get; set; } = false;
|
||||
|
||||
[Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
|
||||
public string KubeConfigFile { get; set; } = "null";
|
||||
|
||||
[Uniform("stop", "s", "STOPONFAIL", false, "If greater than zero, runner will stop after this many test failures and download all cluster container logs. 0 by default.")]
|
||||
public int StopOnFailure { get; set; } = 0;
|
||||
|
||||
[Uniform("dl-logs", "dl", "DLLOGS", false, "If true, runner will periodically download and save/append container logs to the log path.")]
|
||||
public bool DownloadContainerLogs { get; set; } = false;
|
||||
|
||||
public CodexDeployment CodexDeployment { get; set; } = null!;
|
||||
|
||||
public RunnerLocation RunnerLocation { get; set; }
|
||||
}
|
||||
|
||||
public class ConfigLoader
|
||||
{
|
||||
public Configuration Load(string[] args)
|
||||
{
|
||||
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
|
||||
|
||||
var result = uniformArgs.Parse(true);
|
||||
|
||||
result.CodexDeployment = ParseCodexDeploymentJson(result.CodexDeploymentJson);
|
||||
result.RunnerLocation = RunnerLocationUtils.DetermineRunnerLocation(result.CodexDeployment.CodexContainers.First());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private CodexDeployment ParseCodexDeploymentJson(string filename)
|
||||
{
|
||||
var d = JsonConvert.DeserializeObject<CodexDeployment>(File.ReadAllText(filename))!;
|
||||
if (d == null) throw new Exception("Unable to parse " + filename);
|
||||
return d;
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
Console.WriteLine("ContinuousTests will run a set of tests against a codex deployment given a codex-deployment.json file." + nl +
|
||||
"The tests will run in an endless loop unless otherwise specified, using the test-specific timing values." + nl);
|
||||
|
||||
Console.WriteLine("ContinuousTests assumes you are running this tool from *inside* the Kubernetes cluster. " +
|
||||
"If you are not running this from a container inside the cluster, add the argument '--external'." + nl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Logs;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public abstract class ContinuousTestLongTimeouts : ContinuousTest
|
||||
{
|
||||
public override ITimeSet TimeSet => new LongTimeSet();
|
||||
}
|
||||
|
||||
public abstract class ContinuousTest
|
||||
{
|
||||
protected const int Zero = 0;
|
||||
protected const int MinuteOne = 60;
|
||||
protected const int MinuteFive = MinuteOne * 5;
|
||||
protected const int HourOne = MinuteOne * 60;
|
||||
protected const int HourThree = HourOne * 3;
|
||||
protected const int DayOne = HourOne * 24;
|
||||
protected const int DayThree = DayOne * 3;
|
||||
|
||||
private const string UploadFailedMessage = "Unable to store block";
|
||||
|
||||
public void Initialize(CodexAccess[] nodes, BaseLog log, FileManager fileManager, Configuration configuration, CancellationToken cancelToken)
|
||||
{
|
||||
Nodes = nodes;
|
||||
Log = log;
|
||||
FileManager = fileManager;
|
||||
Configuration = configuration;
|
||||
CancelToken = cancelToken;
|
||||
|
||||
if (nodes != null)
|
||||
{
|
||||
NodeRunner = new NodeRunner(Nodes, configuration, TimeSet, Log, CustomK8sNamespace, EthereumAccountIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
NodeRunner = null!;
|
||||
}
|
||||
}
|
||||
|
||||
public CodexAccess[] Nodes { get; private set; } = null!;
|
||||
public BaseLog Log { get; private set; } = null!;
|
||||
public IFileManager FileManager { get; private set; } = null!;
|
||||
public Configuration Configuration { get; private set; } = null!;
|
||||
public virtual ITimeSet TimeSet { get { return new DefaultTimeSet(); } }
|
||||
public CancellationToken CancelToken { get; private set; } = new CancellationToken();
|
||||
public NodeRunner NodeRunner { get; private set; } = null!;
|
||||
|
||||
public abstract int RequiredNumberOfNodes { get; }
|
||||
public abstract TimeSpan RunTestEvery { get; }
|
||||
public abstract TestFailMode TestFailMode { get; }
|
||||
public virtual int EthereumAccountIndex { get { return -1; } }
|
||||
public virtual string CustomK8sNamespace { get { return string.Empty; } }
|
||||
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetType().Name;
|
||||
}
|
||||
}
|
||||
|
||||
public ContentId? UploadFile(CodexAccess node, TestFile file)
|
||||
{
|
||||
using var fileStream = File.OpenRead(file.Filename);
|
||||
|
||||
var logMessage = $"Uploading file {file.Describe()}...";
|
||||
var response = Stopwatch.Measure(Log, logMessage, () =>
|
||||
{
|
||||
return node.UploadFile(fileStream);
|
||||
});
|
||||
|
||||
if (string.IsNullOrEmpty(response)) return null;
|
||||
if (response.StartsWith(UploadFailedMessage)) return null;
|
||||
|
||||
Log.Log($"Uploaded file. Received contentId: '{response}'.");
|
||||
return new ContentId(response);
|
||||
}
|
||||
|
||||
public TestFile DownloadFile(CodexAccess node, ContentId contentId, string fileLabel = "")
|
||||
{
|
||||
var logMessage = $"Downloading for contentId: '{contentId.Id}'...";
|
||||
var file = FileManager.CreateEmptyTestFile(fileLabel);
|
||||
Stopwatch.Measure(Log, logMessage, () => DownloadToFile(node, contentId.Id, file));
|
||||
Log.Log($"Downloaded file {file.Describe()} to '{file.Filename}'.");
|
||||
return file;
|
||||
}
|
||||
|
||||
public IDownloadedLog DownloadContainerLog(RunningContainer container, int? tailLines = null)
|
||||
{
|
||||
var nodeRunner = new NodeRunner(Nodes, Configuration, TimeSet, Log, Configuration.CodexDeployment.Metadata.KubeNamespace, EthereumAccountIndex);
|
||||
return nodeRunner.DownloadLog(container, tailLines);
|
||||
}
|
||||
|
||||
private void DownloadToFile(CodexAccess node, string contentId, TestFile file)
|
||||
{
|
||||
using var fileStream = File.OpenWrite(file.Filename);
|
||||
try
|
||||
{
|
||||
using var downloadStream = node.DownloadFile(contentId);
|
||||
downloadStream.CopyTo(fileStream);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Log.Log($"Failed to download file '{contentId}'.");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum TestFailMode
|
||||
{
|
||||
StopAfterFirstFailure,
|
||||
AlwaysRunAllMoments
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using DistTestCore;
|
||||
using Logging;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class ContinuousTestRunner
|
||||
{
|
||||
private readonly K8sFactory k8SFactory = new K8sFactory();
|
||||
private readonly ConfigLoader configLoader = new ConfigLoader();
|
||||
private readonly TestFactory testFactory = new TestFactory();
|
||||
private readonly Configuration config;
|
||||
private readonly StartupChecker startupChecker;
|
||||
private readonly CancellationToken cancelToken;
|
||||
|
||||
public ContinuousTestRunner(string[] args, CancellationToken cancelToken)
|
||||
{
|
||||
config = configLoader.Load(args);
|
||||
startupChecker = new StartupChecker(config, cancelToken);
|
||||
this.cancelToken = cancelToken;
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
startupChecker.Check();
|
||||
|
||||
var taskFactory = new TaskFactory();
|
||||
var overviewLog = new FixtureLog(new LogConfig(config.LogPath, false), DateTime.UtcNow, "Overview");
|
||||
overviewLog.Log("Continuous tests starting...");
|
||||
var allTests = testFactory.CreateTests();
|
||||
|
||||
ClearAllCustomNamespaces(allTests, overviewLog);
|
||||
|
||||
var testLoops = allTests.Select(t => new TestLoop(taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, startupChecker, cancelToken)).ToArray();
|
||||
|
||||
foreach (var testLoop in testLoops)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested) break;
|
||||
|
||||
overviewLog.Log("Launching test-loop for " + testLoop.Name);
|
||||
testLoop.Begin();
|
||||
Thread.Sleep(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
|
||||
overviewLog.Log("Finished launching test-loops.");
|
||||
cancelToken.WaitHandle.WaitOne();
|
||||
overviewLog.Log("Cancelling all test-loops...");
|
||||
taskFactory.WaitAll();
|
||||
overviewLog.Log("All tasks cancelled.");
|
||||
}
|
||||
|
||||
private void ClearAllCustomNamespaces(ContinuousTest[] allTests, FixtureLog log)
|
||||
{
|
||||
foreach (var test in allTests) ClearAllCustomNamespaces(test, log);
|
||||
}
|
||||
|
||||
private void ClearAllCustomNamespaces(ContinuousTest test, FixtureLog log)
|
||||
{
|
||||
if (string.IsNullOrEmpty(test.CustomK8sNamespace)) return;
|
||||
|
||||
log.Log($"Clearing namespace '{test.CustomK8sNamespace}'...");
|
||||
var lifecycle = k8SFactory.CreateTestLifecycle(config.KubeConfigFile, config.LogPath, config.DataPath, test.CustomK8sNamespace, new DefaultTimeSet(), log);
|
||||
lifecycle.WorkflowCreator.CreateWorkflow().DeleteTestResources();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\DistTestCore\DistTestCore.csproj" />
|
||||
<ProjectReference Include="..\KubernetesWorkflow\KubernetesWorkflow.csproj" />
|
||||
<ProjectReference Include="..\Logging\Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore;
|
||||
using Logging;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class K8sFactory
|
||||
{
|
||||
public TestLifecycle CreateTestLifecycle(string kubeConfigFile, string logPath, string dataFilePath, string customNamespace, ITimeSet timeSet, BaseLog log)
|
||||
{
|
||||
var kubeConfig = GetKubeConfig(kubeConfigFile);
|
||||
var lifecycleConfig = new DistTestCore.Configuration
|
||||
(
|
||||
kubeConfigFile: kubeConfig,
|
||||
logPath: logPath,
|
||||
logDebug: false,
|
||||
dataFilesPath: dataFilePath,
|
||||
codexLogLevel: CodexLogLevel.Debug,
|
||||
k8sNamespacePrefix: customNamespace
|
||||
);
|
||||
|
||||
var lifecycle = new TestLifecycle(log, lifecycleConfig, timeSet, string.Empty);
|
||||
DefaultContainerRecipe.TestsType = "continuous-tests";
|
||||
DefaultContainerRecipe.ApplicationIds = lifecycle.GetApplicationIds();
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
private static string? GetKubeConfig(string kubeConfigFile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kubeConfigFile) || kubeConfigFile.ToLowerInvariant() == "null") return null;
|
||||
return kubeConfigFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Marketplace;
|
||||
using DistTestCore;
|
||||
using KubernetesWorkflow;
|
||||
using NUnit.Framework;
|
||||
using Logging;
|
||||
using Utils;
|
||||
using DistTestCore.Logs;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class NodeRunner
|
||||
{
|
||||
private readonly K8sFactory k8SFactory = new K8sFactory();
|
||||
private readonly CodexAccess[] nodes;
|
||||
private readonly Configuration config;
|
||||
private readonly ITimeSet timeSet;
|
||||
private readonly BaseLog log;
|
||||
private readonly string customNamespace;
|
||||
private readonly int ethereumAccountIndex;
|
||||
|
||||
public NodeRunner(CodexAccess[] nodes, Configuration config, ITimeSet timeSet, BaseLog log, string customNamespace, int ethereumAccountIndex)
|
||||
{
|
||||
this.nodes = nodes;
|
||||
this.config = config;
|
||||
this.timeSet = timeSet;
|
||||
this.log = log;
|
||||
this.customNamespace = customNamespace;
|
||||
this.ethereumAccountIndex = ethereumAccountIndex;
|
||||
}
|
||||
|
||||
public void RunNode(Action<CodexAccess, MarketplaceAccess, TestLifecycle> operation)
|
||||
{
|
||||
RunNode(nodes.ToList().PickOneRandom(), operation, 0.TestTokens());
|
||||
}
|
||||
|
||||
public void RunNode(CodexAccess bootstrapNode, Action<CodexAccess, MarketplaceAccess, TestLifecycle> operation)
|
||||
{
|
||||
RunNode(bootstrapNode, operation, 0.TestTokens());
|
||||
}
|
||||
|
||||
public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null)
|
||||
{
|
||||
var subFile = log.CreateSubfile();
|
||||
var description = container.Name;
|
||||
var handler = new LogDownloadHandler(container, description, subFile);
|
||||
|
||||
log.Log($"Downloading logs for {description} to file '{subFile.FullFilename}'");
|
||||
|
||||
var lifecycle = CreateTestLifecycle();
|
||||
var flow = lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
flow.DownloadContainerLog(container, handler, tailLines);
|
||||
|
||||
return new DownloadedLog(subFile, description);
|
||||
}
|
||||
|
||||
public void RunNode(CodexAccess bootstrapNode, Action<CodexAccess, MarketplaceAccess, TestLifecycle> operation, TestToken mintTestTokens)
|
||||
{
|
||||
var lifecycle = CreateTestLifecycle();
|
||||
var flow = lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
|
||||
try
|
||||
{
|
||||
var debugInfo = bootstrapNode.GetDebugInfo();
|
||||
Assert.That(!string.IsNullOrEmpty(debugInfo.spr));
|
||||
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.NameOverride = "TransientNode";
|
||||
var codexStartConfig = new CodexStartupConfig(CodexLogLevel.Trace);
|
||||
codexStartConfig.MarketplaceConfig = new MarketplaceInitialConfig(0.Eth(), 0.TestTokens(), false);
|
||||
codexStartConfig.MarketplaceConfig.AccountIndexOverride = ethereumAccountIndex;
|
||||
codexStartConfig.BootstrapSpr = debugInfo.spr;
|
||||
startupConfig.Add(codexStartConfig);
|
||||
startupConfig.Add(config.CodexDeployment.GethStartResult);
|
||||
var rc = flow.Start(1, Location.Unspecified, new CodexContainerRecipe(), startupConfig);
|
||||
|
||||
var account = config.CodexDeployment.GethStartResult.CompanionNode.Accounts[ethereumAccountIndex];
|
||||
|
||||
var marketplaceNetwork = config.CodexDeployment.GethStartResult.MarketplaceNetwork;
|
||||
if (mintTestTokens.Amount > 0)
|
||||
{
|
||||
var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress;
|
||||
var interaction = marketplaceNetwork.Bootstrap.StartInteraction(lifecycle);
|
||||
interaction.MintTestTokens(new[] { account.Account }, mintTestTokens.Amount, tokenAddress);
|
||||
}
|
||||
|
||||
var container = rc.Containers[0];
|
||||
var address = lifecycle.Configuration.GetAddress(container);
|
||||
var codexAccess = new CodexAccess(log, container, lifecycle.TimeSet, address);
|
||||
var marketAccess = new MarketplaceAccess(lifecycle, marketplaceNetwork, account, codexAccess);
|
||||
|
||||
try
|
||||
{
|
||||
operation(codexAccess, marketAccess, lifecycle);
|
||||
}
|
||||
catch
|
||||
{
|
||||
lifecycle.DownloadLog(container);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
flow.DeleteTestResources();
|
||||
}
|
||||
}
|
||||
|
||||
private TestLifecycle CreateTestLifecycle()
|
||||
{
|
||||
return k8SFactory.CreateTestLifecycle(config.KubeConfigFile, config.LogPath, config.DataPath, customNamespace, timeSet, log);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using ContinuousTests;
|
||||
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Codex Continous-Test-Runner.");
|
||||
Console.WriteLine("Running...");
|
||||
|
||||
var runner = new ContinuousTestRunner(args, Cancellation.Cts.Token);
|
||||
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Console.WriteLine("Stopping...");
|
||||
e.Cancel = true;
|
||||
|
||||
Cancellation.Cts.Cancel();
|
||||
};
|
||||
|
||||
runner.Run();
|
||||
Console.WriteLine("Done.");
|
||||
}
|
||||
|
||||
public static class Cancellation
|
||||
{
|
||||
static Cancellation()
|
||||
{
|
||||
Cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
public static CancellationTokenSource Cts { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore;
|
||||
using Logging;
|
||||
using Utils;
|
||||
using KubernetesWorkflow;
|
||||
using NUnit.Framework.Internal;
|
||||
using System.Reflection;
|
||||
using static Program;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class SingleTestRun
|
||||
{
|
||||
private readonly CodexAccessFactory codexNodeFactory = new CodexAccessFactory();
|
||||
private readonly List<Exception> exceptions = new List<Exception>();
|
||||
private readonly TaskFactory taskFactory;
|
||||
private readonly Configuration config;
|
||||
private readonly BaseLog overviewLog;
|
||||
private readonly TestHandle handle;
|
||||
private readonly CancellationToken cancelToken;
|
||||
private readonly CodexAccess[] nodes;
|
||||
private readonly FileManager fileManager;
|
||||
private readonly FixtureLog fixtureLog;
|
||||
private readonly string testName;
|
||||
private readonly string dataFolder;
|
||||
private static int failureCount = 0;
|
||||
|
||||
public SingleTestRun(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, TestHandle handle, StartupChecker startupChecker, CancellationToken cancelToken)
|
||||
{
|
||||
this.taskFactory = taskFactory;
|
||||
this.config = config;
|
||||
this.overviewLog = overviewLog;
|
||||
this.handle = handle;
|
||||
this.cancelToken = cancelToken;
|
||||
testName = handle.Test.GetType().Name;
|
||||
fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), DateTime.UtcNow, testName);
|
||||
ApplyLogReplacements(fixtureLog, startupChecker);
|
||||
|
||||
nodes = CreateRandomNodes();
|
||||
dataFolder = config.DataPath + "-" + Guid.NewGuid();
|
||||
fileManager = new FileManager(fixtureLog, CreateFileManagerConfiguration());
|
||||
}
|
||||
|
||||
public void Run(EventWaitHandle runFinishedHandle)
|
||||
{
|
||||
taskFactory.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
RunTest();
|
||||
fileManager.DeleteAllTestFiles();
|
||||
Directory.Delete(dataFolder, true);
|
||||
runFinishedHandle.Set();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
overviewLog.Error("Test infra failure: SingleTestRun failed with " + ex);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void RunTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
RunTestMoments();
|
||||
|
||||
if (!config.KeepPassedTestLogs) fixtureLog.Delete();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
fixtureLog.Error("Test run failed with exception: " + ex);
|
||||
fixtureLog.MarkAsFailed();
|
||||
|
||||
failureCount++;
|
||||
if (config.StopOnFailure > 0)
|
||||
{
|
||||
OverviewLog($"Failures: {failureCount} / {config.StopOnFailure}");
|
||||
if (failureCount >= config.StopOnFailure)
|
||||
{
|
||||
OverviewLog($"Configured to stop after {config.StopOnFailure} failures. Downloading cluster logs...");
|
||||
DownloadClusterLogs();
|
||||
OverviewLog("Log download finished. Cancelling test runner...");
|
||||
Cancellation.Cts.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLogReplacements(FixtureLog fixtureLog, StartupChecker startupChecker)
|
||||
{
|
||||
foreach (var replacement in startupChecker.LogReplacements) fixtureLog.AddStringReplace(replacement.From, replacement.To);
|
||||
}
|
||||
|
||||
private void RunTestMoments()
|
||||
{
|
||||
var earliestMoment = handle.GetEarliestMoment();
|
||||
|
||||
var t = earliestMoment;
|
||||
while (true)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
RunMoment(t);
|
||||
|
||||
if (handle.Test.TestFailMode == TestFailMode.StopAfterFirstFailure && exceptions.Any())
|
||||
{
|
||||
Log("Exception detected. TestFailMode = StopAfterFirstFailure. Stopping...");
|
||||
ThrowFailTest();
|
||||
}
|
||||
|
||||
var nextMoment = handle.GetNextMoment(t);
|
||||
if (nextMoment != null)
|
||||
{
|
||||
var delta = TimeSpan.FromSeconds(nextMoment.Value - t);
|
||||
Log($" > Next TestMoment in {Time.FormatDuration(delta)} seconds...");
|
||||
cancelToken.WaitHandle.WaitOne(delta);
|
||||
t = nextMoment.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (exceptions.Any())
|
||||
{
|
||||
ThrowFailTest();
|
||||
}
|
||||
OverviewLog(" > Test passed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowFailTest()
|
||||
{
|
||||
var exs = UnpackExceptions(exceptions);
|
||||
var exceptionsMessage = GetCombinedExceptionsMessage(exs);
|
||||
Log(exceptionsMessage);
|
||||
OverviewLog($" > Test failed: " + exceptionsMessage);
|
||||
throw new Exception(exceptionsMessage);
|
||||
}
|
||||
|
||||
private void DownloadClusterLogs()
|
||||
{
|
||||
var k8sFactory = new K8sFactory();
|
||||
var log = new NullLog();
|
||||
log.FullFilename = Path.Combine(config.LogPath, "NODE");
|
||||
var lifecycle = k8sFactory.CreateTestLifecycle(config.KubeConfigFile, config.LogPath, "dataPath", config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), log);
|
||||
|
||||
foreach (var container in config.CodexDeployment.CodexContainers)
|
||||
{
|
||||
lifecycle.DownloadLog(container);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCombinedExceptionsMessage(Exception[] exceptions)
|
||||
{
|
||||
return string.Join(Environment.NewLine, exceptions.Select(ex => ex.ToString()));
|
||||
}
|
||||
|
||||
private Exception[] UnpackExceptions(List<Exception> exceptions)
|
||||
{
|
||||
return exceptions.Select(UnpackException).ToArray();
|
||||
}
|
||||
|
||||
private Exception UnpackException(Exception exception)
|
||||
{
|
||||
if (exception is AggregateException a)
|
||||
{
|
||||
return UnpackException(a.InnerExceptions.First());
|
||||
}
|
||||
if (exception is TargetInvocationException t)
|
||||
{
|
||||
return UnpackException(t.InnerException!);
|
||||
}
|
||||
|
||||
return exception;
|
||||
}
|
||||
|
||||
private void RunMoment(int t)
|
||||
{
|
||||
using (var context = new TestExecutionContext.IsolatedContext())
|
||||
{
|
||||
try
|
||||
{
|
||||
handle.InvokeMoment(t, InitializeTest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
DecommissionTest();
|
||||
}
|
||||
|
||||
private void InitializeTest(string name)
|
||||
{
|
||||
Log($" > Running TestMoment '{name}'");
|
||||
handle.Test.Initialize(nodes, fixtureLog, fileManager, config, cancelToken);
|
||||
}
|
||||
|
||||
private void DecommissionTest()
|
||||
{
|
||||
handle.Test.Initialize(null!, null!, null!, null!, cancelToken);
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
fixtureLog.Log(msg);
|
||||
}
|
||||
|
||||
private void OverviewLog(string msg)
|
||||
{
|
||||
Log(msg);
|
||||
var containerNames = GetContainerNames();
|
||||
overviewLog.Log($"{containerNames} {testName}: {msg}");
|
||||
}
|
||||
|
||||
private string GetContainerNames()
|
||||
{
|
||||
if (handle.Test.RequiredNumberOfNodes == -1) return "(All Nodes)";
|
||||
return $"({string.Join(",", nodes.Select(n => n.Container.Name))})";
|
||||
}
|
||||
|
||||
private CodexAccess[] CreateRandomNodes()
|
||||
{
|
||||
var containers = SelectRandomContainers();
|
||||
fixtureLog.Log("Selected nodes: " + string.Join(",", containers.Select(c => c.Name)));
|
||||
return codexNodeFactory.Create(config, containers, fixtureLog, handle.Test.TimeSet);
|
||||
}
|
||||
|
||||
private RunningContainer[] SelectRandomContainers()
|
||||
{
|
||||
var number = handle.Test.RequiredNumberOfNodes;
|
||||
if (number == -1) return config.CodexDeployment.CodexContainers;
|
||||
|
||||
var containers = config.CodexDeployment.CodexContainers.ToList();
|
||||
var result = new RunningContainer[number];
|
||||
for (var i = 0; i < number; i++)
|
||||
{
|
||||
result[i] = containers.PickOneRandom();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private DistTestCore.Configuration CreateFileManagerConfiguration()
|
||||
{
|
||||
return new DistTestCore.Configuration(null, string.Empty, false, dataFolder,
|
||||
CodexLogLevel.Error, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore;
|
||||
using Logging;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class StartupChecker
|
||||
{
|
||||
private readonly TestFactory testFactory = new TestFactory();
|
||||
private readonly CodexAccessFactory codexNodeFactory = new CodexAccessFactory();
|
||||
private readonly Configuration config;
|
||||
private readonly CancellationToken cancelToken;
|
||||
|
||||
public StartupChecker(Configuration config, CancellationToken cancelToken)
|
||||
{
|
||||
this.config = config;
|
||||
this.cancelToken = cancelToken;
|
||||
LogReplacements = new List<BaseLogStringReplacement>();
|
||||
}
|
||||
|
||||
public void Check()
|
||||
{
|
||||
var log = new FixtureLog(new LogConfig(config.LogPath, false), DateTime.UtcNow, "StartupChecks");
|
||||
log.Log("Starting continuous test run...");
|
||||
log.Log("Checking configuration...");
|
||||
PreflightCheck(config);
|
||||
log.Log("Contacting Codex nodes...");
|
||||
CheckCodexNodes(log, config);
|
||||
log.Log("All OK.");
|
||||
}
|
||||
|
||||
public List<BaseLogStringReplacement> LogReplacements { get; }
|
||||
|
||||
private void PreflightCheck(Configuration config)
|
||||
{
|
||||
var tests = testFactory.CreateTests();
|
||||
if (!tests.Any())
|
||||
{
|
||||
throw new Exception("Unable to find any tests.");
|
||||
}
|
||||
foreach (var test in tests)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
var handle = new TestHandle(test);
|
||||
handle.GetEarliestMoment();
|
||||
handle.GetLastMoment();
|
||||
}
|
||||
|
||||
if (!Directory.Exists(config.LogPath))
|
||||
{
|
||||
Directory.CreateDirectory(config.LogPath);
|
||||
}
|
||||
|
||||
var errors = CheckTests(tests);
|
||||
if (errors.Any())
|
||||
{
|
||||
throw new Exception("Prerun check failed: " + string.Join(", ", errors));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckCodexNodes(BaseLog log, Configuration config)
|
||||
{
|
||||
var nodes = codexNodeFactory.Create(config, config.CodexDeployment.CodexContainers, log, new DefaultTimeSet());
|
||||
var pass = true;
|
||||
foreach (var n in nodes)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
log.Log($"Checking {n.Container.Name} @ '{n.Address.Host}:{n.Address.Port}'...");
|
||||
|
||||
if (EnsureOnline(log, n))
|
||||
{
|
||||
log.Log("OK");
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Error($"No response from '{n.Address.Host}'.");
|
||||
pass = false;
|
||||
}
|
||||
}
|
||||
if (!pass)
|
||||
{
|
||||
throw new Exception("Not all codex nodes responded.");
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureOnline(BaseLog log, CodexAccess n)
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = n.GetDebugInfo();
|
||||
if (info == null || string.IsNullOrEmpty(info.id)) return false;
|
||||
|
||||
log.Log($"Codex version: '{info.codex.version}' revision: '{info.codex.revision}'");
|
||||
LogReplacements.Add(new BaseLogStringReplacement(info.id, n.GetName()));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<string> CheckTests(ContinuousTest[] tests)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
CheckRequiredNumberOfNodes(tests, errors);
|
||||
CheckCustomNamespaceClashes(tests, errors);
|
||||
CheckEthereumIndexClashes(tests, errors);
|
||||
return errors;
|
||||
}
|
||||
|
||||
private void CheckEthereumIndexClashes(ContinuousTest[] tests, List<string> errors)
|
||||
{
|
||||
var offLimits = config.CodexDeployment.CodexContainers.Length;
|
||||
foreach (var test in tests)
|
||||
{
|
||||
if (test.EthereumAccountIndex != -1)
|
||||
{
|
||||
if (test.EthereumAccountIndex <= offLimits)
|
||||
{
|
||||
errors.Add($"Test '{test.Name}' has selected 'EthereumAccountIndex' = {test.EthereumAccountIndex}. All accounts up to and including {offLimits} are being used by the targetted Codex net. Select a different 'EthereumAccountIndex'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DuplicatesCheck(tests, errors,
|
||||
considerCondition: t => t.EthereumAccountIndex != -1,
|
||||
getValue: t => t.EthereumAccountIndex,
|
||||
propertyName: nameof(ContinuousTest.EthereumAccountIndex));
|
||||
}
|
||||
|
||||
private void CheckCustomNamespaceClashes(ContinuousTest[] tests, List<string> errors)
|
||||
{
|
||||
DuplicatesCheck(tests, errors,
|
||||
considerCondition: t => !string.IsNullOrEmpty(t.CustomK8sNamespace),
|
||||
getValue: t => t.CustomK8sNamespace,
|
||||
propertyName: nameof(ContinuousTest.CustomK8sNamespace));
|
||||
}
|
||||
|
||||
private void DuplicatesCheck(ContinuousTest[] tests, List<string> errors, Func<ContinuousTest, bool> considerCondition, Func<ContinuousTest, object> getValue, string propertyName)
|
||||
{
|
||||
foreach (var test in tests)
|
||||
{
|
||||
if (considerCondition(test))
|
||||
{
|
||||
var duplicates = tests.Where(t => t != test && getValue(t) == getValue(test)).ToList();
|
||||
if (duplicates.Any())
|
||||
{
|
||||
duplicates.Add(test);
|
||||
errors.Add($"Tests '{string.Join(",", duplicates.Select(d => d.Name))}' have the same '{propertyName}'. These must be unique.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckRequiredNumberOfNodes(ContinuousTest[] tests, List<string> errors)
|
||||
{
|
||||
foreach (var test in tests)
|
||||
{
|
||||
if (test.RequiredNumberOfNodes != -1)
|
||||
{
|
||||
if (test.RequiredNumberOfNodes < 1)
|
||||
{
|
||||
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Test must require > 0 nodes, or -1 to select all nodes.");
|
||||
}
|
||||
else if (test.RequiredNumberOfNodes > config.CodexDeployment.CodexContainers.Length)
|
||||
{
|
||||
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Deployment only has {config.CodexDeployment.CodexContainers.Length}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class TaskFactory
|
||||
{
|
||||
private readonly object taskLock = new();
|
||||
private readonly List<Task> activeTasks = new List<Task>();
|
||||
|
||||
public void Run(Action action)
|
||||
{
|
||||
lock (taskLock)
|
||||
{
|
||||
activeTasks.Add(Task.Run(action).ContinueWith(CleanupTask, null));
|
||||
}
|
||||
}
|
||||
|
||||
public void WaitAll()
|
||||
{
|
||||
var tasks = activeTasks.ToArray();
|
||||
Task.WaitAll(tasks);
|
||||
|
||||
var moreTasks = false;
|
||||
lock (taskLock)
|
||||
{
|
||||
activeTasks.RemoveAll(task => task.IsCompleted);
|
||||
moreTasks = activeTasks.Any();
|
||||
}
|
||||
|
||||
if (moreTasks) WaitAll();
|
||||
}
|
||||
|
||||
private void CleanupTask(Task completedTask, object? arg)
|
||||
{
|
||||
lock (taskLock)
|
||||
{
|
||||
activeTasks.Remove(completedTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class TestFactory
|
||||
{
|
||||
public ContinuousTest[] CreateTests()
|
||||
{
|
||||
var types = GetType().Assembly.GetTypes();
|
||||
var testTypes = types.Where(t => typeof(ContinuousTest).IsAssignableFrom(t) && !t.IsAbstract);
|
||||
return testTypes.Select(t => (ContinuousTest)Activator.CreateInstance(t)!).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using System.Reflection;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class TestHandle
|
||||
{
|
||||
private readonly List<MethodMoment> moments = new List<MethodMoment>();
|
||||
|
||||
public TestHandle(ContinuousTest test)
|
||||
{
|
||||
Test = test;
|
||||
|
||||
ReflectTestMoments();
|
||||
|
||||
var testName = test.GetType().Name;
|
||||
if (!moments.Any()) throw new Exception($"Test '{testName}' has no moments.");
|
||||
if (moments.Count != moments.Select(m => m.Moment).Distinct().Count()) throw new Exception($"Test '{testName}' has duplicate moments");
|
||||
}
|
||||
|
||||
public ContinuousTest Test { get; }
|
||||
|
||||
public int GetEarliestMoment()
|
||||
{
|
||||
return moments.Min(m => m.Moment);
|
||||
}
|
||||
|
||||
public int GetLastMoment()
|
||||
{
|
||||
return moments.Max(m => m.Moment);
|
||||
}
|
||||
|
||||
public int? GetNextMoment(int currentMoment)
|
||||
{
|
||||
var remainingMoments = moments.Where(m => m.Moment > currentMoment).ToArray();
|
||||
if (!remainingMoments.Any()) return null;
|
||||
return remainingMoments.Min(m => m.Moment);
|
||||
}
|
||||
|
||||
public void InvokeMoment(int currentMoment, Action<string> beforeInvoke)
|
||||
{
|
||||
var moment = moments.SingleOrDefault(m => m.Moment == currentMoment);
|
||||
if (moment == null) return;
|
||||
|
||||
lock (MomentLock.Lock)
|
||||
{
|
||||
beforeInvoke(moment.Method.Name);
|
||||
moment.Method.Invoke(Test, Array.Empty<object>());
|
||||
}
|
||||
}
|
||||
|
||||
private void ReflectTestMoments()
|
||||
{
|
||||
var methods = Test.GetType().GetMethods()
|
||||
.Where(m => m.GetCustomAttributes(typeof(TestMomentAttribute), false).Length > 0)
|
||||
.ToArray();
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var moment = method.GetCustomAttribute<TestMomentAttribute>();
|
||||
if (moment != null && moment.T >= 0)
|
||||
{
|
||||
moments.Add(new MethodMoment(method, moment.T));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MethodMoment
|
||||
{
|
||||
public MethodMoment(MethodInfo method, int moment)
|
||||
{
|
||||
Method = method;
|
||||
Moment = moment;
|
||||
|
||||
if (moment < 0) throw new Exception("Moment must be zero or greater.");
|
||||
}
|
||||
|
||||
public MethodInfo Method { get; }
|
||||
public int Moment { get; }
|
||||
}
|
||||
|
||||
public static class MomentLock
|
||||
{
|
||||
public static readonly object Lock = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Logging;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class TestLoop
|
||||
{
|
||||
private readonly TaskFactory taskFactory;
|
||||
private readonly Configuration config;
|
||||
private readonly BaseLog overviewLog;
|
||||
private readonly Type testType;
|
||||
private readonly TimeSpan runsEvery;
|
||||
private readonly StartupChecker startupChecker;
|
||||
private readonly CancellationToken cancelToken;
|
||||
private readonly EventWaitHandle runFinishedHandle = new EventWaitHandle(true, EventResetMode.ManualReset);
|
||||
|
||||
public TestLoop(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, Type testType, TimeSpan runsEvery, StartupChecker startupChecker, CancellationToken cancelToken)
|
||||
{
|
||||
this.taskFactory = taskFactory;
|
||||
this.config = config;
|
||||
this.overviewLog = overviewLog;
|
||||
this.testType = testType;
|
||||
this.runsEvery = runsEvery;
|
||||
this.startupChecker = startupChecker;
|
||||
this.cancelToken = cancelToken;
|
||||
Name = testType.Name;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public void Begin()
|
||||
{
|
||||
taskFactory.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
WaitHandle.WaitAny(new[] { runFinishedHandle, cancelToken.WaitHandle });
|
||||
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
|
||||
StartTest();
|
||||
|
||||
cancelToken.WaitHandle.WaitOne(runsEvery);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
overviewLog.Log("Test-loop " + testType.Name + " is cancelled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
overviewLog.Error("Test infra failure: TestLoop failed with " + ex);
|
||||
Environment.Exit(-1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void StartTest()
|
||||
{
|
||||
var test = (ContinuousTest)Activator.CreateInstance(testType)!;
|
||||
var handle = new TestHandle(test);
|
||||
var run = new SingleTestRun(taskFactory, config, overviewLog, handle, startupChecker, cancelToken);
|
||||
|
||||
runFinishedHandle.Reset();
|
||||
run.Run(runFinishedHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ContinuousTests
|
||||
{
|
||||
public class TestMomentAttribute : Attribute
|
||||
{
|
||||
public TestMomentAttribute(int t)
|
||||
{
|
||||
T = t;
|
||||
}
|
||||
|
||||
public int T { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using DistTestCore.Helpers;
|
||||
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class FetchTest : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => -1;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(2);
|
||||
public override TestFailMode TestFailMode => TestFailMode.AlwaysRunAllMoments;
|
||||
|
||||
[TestMoment(t: 0)]
|
||||
public void CheckConnectivity()
|
||||
{
|
||||
var checker = new PeerFetchTestHelpers(Log, FileManager);
|
||||
checker.AssertFullFetchInterconnectivity(Nodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using DistTestCore;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class HoldMyBeerTest : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => 1;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(2);
|
||||
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
|
||||
private ContentId? cid;
|
||||
private TestFile file = null!;
|
||||
|
||||
[TestMoment(t: Zero)]
|
||||
public void UploadTestFile()
|
||||
{
|
||||
var filesize = 80.MB();
|
||||
|
||||
file = FileManager.GenerateTestFile(filesize);
|
||||
|
||||
cid = UploadFile(Nodes[0], file);
|
||||
Assert.That(cid, Is.Not.Null);
|
||||
|
||||
var dl = DownloadFile(Nodes[0], cid!);
|
||||
file.AssertIsEqual(dl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//using DistTestCore;
|
||||
//using DistTestCore.Codex;
|
||||
//using Newtonsoft.Json;
|
||||
//using NUnit.Framework;
|
||||
//using Utils;
|
||||
|
||||
//namespace ContinuousTests.Tests
|
||||
//{
|
||||
// public class MarketplaceTest : ContinuousTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 1;
|
||||
// public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(10);
|
||||
// public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
// public override int EthereumAccountIndex => 200;
|
||||
// public override string CustomK8sNamespace => "codex-continuous-marketplace";
|
||||
|
||||
// private readonly uint numberOfSlots = 3;
|
||||
// private readonly ByteSize fileSize = 10.MB();
|
||||
// private readonly TestToken pricePerSlotPerSecond = 10.TestTokens();
|
||||
|
||||
// private TestFile file = null!;
|
||||
// private ContentId? cid;
|
||||
// private string purchaseId = string.Empty;
|
||||
|
||||
// [TestMoment(t: Zero)]
|
||||
// public void NodePostsStorageRequest()
|
||||
// {
|
||||
// var contractDuration = TimeSpan.FromMinutes(8);
|
||||
// decimal totalDurationSeconds = Convert.ToDecimal(contractDuration.TotalSeconds);
|
||||
// var expectedTotalCost = numberOfSlots * pricePerSlotPerSecond.Amount * (totalDurationSeconds + 1) * 1000000;
|
||||
|
||||
// file = FileManager.GenerateTestFile(fileSize);
|
||||
|
||||
// NodeRunner.RunNode((codexAccess, marketplaceAccess) =>
|
||||
// {
|
||||
// cid = UploadFile(codexAccess.Node, file);
|
||||
// Assert.That(cid, Is.Not.Null);
|
||||
|
||||
// purchaseId = marketplaceAccess.RequestStorage(
|
||||
// contentId: cid!,
|
||||
// pricePerSlotPerSecond: pricePerSlotPerSecond,
|
||||
// requiredCollateral: 100.TestTokens(),
|
||||
// minRequiredNumberOfNodes: numberOfSlots,
|
||||
// proofProbability: 10,
|
||||
// duration: contractDuration);
|
||||
|
||||
// Assert.That(!string.IsNullOrEmpty(purchaseId));
|
||||
|
||||
// WaitForContractToStart(codexAccess, purchaseId);
|
||||
// });
|
||||
// }
|
||||
|
||||
// [TestMoment(t: MinuteFive + MinuteOne)]
|
||||
// public void StoredDataIsAvailableAfterThreeDays()
|
||||
// {
|
||||
// NodeRunner.RunNode((codexAccess, marketplaceAccess) =>
|
||||
// {
|
||||
// var result = DownloadFile(codexAccess.Node, cid!);
|
||||
|
||||
// file.AssertIsEqual(result);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,57 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Helpers;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class PeersTest : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => -1;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(2);
|
||||
public override TestFailMode TestFailMode => TestFailMode.AlwaysRunAllMoments;
|
||||
|
||||
[TestMoment(t: 0)]
|
||||
public void CheckConnectivity()
|
||||
{
|
||||
var checker = new PeerConnectionTestHelpers(Log);
|
||||
checker.AssertFullyConnected(Nodes);
|
||||
}
|
||||
|
||||
[TestMoment(t: 10)]
|
||||
public void CheckRoutingTables()
|
||||
{
|
||||
var allInfos = Nodes.Select(n =>
|
||||
{
|
||||
var info = n.GetDebugInfo();
|
||||
Log.Log($"{n.GetName()} = {info.table.localNode.nodeId}");
|
||||
Log.AddStringReplace(info.table.localNode.nodeId, n.GetName());
|
||||
return info;
|
||||
}).ToArray();
|
||||
|
||||
var allIds = allInfos.Select(i => i.table.localNode.nodeId).ToArray();
|
||||
var errors = Nodes.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray();
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
Assert.Fail(string.Join(Environment.NewLine, errors));
|
||||
}
|
||||
}
|
||||
|
||||
private string AreAllPresent(CodexAccess n, string[] allIds)
|
||||
{
|
||||
var info = n.GetDebugInfo();
|
||||
var known = info.table.nodes.Select(n => n.nodeId).ToArray();
|
||||
var expected = allIds.Where(i => i != info.table.localNode.nodeId).ToArray();
|
||||
|
||||
if (!expected.All(ex => known.Contains(ex)))
|
||||
{
|
||||
var nl = Environment.NewLine;
|
||||
return $"{nl}At node '{info.table.localNode.nodeId}'{nl}" +
|
||||
$"Not all of{nl}'{string.Join(",", expected)}'{nl}" +
|
||||
$"were present in routing table:{nl}'{string.Join(",", known)}'";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
//using DistTestCore;
|
||||
//using DistTestCore.Codex;
|
||||
//using NUnit.Framework;
|
||||
|
||||
//namespace ContinuousTests.Tests
|
||||
//{
|
||||
// public class UploadPerformanceTest : PerformanceTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 1;
|
||||
|
||||
// [TestMoment(t: Zero)]
|
||||
// public void UploadTest()
|
||||
// {
|
||||
// UploadTest(100, Nodes[0]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public class DownloadLocalPerformanceTest : PerformanceTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 1;
|
||||
|
||||
// [TestMoment(t: Zero)]
|
||||
// public void DownloadTest()
|
||||
// {
|
||||
// DownloadTest(100, Nodes[0], Nodes[0]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public class DownloadRemotePerformanceTest : PerformanceTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 2;
|
||||
|
||||
// [TestMoment(t: Zero)]
|
||||
// public void DownloadTest()
|
||||
// {
|
||||
// DownloadTest(100, Nodes[0], Nodes[1]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// public abstract class PerformanceTest : ContinuousTest
|
||||
// {
|
||||
// public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(10);
|
||||
// public override TestFailMode TestFailMode => TestFailMode.AlwaysRunAllMoments;
|
||||
|
||||
// public void UploadTest(int megabytes, CodexAccess uploadNode)
|
||||
// {
|
||||
// var file = FileManager.GenerateTestFile(megabytes.MB());
|
||||
|
||||
// var time = Measure(() =>
|
||||
// {
|
||||
// UploadFile(uploadNode, file);
|
||||
// });
|
||||
|
||||
// var timePerMB = time / megabytes;
|
||||
|
||||
// Assert.That(timePerMB, Is.LessThan(CodexContainerRecipe.MaxUploadTimePerMegabyte), "MaxUploadTimePerMegabyte performance threshold breached.");
|
||||
// }
|
||||
|
||||
// public void DownloadTest(int megabytes, CodexAccess uploadNode, CodexAccess downloadNode)
|
||||
// {
|
||||
// var file = FileManager.GenerateTestFile(megabytes.MB());
|
||||
|
||||
// var cid = UploadFile(uploadNode, file);
|
||||
// Assert.That(cid, Is.Not.Null);
|
||||
|
||||
// TestFile? result = null;
|
||||
// var time = Measure(() =>
|
||||
// {
|
||||
// result = DownloadFile(downloadNode, cid!);
|
||||
// });
|
||||
|
||||
// file.AssertIsEqual(result);
|
||||
|
||||
// var timePerMB = time / megabytes;
|
||||
|
||||
// Assert.That(timePerMB, Is.LessThan(CodexContainerRecipe.MaxDownloadTimePerMegabyte), "MaxDownloadTimePerMegabyte performance threshold breached.");
|
||||
// }
|
||||
|
||||
// private static TimeSpan Measure(Action action)
|
||||
// {
|
||||
// var start = DateTime.UtcNow;
|
||||
// action();
|
||||
// return DateTime.UtcNow - start;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,60 @@
|
||||
using DistTestCore;
|
||||
using DistTestCore.Codex;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class ThresholdChecks : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => 1;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromSeconds(30);
|
||||
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
|
||||
private static readonly List<string> previousBreaches = new List<string>();
|
||||
|
||||
[TestMoment(t: 0)]
|
||||
public void CheckAllThresholds()
|
||||
{
|
||||
var allNodes = CreateAccessToAllNodes();
|
||||
foreach (var n in allNodes) CheckThresholds(n);
|
||||
}
|
||||
|
||||
private void CheckThresholds(CodexAccess n)
|
||||
{
|
||||
var breaches = n.GetDebugThresholdBreaches();
|
||||
if (breaches.breaches.Any())
|
||||
{
|
||||
var newBreaches = new List<string>();
|
||||
foreach (var b in breaches.breaches)
|
||||
{
|
||||
if (!previousBreaches.Contains(b))
|
||||
{
|
||||
newBreaches.Add(b);
|
||||
previousBreaches.Add(b);
|
||||
}
|
||||
}
|
||||
|
||||
if (newBreaches.Any())
|
||||
{
|
||||
Assert.Fail(string.Join(",", newBreaches.Select(b => FormatBreach(n, b))));
|
||||
|
||||
Program.Cancellation.Cts.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string FormatBreach(CodexAccess n, string breach)
|
||||
{
|
||||
return $"{n.Container.Name} = '{breach}'";
|
||||
}
|
||||
|
||||
private CodexAccess[] CreateAccessToAllNodes()
|
||||
{
|
||||
// Normally, a continuous test accesses only a subset of the nodes in the deployment.
|
||||
// This time, we want to check all of them.
|
||||
var factory = new CodexAccessFactory();
|
||||
var allContainers = Configuration.CodexDeployment.CodexContainers;
|
||||
return factory.Create(Configuration, allContainers, Log, new DefaultTimeSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//using DistTestCore;
|
||||
//using DistTestCore.Codex;
|
||||
//using NUnit.Framework;
|
||||
|
||||
//namespace ContinuousTests.Tests
|
||||
//{
|
||||
// public class TransientNodeTest : ContinuousTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 3;
|
||||
// public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(1);
|
||||
// public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
// public override string CustomK8sNamespace => nameof(TransientNodeTest).ToLowerInvariant();
|
||||
// public override int EthereumAccountIndex => 201;
|
||||
|
||||
// private TestFile file = null!;
|
||||
// private ContentId cid = null!;
|
||||
|
||||
// private CodexAccess UploadBootstapNode { get { return Nodes[0]; } }
|
||||
// private CodexAccess DownloadBootstapNode { get { return Nodes[1]; } }
|
||||
// private CodexAccess IntermediateNode { get { return Nodes[2]; } }
|
||||
|
||||
// [TestMoment(t: 0)]
|
||||
// public void UploadWithTransientNode()
|
||||
// {
|
||||
// file = FileManager.GenerateTestFile(10.MB());
|
||||
|
||||
// NodeRunner.RunNode(UploadBootstapNode, (codexAccess, marketplaceAccess, lifecycle) =>
|
||||
// {
|
||||
// cid = UploadFile(codexAccess, file)!;
|
||||
// Assert.That(cid, Is.Not.Null);
|
||||
|
||||
// var dlt = Task.Run(() =>
|
||||
// {
|
||||
// Thread.Sleep(10000);
|
||||
// lifecycle.DownloadLog(codexAccess.Container);
|
||||
// });
|
||||
|
||||
// var resultFile = DownloadFile(IntermediateNode, cid);
|
||||
// dlt.Wait();
|
||||
// file.AssertIsEqual(resultFile);
|
||||
// });
|
||||
// }
|
||||
|
||||
// [TestMoment(t: 30)]
|
||||
// public void DownloadWithTransientNode()
|
||||
// {
|
||||
// NodeRunner.RunNode(DownloadBootstapNode, (codexAccess, marketplaceAccess, lifecycle) =>
|
||||
// {
|
||||
// var resultFile = DownloadFile(codexAccess, cid);
|
||||
// file.AssertIsEqual(resultFile);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,32 @@
|
||||
using DistTestCore;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class TwoClientTest : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => 2;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(2);
|
||||
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
|
||||
private ContentId? cid;
|
||||
private TestFile file = null!;
|
||||
|
||||
[TestMoment(t: Zero)]
|
||||
public void UploadTestFile()
|
||||
{
|
||||
file = FileManager.GenerateTestFile(80.MB());
|
||||
|
||||
cid = UploadFile(Nodes[0], file);
|
||||
Assert.That(cid, Is.Not.Null);
|
||||
}
|
||||
|
||||
[TestMoment(t: 10)]
|
||||
public void DownloadTestFile()
|
||||
{
|
||||
var dl = DownloadFile(Nodes[1], cid!);
|
||||
|
||||
file.AssertIsEqual(dl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Codex Continuous Test-net Report
|
||||
Date: 05-09-2023
|
||||
|
||||
Report for: 08-2023
|
||||
|
||||
|
||||
## Test-net Status
|
||||
- Start of month: Offline - faulted
|
||||
- End of month: Offline - stopped
|
||||
|
||||
(Faulted: Tests fail with such frequency that the information gathered does not justify the cost of leaving the test-net running.)
|
||||
|
||||
(Stopped: The number of tests that can successfully run on the test-net is not high enough to justify the cost of leaving it running.)
|
||||
|
||||
## Deployment Configuration
|
||||
Continous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
|
||||
5x Codex Nodes:
|
||||
- Log-level: Trace
|
||||
- Storage quota: 2048 MB
|
||||
- Storage sell: 1024 MB
|
||||
- Min price: 1024
|
||||
- Max collateral: 1024
|
||||
- Max duration: 3600000 seconds
|
||||
- Block-TTL: 180 seconds
|
||||
- Block-MI: 120 seconds
|
||||
- Block-MN: 10000 blocks
|
||||
|
||||
3 of these 5 nodes have:
|
||||
- Validator: true
|
||||
|
||||
Kubernetes namespace: 'codex-continuous-tests'
|
||||
|
||||
## Test Overview
|
||||
| Changes | Test | Description | Status | Results |
|
||||
|----------------|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|---------------------------------------------------------------------------------------|
|
||||
| No change | Two-client test | See report for July 2023. | Faulted | Test reliably fails. Both upload and download failures occur. |
|
||||
| No change | Transient-node test | See report for July 2023. | Not running | Test was not run because the previous more rudamentory test has faulted. |
|
||||
| New in 08-2023 | HoldMyBeer test | Named so because the test is quite dumb and only holds a file, this test selects 1 node at random and uploads an 80 MB file. Then it downloads and verifies it. The purpose of this test is to isolate the upload/download behavior to identify issues in it. | Passed | Several successful runs. Record run: 48h. After that the test was manually stopped. |
|
||||
| New in 08-2023 | Peers test | Another attempt to isolate a portion of the two-client test behavior. This test periodically checks the connectivity status between each node in the network. It does this by analysing routing-table information and by performing a node-find operation. | Passed | Several successful runs. Record run: 2d21h. After that the test was manually stopped. |
|
||||
|
||||
## Resulting changes
|
||||
As a result of the testing efforts in 08-2023, these three changes were made:
|
||||
1. Block announcement performance - Block iteration (as part of the advertising loop) was found to be performing too much disc IO. Reducing this greatly increased client stability.
|
||||
1. Component initialization order - A timing issue in the initialization order of the several Codex components caused nodes to improperly announce their addresses over the network in case the marketplace support was enabled.
|
||||
1. Block maintenance performance - Block iteration (again but this time in maintenance) was causing the node to not respond to Ping messages quickly enough, causing other nodes to kick it from the network. By slowing down block maintenance this is solved. A future update of the DHT logic will also impact this behavior.
|
||||
|
||||
## Action Points
|
||||
- Despite both HoldMyBeer and Peers test success, the two-client test is still not passing. In follow-up actions, we can investigate this issue further by decoupling block retrieval from file download.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Codex Continuous Test-net Report
|
||||
Date: 02-08-2023
|
||||
|
||||
Report for: 07-2023
|
||||
|
||||
|
||||
## Test-net Status
|
||||
- Start of month: Offline - faulted
|
||||
- End of month: Offline - faulted
|
||||
|
||||
(Faulted: Tests fail with such frequency that the information gathered does not justify the cost of leaving the test-net running.)
|
||||
|
||||
## Deployment Configuration
|
||||
Continous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
|
||||
5x Codex Nodes:
|
||||
- Log-level: Trace
|
||||
- Storage quota: 2048 MB
|
||||
- Storage sell: 1024 MB
|
||||
- Min price: 1024
|
||||
- Max collateral: 1024
|
||||
- Max duration: 3600000 seconds
|
||||
- Block-TTL: 120 seconds
|
||||
|
||||
3 of these 5 nodes have:
|
||||
- Validator: true
|
||||
|
||||
Kubernetes namespace: 'codex-continuous-tests'
|
||||
|
||||
## Test Overview
|
||||
| Changes | Test | Description | Status | Results |
|
||||
|----------------|---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|--------------------------------------------------------------------------------------|
|
||||
| New in 07-2023 | Two-client test | Every 30 seconds, two nodes are chosen at random. A 10 MB file is generated and uploaded to one. 10 seconds later, it is downloaded from the other. File contents are asserted to be equal. | Faulted | Test reliably fails after 30 to 45 minutes. Both upload and download failures occur. |
|
||||
| New in 07-2023 | Transient-node test | Every 1 minute, a new, transient Codex node is started and bootstrapped against a random node of the test net. A 10 MB file is generated and uploaded to the transient node. The file is then downloaded from a second random test net node and file equality is asserted. After that, the transient node is shut down. 30 seconds later, a new transient Codex node is started and bootstrapped against a third (guaranteed different from first and second) node from the test net. The same file is then downloaded from the new transient node. File equality is asserted. | Not running | Test was not run because the previous more rudamentory test has faulted. |
|
||||
|
||||
## Action Points
|
||||
- Codex logs (from these long-running containers) are often incomplete when downloaded after a test failure. A reliable way of maintaining these logs is needed. The logs can quickly explode in size, even for test-nets that run only for a few hours.
|
||||
- The distributed testing setup can and has been used to reproduce the failure of the Two-client test in a local environment. Investigation is on-going.
|
||||
@@ -0,0 +1,6 @@
|
||||
dotnet run \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--codex-deployment=codex-deployment.json \
|
||||
--keep=1 \
|
||||
--stop=10 \
|
||||
--dl-logs=1
|
||||
@@ -0,0 +1,30 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class AutoBootstrapDistTest : DistTest
|
||||
{
|
||||
public override IOnlineCodexNode SetupCodexBootstrapNode(Action<ICodexSetup> setup)
|
||||
{
|
||||
throw new Exception("AutoBootstrapDistTest creates and attaches a single bootstrap node for you. " +
|
||||
"If you want to control the bootstrap node from your test, please use DistTest instead.");
|
||||
}
|
||||
|
||||
public override ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
|
||||
{
|
||||
var codexSetup = CreateCodexSetup(numberOfNodes);
|
||||
setup(codexSetup);
|
||||
codexSetup.WithBootstrapNode(BootstrapNode);
|
||||
return BringOnline(codexSetup);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUpBootstrapNode()
|
||||
{
|
||||
var setup = CreateCodexSetup(1).WithName("BOOTSTRAP");
|
||||
BootstrapNode = BringOnline(setup)[0];
|
||||
}
|
||||
|
||||
protected IOnlineCodexNode BootstrapNode { get; private set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Logging;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class BaseStarter
|
||||
{
|
||||
protected readonly TestLifecycle lifecycle;
|
||||
private Stopwatch? stopwatch;
|
||||
|
||||
public BaseStarter(TestLifecycle lifecycle)
|
||||
{
|
||||
this.lifecycle = lifecycle;
|
||||
}
|
||||
|
||||
protected void LogStart(string msg)
|
||||
{
|
||||
Log(msg);
|
||||
stopwatch = Stopwatch.Begin(lifecycle.Log, GetClassName());
|
||||
}
|
||||
|
||||
protected void LogEnd(string msg)
|
||||
{
|
||||
stopwatch!.End(msg);
|
||||
stopwatch = null;
|
||||
}
|
||||
|
||||
protected void Log(string msg)
|
||||
{
|
||||
lifecycle.Log.Log($"{GetClassName()} {msg}");
|
||||
}
|
||||
|
||||
protected void Debug(string msg)
|
||||
{
|
||||
lifecycle.Log.Debug($"{GetClassName()} {msg}", 1);
|
||||
}
|
||||
|
||||
private string GetClassName()
|
||||
{
|
||||
return $"({GetType().Name})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,48 @@
|
||||
namespace CodexDistTestCore
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class ByteSize
|
||||
{
|
||||
|
||||
public ByteSize(long sizeInBytes)
|
||||
{
|
||||
if (sizeInBytes < 0) throw new ArgumentException("Cannot create ByteSize object with size less than 0. Was: " + sizeInBytes);
|
||||
SizeInBytes = sizeInBytes;
|
||||
}
|
||||
|
||||
public long SizeInBytes { get; }
|
||||
|
||||
public long ToMB()
|
||||
{
|
||||
return SizeInBytes / (1024 * 1024);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ByteSize size && SizeInBytes == size.SizeInBytes;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(SizeInBytes);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Formatter.FormatByteSize(SizeInBytes);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IntExtensions
|
||||
public static class ByteSizeIntExtensions
|
||||
{
|
||||
private const long Kilo = 1024;
|
||||
|
||||
public static ByteSize Bytes(this long i)
|
||||
{
|
||||
return new ByteSize(i);
|
||||
}
|
||||
|
||||
public static ByteSize KB(this long i)
|
||||
{
|
||||
return new ByteSize(i * Kilo);
|
||||
@@ -34,6 +63,11 @@
|
||||
return (i * Kilo).GB();
|
||||
}
|
||||
|
||||
public static ByteSize Bytes(this int i)
|
||||
{
|
||||
return new ByteSize(i);
|
||||
}
|
||||
|
||||
public static ByteSize KB(this int i)
|
||||
{
|
||||
return Convert.ToInt64(i).KB();
|
||||
@@ -0,0 +1,123 @@
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore.Codex
|
||||
{
|
||||
public class CodexAccess : ILogHandler
|
||||
{
|
||||
private readonly BaseLog log;
|
||||
private readonly ITimeSet timeSet;
|
||||
private bool hasContainerCrashed;
|
||||
|
||||
public CodexAccess(BaseLog log, RunningContainer container, ITimeSet timeSet, Address address)
|
||||
{
|
||||
this.log = log;
|
||||
Container = container;
|
||||
this.timeSet = timeSet;
|
||||
Address = address;
|
||||
hasContainerCrashed = false;
|
||||
|
||||
if (container.CrashWatcher != null) container.CrashWatcher.Start(this);
|
||||
}
|
||||
|
||||
public RunningContainer Container { get; }
|
||||
public Address Address { get; }
|
||||
|
||||
public CodexDebugResponse GetDebugInfo()
|
||||
{
|
||||
return Http().HttpGetJson<CodexDebugResponse>("debug/info");
|
||||
}
|
||||
|
||||
public CodexDebugPeerResponse GetDebugPeer(string peerId)
|
||||
{
|
||||
var http = Http();
|
||||
var str = http.HttpGetString($"debug/peer/{peerId}");
|
||||
|
||||
if (str.ToLowerInvariant() == "unable to find peer!")
|
||||
{
|
||||
return new CodexDebugPeerResponse
|
||||
{
|
||||
IsPeerFound = false
|
||||
};
|
||||
}
|
||||
|
||||
var result = http.TryJsonDeserialize<CodexDebugPeerResponse>(str);
|
||||
result.IsPeerFound = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
public CodexDebugFetchResponse DebugFetch(string contentId)
|
||||
{
|
||||
var http = Http();
|
||||
return http.HttpGetJson<CodexDebugFetchResponse>($"debug/fetch/{contentId}");
|
||||
}
|
||||
|
||||
public CodexDebugThresholdBreaches GetDebugThresholdBreaches()
|
||||
{
|
||||
return Http().HttpGetJson<CodexDebugThresholdBreaches>("debug/loop");
|
||||
}
|
||||
|
||||
public string UploadFile(FileStream fileStream)
|
||||
{
|
||||
return Http().HttpPostStream("upload", fileStream);
|
||||
}
|
||||
|
||||
public Stream DownloadFile(string contentId)
|
||||
{
|
||||
return Http().HttpGetStream("download/" + contentId);
|
||||
}
|
||||
|
||||
public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request)
|
||||
{
|
||||
return Http().HttpPostJson<CodexSalesAvailabilityRequest, CodexSalesAvailabilityResponse>("sales/availability", request);
|
||||
}
|
||||
|
||||
public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId)
|
||||
{
|
||||
return Http().HttpPostJson($"storage/request/{contentId}", request);
|
||||
}
|
||||
|
||||
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
|
||||
{
|
||||
return Http().HttpGetJson<CodexStoragePurchase>($"storage/purchases/{purchaseId}");
|
||||
}
|
||||
|
||||
public string ConnectToPeer(string peerId, string peerMultiAddress)
|
||||
{
|
||||
return Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
|
||||
}
|
||||
|
||||
public string GetName()
|
||||
{
|
||||
return Container.Name;
|
||||
}
|
||||
|
||||
private Http Http()
|
||||
{
|
||||
return new Http(log, timeSet, Address, baseUrl: "/api/codex/v1", CheckContainerCrashed, Container.Name);
|
||||
}
|
||||
|
||||
private void CheckContainerCrashed(HttpClient client)
|
||||
{
|
||||
if (hasContainerCrashed) throw new Exception("Container has crashed.");
|
||||
}
|
||||
|
||||
public void Log(Stream crashLog)
|
||||
{
|
||||
var file = log.CreateSubfile();
|
||||
log.Log($"Container {Container.Name} has crashed. Downloading crash log to '{file.FullFilename}'...");
|
||||
|
||||
using var reader = new StreamReader(crashLog);
|
||||
var line = reader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
file.Write(line);
|
||||
line = reader.ReadLine();
|
||||
}
|
||||
|
||||
log.Log("Crash log successfully downloaded.");
|
||||
hasContainerCrashed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace DistTestCore.Codex
|
||||
{
|
||||
public class CodexDebugResponse
|
||||
{
|
||||
public string id { get; set; } = string.Empty;
|
||||
public string[] addrs { get; set; } = new string[0];
|
||||
public string repo { get; set; } = string.Empty;
|
||||
public string spr { get; set; } = string.Empty;
|
||||
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
|
||||
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
|
||||
public CodexDebugVersionResponse codex { get; set; } = new();
|
||||
public CodexDebugTableResponse table { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CodexDebugFutures
|
||||
{
|
||||
public int futures { get; set; }
|
||||
}
|
||||
|
||||
public class CodexDebugTableResponse
|
||||
{
|
||||
public CodexDebugTableNodeResponse localNode { get; set; } = new();
|
||||
public CodexDebugTableNodeResponse[] nodes { get; set; } = Array.Empty<CodexDebugTableNodeResponse>();
|
||||
}
|
||||
|
||||
public class CodexDebugTableNodeResponse
|
||||
{
|
||||
public string nodeId { get; set; } = string.Empty;
|
||||
public string peerId { get; set; } = string.Empty;
|
||||
public string record { get; set; } = string.Empty;
|
||||
public string address { get; set; } = string.Empty;
|
||||
public bool seen { get; set; }
|
||||
}
|
||||
|
||||
public class EnginePeerResponse
|
||||
{
|
||||
public string peerId { get; set; } = string.Empty;
|
||||
public EnginePeerContextResponse context { get; set; } = new();
|
||||
}
|
||||
|
||||
public class EnginePeerContextResponse
|
||||
{
|
||||
public int blocks { get; set; } = 0;
|
||||
public int peerWants { get; set; } = 0;
|
||||
public int exchanged { get; set; } = 0;
|
||||
public string lastExchange { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SwitchPeerResponse
|
||||
{
|
||||
public string peerId { get; set; } = string.Empty;
|
||||
public string key { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CodexDebugVersionResponse
|
||||
{
|
||||
public string version { get; set; } = string.Empty;
|
||||
public string revision { get; set; } = string.Empty;
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(revision);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return JsonConvert.SerializeObject(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class CodexDebugPeerResponse
|
||||
{
|
||||
public bool IsPeerFound { get; set; }
|
||||
|
||||
public string peerId { get; set; } = string.Empty;
|
||||
public long seqNo { get; set; }
|
||||
public CodexDebugPeerAddressResponse[] addresses { get; set; } = Array.Empty<CodexDebugPeerAddressResponse>();
|
||||
}
|
||||
|
||||
public class CodexDebugPeerAddressResponse
|
||||
{
|
||||
public string address { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CodexDebugFetchResponse
|
||||
{
|
||||
public string originalBytes { get; set; } = string.Empty;
|
||||
public string blockSize { get; set; } = string.Empty;
|
||||
public string numberOfBlocks { get; set; } = string.Empty;
|
||||
public string version { get; set; } = string.Empty;
|
||||
public string hcodec { get; set; } = string.Empty;
|
||||
public string codec { get; set; } = string.Empty;
|
||||
public string @protected { get; set; } = string.Empty;
|
||||
public CodexDebugFetchBlockResponse[] blocks { get; set; } = Array.Empty<CodexDebugFetchBlockResponse>();
|
||||
}
|
||||
|
||||
public class CodexDebugFetchBlockResponse
|
||||
{
|
||||
public string cid { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CodexDebugThresholdBreaches
|
||||
{
|
||||
public string[] breaches { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public class CodexSalesAvailabilityRequest
|
||||
{
|
||||
public string size { get; set; } = string.Empty;
|
||||
public string duration { get; set; } = string.Empty;
|
||||
public string minPrice { get; set; } = string.Empty;
|
||||
public string maxCollateral { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CodexSalesAvailabilityResponse
|
||||
{
|
||||
public string id { get; set; } = string.Empty;
|
||||
public string size { get; set; } = string.Empty;
|
||||
public string duration { get; set; } = string.Empty;
|
||||
public string minPrice { get; set; } = string.Empty;
|
||||
public string maxCollateral { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CodexSalesRequestStorageRequest
|
||||
{
|
||||
public string duration { get; set; } = string.Empty;
|
||||
public string proofProbability { get; set; } = string.Empty;
|
||||
public string reward { get; set; } = string.Empty;
|
||||
public string collateral { get; set; } = string.Empty;
|
||||
public string? expiry { get; set; }
|
||||
public uint? nodes { get; set; }
|
||||
public uint? tolerance { get; set; }
|
||||
}
|
||||
|
||||
public class CodexStoragePurchase
|
||||
{
|
||||
public string state { get; set; } = string.Empty;
|
||||
public string error { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
using DistTestCore.Marketplace;
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore.Codex
|
||||
{
|
||||
public class CodexContainerRecipe : DefaultContainerRecipe
|
||||
{
|
||||
//private const string DefaultDockerImage = "codexstorage/nim-codex:sha-d279eeb-dist-tests";
|
||||
private const string DefaultDockerImage = "thatbenbierens/nim-codex:debugfetch";
|
||||
|
||||
public const string MetricsPortTag = "metrics_port";
|
||||
public const string DiscoveryPortTag = "discovery-port";
|
||||
|
||||
// Used by tests for time-constraint assertions.
|
||||
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
|
||||
public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
|
||||
|
||||
public override string AppName => "codex";
|
||||
public override string Image { get; }
|
||||
|
||||
public CodexContainerRecipe()
|
||||
{
|
||||
Image = GetDockerImage();
|
||||
}
|
||||
|
||||
protected override void InitializeRecipe(StartupConfig startupConfig)
|
||||
{
|
||||
var config = startupConfig.Get<CodexStartupConfig>();
|
||||
|
||||
AddExposedPortAndVar("CODEX_API_PORT");
|
||||
AddEnvVar("CODEX_API_BINDADDR", "0.0.0.0");
|
||||
|
||||
AddEnvVar("CODEX_DATA_DIR", $"datadir{ContainerNumber}");
|
||||
AddInternalPortAndVar("CODEX_DISC_PORT", DiscoveryPortTag);
|
||||
AddEnvVar("CODEX_LOG_LEVEL", config.LogLevel.ToString()!.ToUpperInvariant());
|
||||
|
||||
// This makes the node announce itself to its local (pod) IP address.
|
||||
AddEnvVar("NAT_IP_AUTO", "true");
|
||||
|
||||
var listenPort = AddInternalPort();
|
||||
AddEnvVar("CODEX_LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}");
|
||||
|
||||
if (!string.IsNullOrEmpty(config.BootstrapSpr))
|
||||
{
|
||||
AddEnvVar("CODEX_BOOTSTRAP_NODE", config.BootstrapSpr);
|
||||
}
|
||||
if (config.StorageQuota != null)
|
||||
{
|
||||
AddEnvVar("CODEX_STORAGE_QUOTA", config.StorageQuota.SizeInBytes.ToString()!);
|
||||
}
|
||||
if (config.BlockTTL != null)
|
||||
{
|
||||
AddEnvVar("CODEX_BLOCK_TTL", config.BlockTTL.ToString()!);
|
||||
}
|
||||
if (config.BlockMaintenanceInterval != null)
|
||||
{
|
||||
AddEnvVar("CODEX_BLOCK_MI", Convert.ToInt32(config.BlockMaintenanceInterval.Value.TotalSeconds).ToString());
|
||||
}
|
||||
if (config.BlockMaintenanceNumber != null)
|
||||
{
|
||||
AddEnvVar("CODEX_BLOCK_MN", config.BlockMaintenanceNumber.ToString()!);
|
||||
}
|
||||
if (config.MetricsMode != Metrics.MetricsMode.None)
|
||||
{
|
||||
var metricsPort = AddInternalPort(MetricsPortTag);
|
||||
AddEnvVar("CODEX_METRICS", "true");
|
||||
AddEnvVar("CODEX_METRICS_ADDRESS", "0.0.0.0");
|
||||
AddEnvVar("CODEX_METRICS_PORT", metricsPort);
|
||||
AddPodAnnotation("prometheus.io/scrape", "true");
|
||||
AddPodAnnotation("prometheus.io/port", metricsPort.Number.ToString());
|
||||
}
|
||||
|
||||
if (config.MarketplaceConfig != null)
|
||||
{
|
||||
var gethConfig = startupConfig.Get<GethStartResult>();
|
||||
var companionNode = gethConfig.CompanionNode;
|
||||
var companionNodeAccount = companionNode.Accounts[GetAccountIndex(config.MarketplaceConfig)];
|
||||
Additional(companionNodeAccount);
|
||||
|
||||
var ip = companionNode.RunningContainer.Pod.PodInfo.Ip;
|
||||
var port = companionNode.RunningContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag).Number;
|
||||
|
||||
AddEnvVar("CODEX_ETH_PROVIDER", $"ws://{ip}:{port}");
|
||||
AddEnvVar("CODEX_ETH_ACCOUNT", companionNodeAccount.Account);
|
||||
AddEnvVar("CODEX_MARKETPLACE_ADDRESS", gethConfig.MarketplaceNetwork.Marketplace.Address);
|
||||
AddEnvVar("CODEX_PERSISTENCE", "true");
|
||||
|
||||
if (config.MarketplaceConfig.IsValidator)
|
||||
{
|
||||
AddEnvVar("CODEX_VALIDATOR", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int GetAccountIndex(MarketplaceInitialConfig marketplaceConfig)
|
||||
{
|
||||
if (marketplaceConfig.AccountIndexOverride != null) return marketplaceConfig.AccountIndexOverride.Value;
|
||||
return Index;
|
||||
}
|
||||
|
||||
private string GetDockerImage()
|
||||
{
|
||||
var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE");
|
||||
if (!string.IsNullOrEmpty(image)) return image;
|
||||
return DefaultDockerImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using DistTestCore.Marketplace;
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore.Codex
|
||||
{
|
||||
public class CodexDeployment
|
||||
{
|
||||
public CodexDeployment(GethStartResult gethStartResult, RunningContainer[] codexContainers, RunningContainer? prometheusContainer, GrafanaStartInfo? grafanaStartInfo, DeploymentMetadata metadata)
|
||||
{
|
||||
GethStartResult = gethStartResult;
|
||||
CodexContainers = codexContainers;
|
||||
PrometheusContainer = prometheusContainer;
|
||||
GrafanaStartInfo = grafanaStartInfo;
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public GethStartResult GethStartResult { get; }
|
||||
public RunningContainer[] CodexContainers { get; }
|
||||
public RunningContainer? PrometheusContainer { get; }
|
||||
public GrafanaStartInfo? GrafanaStartInfo { get; }
|
||||
public DeploymentMetadata Metadata { get; }
|
||||
}
|
||||
|
||||
public class DeploymentMetadata
|
||||
{
|
||||
public DeploymentMetadata(string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
|
||||
{
|
||||
DeployDateTimeUtc = DateTime.UtcNow;
|
||||
KubeNamespace = kubeNamespace;
|
||||
NumberOfCodexNodes = numberOfCodexNodes;
|
||||
NumberOfValidators = numberOfValidators;
|
||||
StorageQuotaMB = storageQuotaMB;
|
||||
CodexLogLevel = codexLogLevel;
|
||||
InitialTestTokens = initialTestTokens;
|
||||
MinPrice = minPrice;
|
||||
MaxCollateral = maxCollateral;
|
||||
MaxDuration = maxDuration;
|
||||
BlockTTL = blockTTL;
|
||||
BlockMI = blockMI;
|
||||
BlockMN = blockMN;
|
||||
}
|
||||
|
||||
public DateTime DeployDateTimeUtc { get; }
|
||||
public string KubeNamespace { get; }
|
||||
public int NumberOfCodexNodes { get; }
|
||||
public int NumberOfValidators { get; }
|
||||
public int StorageQuotaMB { get; }
|
||||
public CodexLogLevel CodexLogLevel { get; }
|
||||
public int InitialTestTokens { get; }
|
||||
public int MinPrice { get; }
|
||||
public int MaxCollateral { get; }
|
||||
public int MaxDuration { get; }
|
||||
public int BlockTTL { get; }
|
||||
public int BlockMI { get; }
|
||||
public int BlockMN { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace DistTestCore.Codex
|
||||
{
|
||||
public enum CodexLogLevel
|
||||
{
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using DistTestCore.Marketplace;
|
||||
using DistTestCore.Metrics;
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore.Codex
|
||||
{
|
||||
public class CodexStartupConfig
|
||||
{
|
||||
public CodexStartupConfig(CodexLogLevel logLevel)
|
||||
{
|
||||
LogLevel = logLevel;
|
||||
}
|
||||
|
||||
public string? NameOverride { get; set; }
|
||||
public Location Location { get; set; }
|
||||
public CodexLogLevel LogLevel { get; }
|
||||
public ByteSize? StorageQuota { get; set; }
|
||||
public MetricsMode MetricsMode { get; set; }
|
||||
public MarketplaceInitialConfig? MarketplaceConfig { get; set; }
|
||||
public string? BootstrapSpr { get; set; }
|
||||
public int? BlockTTL { get; set; }
|
||||
public TimeSpan? BlockMaintenanceInterval { get; set; }
|
||||
public int? BlockMaintenanceNumber { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Marketplace;
|
||||
using DistTestCore.Metrics;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public interface ICodexNodeFactory
|
||||
{
|
||||
OnlineCodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group);
|
||||
}
|
||||
|
||||
public class CodexNodeFactory : ICodexNodeFactory
|
||||
{
|
||||
private readonly TestLifecycle lifecycle;
|
||||
private readonly IMetricsAccessFactory metricsAccessFactory;
|
||||
private readonly IMarketplaceAccessFactory marketplaceAccessFactory;
|
||||
|
||||
public CodexNodeFactory(TestLifecycle lifecycle, IMetricsAccessFactory metricsAccessFactory, IMarketplaceAccessFactory marketplaceAccessFactory)
|
||||
{
|
||||
this.lifecycle = lifecycle;
|
||||
this.metricsAccessFactory = metricsAccessFactory;
|
||||
this.marketplaceAccessFactory = marketplaceAccessFactory;
|
||||
}
|
||||
|
||||
public OnlineCodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
|
||||
{
|
||||
var metricsAccess = metricsAccessFactory.CreateMetricsAccess(access.Container);
|
||||
var marketplaceAccess = marketplaceAccessFactory.CreateMarketplaceAccess(access);
|
||||
return new OnlineCodexNode(lifecycle, access, group, metricsAccess, marketplaceAccess);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using DistTestCore.Codex;
|
||||
using KubernetesWorkflow;
|
||||
using System.Collections;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public interface ICodexNodeGroup : IEnumerable<IOnlineCodexNode>
|
||||
{
|
||||
ICodexSetup BringOffline();
|
||||
IOnlineCodexNode this[int index] { get; }
|
||||
}
|
||||
|
||||
public class CodexNodeGroup : ICodexNodeGroup
|
||||
{
|
||||
private readonly TestLifecycle lifecycle;
|
||||
|
||||
public CodexNodeGroup(TestLifecycle lifecycle, CodexSetup setup, RunningContainers[] containers, ICodexNodeFactory codexNodeFactory)
|
||||
{
|
||||
this.lifecycle = lifecycle;
|
||||
Setup = setup;
|
||||
Containers = containers;
|
||||
Nodes = containers.Containers().Select(c => CreateOnlineCodexNode(c, codexNodeFactory)).ToArray();
|
||||
Version = new CodexDebugVersionResponse();
|
||||
}
|
||||
|
||||
public IOnlineCodexNode this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
return Nodes[index];
|
||||
}
|
||||
}
|
||||
|
||||
public ICodexSetup BringOffline()
|
||||
{
|
||||
lifecycle.CodexStarter.BringOffline(this);
|
||||
|
||||
var result = Setup;
|
||||
// Clear everything. Prevent accidental use.
|
||||
Setup = null!;
|
||||
Nodes = Array.Empty<OnlineCodexNode>();
|
||||
Containers = null!;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public CodexSetup Setup { get; private set; }
|
||||
public RunningContainers[] Containers { get; private set; }
|
||||
public OnlineCodexNode[] Nodes { get; private set; }
|
||||
public CodexDebugVersionResponse Version { get; private set; }
|
||||
|
||||
public IEnumerator<IOnlineCodexNode> GetEnumerator()
|
||||
{
|
||||
return Nodes.Cast<IOnlineCodexNode>().GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return Nodes.GetEnumerator();
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return $"group:[{Containers.Describe()}]";
|
||||
}
|
||||
|
||||
public void EnsureOnline()
|
||||
{
|
||||
foreach (var node in Nodes) node.EnsureOnlineGetVersionResponse();
|
||||
var versionResponses = Nodes.Select(n => n.Version);
|
||||
|
||||
var first = versionResponses.First();
|
||||
if (!versionResponses.All(v => v.version == first.version && v.revision == first.revision))
|
||||
{
|
||||
throw new Exception("Inconsistent version information received from one or more Codex nodes: " +
|
||||
string.Join(",", versionResponses.Select(v => v.ToString())));
|
||||
}
|
||||
|
||||
Version = first;
|
||||
}
|
||||
|
||||
private OnlineCodexNode CreateOnlineCodexNode(RunningContainer c, ICodexNodeFactory factory)
|
||||
{
|
||||
var access = new CodexAccess(lifecycle.Log, c, lifecycle.TimeSet, lifecycle.Configuration.GetAddress(c));
|
||||
return factory.CreateOnlineCodexNode(access, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Marketplace;
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public interface ICodexSetup
|
||||
{
|
||||
ICodexSetup WithName(string name);
|
||||
ICodexSetup At(Location location);
|
||||
ICodexSetup WithBootstrapNode(IOnlineCodexNode node);
|
||||
ICodexSetup WithStorageQuota(ByteSize storageQuota);
|
||||
ICodexSetup WithBlockTTL(TimeSpan duration);
|
||||
ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration);
|
||||
ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks);
|
||||
ICodexSetup EnableMetrics();
|
||||
ICodexSetup EnableMarketplace(TestToken initialBalance);
|
||||
ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther);
|
||||
ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther, bool isValidator);
|
||||
}
|
||||
|
||||
public class CodexSetup : CodexStartupConfig, ICodexSetup
|
||||
{
|
||||
public int NumberOfNodes { get; }
|
||||
|
||||
public CodexSetup(int numberOfNodes, CodexLogLevel logLevel)
|
||||
: base(logLevel)
|
||||
{
|
||||
NumberOfNodes = numberOfNodes;
|
||||
}
|
||||
|
||||
public ICodexSetup WithName(string name)
|
||||
{
|
||||
NameOverride = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup At(Location location)
|
||||
{
|
||||
Location = location;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup WithBootstrapNode(IOnlineCodexNode node)
|
||||
{
|
||||
BootstrapSpr = node.GetDebugInfo().spr;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup WithStorageQuota(ByteSize storageQuota)
|
||||
{
|
||||
StorageQuota = storageQuota;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup WithBlockTTL(TimeSpan duration)
|
||||
{
|
||||
BlockTTL = Convert.ToInt32(duration.TotalSeconds);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration)
|
||||
{
|
||||
BlockMaintenanceInterval = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks)
|
||||
{
|
||||
BlockMaintenanceNumber = numberOfBlocks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup EnableMetrics()
|
||||
{
|
||||
MetricsMode = Metrics.MetricsMode.Record;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup EnableMarketplace(TestToken initialBalance)
|
||||
{
|
||||
return EnableMarketplace(initialBalance, 1000.Eth());
|
||||
}
|
||||
|
||||
public ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther)
|
||||
{
|
||||
return EnableMarketplace(initialBalance, initialEther, false);
|
||||
}
|
||||
|
||||
public ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther, bool isValidator)
|
||||
{
|
||||
MarketplaceConfig = new MarketplaceInitialConfig(initialEther, initialBalance, isValidator);
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
var args = string.Join(',', DescribeArgs());
|
||||
return $"({NumberOfNodes} CodexNodes with args:[{args}])";
|
||||
}
|
||||
|
||||
private IEnumerable<string> DescribeArgs()
|
||||
{
|
||||
yield return $"LogLevel={LogLevel}";
|
||||
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
|
||||
if (StorageQuota != null) yield return $"StorageQuote={StorageQuota}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Marketplace;
|
||||
using DistTestCore.Metrics;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class CodexStarter : BaseStarter
|
||||
{
|
||||
public CodexStarter(TestLifecycle lifecycle)
|
||||
: base(lifecycle)
|
||||
{
|
||||
}
|
||||
|
||||
public List<CodexNodeGroup> RunningGroups { get; } = new List<CodexNodeGroup>();
|
||||
|
||||
public ICodexNodeGroup BringOnline(CodexSetup codexSetup)
|
||||
{
|
||||
LogSeparator();
|
||||
LogStart($"Starting {codexSetup.Describe()}...");
|
||||
var gethStartResult = lifecycle.GethStarter.BringOnlineMarketplaceFor(codexSetup);
|
||||
|
||||
var startupConfig = CreateStartupConfig(gethStartResult, codexSetup);
|
||||
|
||||
var containers = StartCodexContainers(startupConfig, codexSetup.NumberOfNodes, codexSetup.Location);
|
||||
|
||||
var metricAccessFactory = CollectMetrics(codexSetup, containers);
|
||||
|
||||
var codexNodeFactory = new CodexNodeFactory(lifecycle, metricAccessFactory, gethStartResult.MarketplaceAccessFactory);
|
||||
|
||||
var group = CreateCodexGroup(codexSetup, containers, codexNodeFactory);
|
||||
lifecycle.SetCodexVersion(group.Version);
|
||||
|
||||
var nl = Environment.NewLine;
|
||||
var podInfos = string.Join(nl, containers.Containers().Select(c => $"Container: '{c.Name}' runs at '{c.Pod.PodInfo.K8SNodeName}'={c.Pod.PodInfo.Ip}"));
|
||||
LogEnd($"Started {codexSetup.NumberOfNodes} nodes " +
|
||||
$"of image '{containers.Containers().First().Recipe.Image}' " +
|
||||
$"and version '{group.Version}'{nl}" +
|
||||
podInfos);
|
||||
LogSeparator();
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
public void BringOffline(CodexNodeGroup group)
|
||||
{
|
||||
LogStart($"Stopping {group.Describe()}...");
|
||||
var workflow = CreateWorkflow();
|
||||
foreach (var c in group.Containers)
|
||||
{
|
||||
StopCrashWatcher(c);
|
||||
workflow.Stop(c);
|
||||
}
|
||||
RunningGroups.Remove(group);
|
||||
LogEnd("Stopped.");
|
||||
}
|
||||
|
||||
public void DeleteAllResources()
|
||||
{
|
||||
var workflow = CreateWorkflow();
|
||||
workflow.DeleteTestResources();
|
||||
|
||||
RunningGroups.Clear();
|
||||
}
|
||||
|
||||
public void DownloadLog(RunningContainer container, ILogHandler logHandler, int? tailLines)
|
||||
{
|
||||
var workflow = CreateWorkflow();
|
||||
workflow.DownloadContainerLog(container, logHandler, tailLines);
|
||||
}
|
||||
|
||||
private IMetricsAccessFactory CollectMetrics(CodexSetup codexSetup, RunningContainers[] containers)
|
||||
{
|
||||
if (codexSetup.MetricsMode == MetricsMode.None) return new MetricsUnavailableAccessFactory();
|
||||
|
||||
var runningContainers = lifecycle.PrometheusStarter.CollectMetricsFor(containers);
|
||||
|
||||
if (codexSetup.MetricsMode == MetricsMode.Dashboard)
|
||||
{
|
||||
lifecycle.GrafanaStarter.StartDashboard(runningContainers.Containers.First(), codexSetup);
|
||||
}
|
||||
|
||||
return new CodexNodeMetricsAccessFactory(lifecycle, runningContainers);
|
||||
}
|
||||
|
||||
private StartupConfig CreateStartupConfig(GethStartResult gethStartResult, CodexSetup codexSetup)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.NameOverride = codexSetup.NameOverride;
|
||||
startupConfig.Add(codexSetup);
|
||||
startupConfig.Add(gethStartResult);
|
||||
return startupConfig;
|
||||
}
|
||||
|
||||
private RunningContainers[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, Location location)
|
||||
{
|
||||
var result = new List<RunningContainers>();
|
||||
var recipe = new CodexContainerRecipe();
|
||||
for (var i = 0; i < numberOfNodes; i++)
|
||||
{
|
||||
var workflow = CreateWorkflow();
|
||||
var rc = workflow.Start(1, location, recipe, startupConfig);
|
||||
CreateCrashWatcher(workflow, rc);
|
||||
result.Add(rc);
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private CodexNodeGroup CreateCodexGroup(CodexSetup codexSetup, RunningContainers[] runningContainers, CodexNodeFactory codexNodeFactory)
|
||||
{
|
||||
var group = new CodexNodeGroup(lifecycle, codexSetup, runningContainers, codexNodeFactory);
|
||||
RunningGroups.Add(group);
|
||||
|
||||
try
|
||||
{
|
||||
Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
CodexNodesNotOnline(runningContainers);
|
||||
throw;
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private void CodexNodesNotOnline(RunningContainers[] runningContainers)
|
||||
{
|
||||
Log("Codex nodes failed to start");
|
||||
foreach (var container in runningContainers.Containers()) lifecycle.DownloadLog(container);
|
||||
}
|
||||
|
||||
private StartupWorkflow CreateWorkflow()
|
||||
{
|
||||
return lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
}
|
||||
|
||||
private void LogSeparator()
|
||||
{
|
||||
Log("----------------------------------------------------------------------------");
|
||||
}
|
||||
|
||||
private void CreateCrashWatcher(StartupWorkflow workflow, RunningContainers rc)
|
||||
{
|
||||
var c = rc.Containers.Single();
|
||||
c.CrashWatcher = workflow.CreateCrashWatcher(c);
|
||||
}
|
||||
|
||||
private void StopCrashWatcher(RunningContainers containers)
|
||||
{
|
||||
foreach (var c in containers.Containers)
|
||||
{
|
||||
c.CrashWatcher?.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
using DistTestCore.Codex;
|
||||
using KubernetesWorkflow;
|
||||
using System.Net.NetworkInformation;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
private readonly string? kubeConfigFile;
|
||||
private readonly string logPath;
|
||||
private readonly bool logDebug;
|
||||
private readonly string dataFilesPath;
|
||||
private readonly CodexLogLevel codexLogLevel;
|
||||
private readonly string k8sNamespacePrefix;
|
||||
private static RunnerLocation? runnerLocation = null;
|
||||
|
||||
public Configuration()
|
||||
{
|
||||
kubeConfigFile = GetNullableEnvVarOrDefault("KUBECONFIG", null);
|
||||
logPath = GetEnvVarOrDefault("LOGPATH", "CodexTestLogs");
|
||||
logDebug = GetEnvVarOrDefault("LOGDEBUG", "false").ToLowerInvariant() == "true";
|
||||
dataFilesPath = GetEnvVarOrDefault("DATAFILEPATH", "TestDataFiles");
|
||||
codexLogLevel = ParseEnum.Parse<CodexLogLevel>(GetEnvVarOrDefault("LOGLEVEL", nameof(CodexLogLevel.Trace)));
|
||||
k8sNamespacePrefix = "ct-";
|
||||
}
|
||||
|
||||
public Configuration(string? kubeConfigFile, string logPath, bool logDebug, string dataFilesPath, CodexLogLevel codexLogLevel, string k8sNamespacePrefix)
|
||||
{
|
||||
this.kubeConfigFile = kubeConfigFile;
|
||||
this.logPath = logPath;
|
||||
this.logDebug = logDebug;
|
||||
this.dataFilesPath = dataFilesPath;
|
||||
this.codexLogLevel = codexLogLevel;
|
||||
this.k8sNamespacePrefix = k8sNamespacePrefix;
|
||||
}
|
||||
|
||||
public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet)
|
||||
{
|
||||
return new KubernetesWorkflow.Configuration(
|
||||
k8sNamespacePrefix: k8sNamespacePrefix,
|
||||
kubeConfigFile: kubeConfigFile,
|
||||
operationTimeout: timeSet.K8sOperationTimeout(),
|
||||
retryDelay: timeSet.WaitForK8sServiceDelay()
|
||||
);
|
||||
}
|
||||
|
||||
public Logging.LogConfig GetLogConfig()
|
||||
{
|
||||
return new Logging.LogConfig(logPath, debugEnabled: logDebug);
|
||||
}
|
||||
|
||||
public string GetFileManagerFolder()
|
||||
{
|
||||
return dataFilesPath;
|
||||
}
|
||||
|
||||
public CodexLogLevel GetCodexLogLevel()
|
||||
{
|
||||
return codexLogLevel;
|
||||
}
|
||||
|
||||
public Address GetAddress(RunningContainer container)
|
||||
{
|
||||
if (runnerLocation == null)
|
||||
{
|
||||
runnerLocation = RunnerLocationUtils.DetermineRunnerLocation(container);
|
||||
}
|
||||
|
||||
if (runnerLocation == RunnerLocation.InternalToCluster)
|
||||
{
|
||||
return container.ClusterInternalAddress;
|
||||
}
|
||||
return container.ClusterExternalAddress;
|
||||
}
|
||||
|
||||
private static string GetEnvVarOrDefault(string varName, string defaultValue)
|
||||
{
|
||||
var v = Environment.GetEnvironmentVariable(varName);
|
||||
if (v == null) return defaultValue;
|
||||
return v;
|
||||
}
|
||||
|
||||
private static string? GetNullableEnvVarOrDefault(string varName, string? defaultValue)
|
||||
{
|
||||
var v = Environment.GetEnvironmentVariable(varName);
|
||||
if (v == null) return defaultValue;
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
public enum RunnerLocation
|
||||
{
|
||||
ExternalToCluster,
|
||||
InternalToCluster,
|
||||
}
|
||||
|
||||
public static class RunnerLocationUtils
|
||||
{
|
||||
private static bool alreadyDidThat = false;
|
||||
|
||||
public static RunnerLocation DetermineRunnerLocation(RunningContainer container)
|
||||
{
|
||||
// We want to be sure we don't ping more often than strictly necessary.
|
||||
// If we have already determined the location during this application
|
||||
// lifetime, don't do it again.
|
||||
if (alreadyDidThat) throw new Exception("We already did that.");
|
||||
alreadyDidThat = true;
|
||||
|
||||
if (PingHost(container.Pod.PodInfo.Ip))
|
||||
{
|
||||
return RunnerLocation.InternalToCluster;
|
||||
}
|
||||
if (PingHost(Format(container.ClusterExternalAddress)))
|
||||
{
|
||||
return RunnerLocation.ExternalToCluster;
|
||||
}
|
||||
|
||||
throw new Exception("Unable to determine runner location.");
|
||||
}
|
||||
|
||||
private static string Format(Address host)
|
||||
{
|
||||
return host.Host
|
||||
.Replace("http://", "")
|
||||
.Replace("https://", "");
|
||||
}
|
||||
|
||||
private static bool PingHost(string host)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var pinger = new Ping();
|
||||
PingReply reply = pinger.Send(host);
|
||||
return reply.Status == IPStatus.Success;
|
||||
}
|
||||
catch (PingException)
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public abstract class DefaultContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public static string TestsType { get; set; } = "NotSet";
|
||||
public static ApplicationIds? ApplicationIds { get; set; } = null;
|
||||
|
||||
protected abstract void InitializeRecipe(StartupConfig config);
|
||||
|
||||
protected override void Initialize(StartupConfig config)
|
||||
{
|
||||
Add("tests-type", TestsType);
|
||||
Add("runid", NameUtils.GetRunId());
|
||||
Add("testid", NameUtils.GetTestId());
|
||||
Add("category", NameUtils.GetCategoryName());
|
||||
Add("fixturename", NameUtils.GetRawFixtureName());
|
||||
Add("testname", NameUtils.GetTestMethodName());
|
||||
|
||||
if (ApplicationIds != null)
|
||||
{
|
||||
Add("codexid", ApplicationIds.CodexId);
|
||||
Add("gethid", ApplicationIds.GethId);
|
||||
Add("prometheusid", ApplicationIds.PrometheusId);
|
||||
Add("codexcontractsid", ApplicationIds.CodexContractsId);
|
||||
Add("grafanaid", ApplicationIds.GrafanaId);
|
||||
}
|
||||
Add("app", AppName);
|
||||
|
||||
InitializeRecipe(config);
|
||||
}
|
||||
|
||||
private void Add(string name, string value)
|
||||
{
|
||||
AddPodLabel(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
using DistTestCore.Codex;
|
||||
using DistTestCore.Helpers;
|
||||
using DistTestCore.Logs;
|
||||
using DistTestCore.Marketplace;
|
||||
using DistTestCore.Metrics;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
using NUnit.Framework;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public abstract class DistTest
|
||||
{
|
||||
private const string TestsType = "dist-tests";
|
||||
private readonly Configuration configuration = new Configuration();
|
||||
private readonly Assembly[] testAssemblies;
|
||||
private readonly FixtureLog fixtureLog;
|
||||
private readonly StatusLog statusLog;
|
||||
private readonly object lifecycleLock = new object();
|
||||
private readonly Dictionary<string, TestLifecycle> lifecycles = new Dictionary<string, TestLifecycle>();
|
||||
|
||||
public DistTest()
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
|
||||
|
||||
var logConfig = configuration.GetLogConfig();
|
||||
var startTime = DateTime.UtcNow;
|
||||
fixtureLog = new FixtureLog(logConfig, startTime);
|
||||
statusLog = new StatusLog(logConfig, startTime);
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void GlobalSetup()
|
||||
{
|
||||
fixtureLog.Log($"Codex Distributed Tests are starting...");
|
||||
fixtureLog.Log($"Codex image: '{new CodexContainerRecipe().Image}'");
|
||||
fixtureLog.Log($"CodexContracts image: '{new CodexContractsContainerRecipe().Image}'");
|
||||
fixtureLog.Log($"Prometheus image: '{new PrometheusContainerRecipe().Image}'");
|
||||
fixtureLog.Log($"Geth image: '{new GethContainerRecipe().Image}'");
|
||||
|
||||
// Previous test run may have been interrupted.
|
||||
// Begin by cleaning everything up.
|
||||
try
|
||||
{
|
||||
Stopwatch.Measure(fixtureLog, "Global setup", () =>
|
||||
{
|
||||
var wc = new WorkflowCreator(fixtureLog, configuration.GetK8sConfiguration(GetTimeSet()), string.Empty);
|
||||
wc.CreateWorkflow().DeleteAllResources();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalTestFailure.HasFailed = true;
|
||||
fixtureLog.Error($"Global setup cleanup failed with: {ex}");
|
||||
throw;
|
||||
}
|
||||
|
||||
fixtureLog.Log("Global setup cleanup successful");
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUpDistTest()
|
||||
{
|
||||
if (GlobalTestFailure.HasFailed)
|
||||
{
|
||||
Assert.Inconclusive("Skip test: Previous test failed during clean up.");
|
||||
}
|
||||
else
|
||||
{
|
||||
CreateNewTestLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDownDistTest()
|
||||
{
|
||||
try
|
||||
{
|
||||
DisposeTestLifecycle();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
fixtureLog.Error("Cleanup failed: " + ex.Message);
|
||||
GlobalTestFailure.HasFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public TestFile GenerateTestFile(ByteSize size, string label = "")
|
||||
{
|
||||
return Get().FileManager.GenerateTestFile(size, label);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Any test files generated in 'action' will be deleted after it returns.
|
||||
/// This helps prevent large tests from filling up discs.
|
||||
/// </summary>
|
||||
public void ScopedTestFiles(Action action)
|
||||
{
|
||||
Get().FileManager.PushFileSet();
|
||||
action();
|
||||
Get().FileManager.PopFileSet();
|
||||
}
|
||||
|
||||
public IOnlineCodexNode SetupCodexBootstrapNode()
|
||||
{
|
||||
return SetupCodexBootstrapNode(s => { });
|
||||
}
|
||||
|
||||
public virtual IOnlineCodexNode SetupCodexBootstrapNode(Action<ICodexSetup> setup)
|
||||
{
|
||||
return SetupCodexNode(s =>
|
||||
{
|
||||
setup(s);
|
||||
s.WithName("Bootstrap");
|
||||
});
|
||||
}
|
||||
|
||||
public IOnlineCodexNode SetupCodexNode()
|
||||
{
|
||||
return SetupCodexNode(s => { });
|
||||
}
|
||||
|
||||
public IOnlineCodexNode SetupCodexNode(Action<ICodexSetup> setup)
|
||||
{
|
||||
return SetupCodexNodes(1, setup)[0];
|
||||
}
|
||||
|
||||
public ICodexNodeGroup SetupCodexNodes(int numberOfNodes)
|
||||
{
|
||||
return SetupCodexNodes(numberOfNodes, s => { });
|
||||
}
|
||||
|
||||
public virtual ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
|
||||
{
|
||||
var codexSetup = CreateCodexSetup(numberOfNodes);
|
||||
|
||||
setup(codexSetup);
|
||||
|
||||
return BringOnline(codexSetup);
|
||||
}
|
||||
|
||||
public ICodexNodeGroup BringOnline(ICodexSetup codexSetup)
|
||||
{
|
||||
return Get().CodexStarter.BringOnline((CodexSetup)codexSetup);
|
||||
}
|
||||
|
||||
public IEnumerable<IOnlineCodexNode> GetAllOnlineCodexNodes()
|
||||
{
|
||||
return Get().CodexStarter.RunningGroups.SelectMany(g => g.Nodes);
|
||||
}
|
||||
|
||||
public BaseLog GetTestLog()
|
||||
{
|
||||
return Get().Log;
|
||||
}
|
||||
|
||||
public void Log(string msg)
|
||||
{
|
||||
TestContext.Progress.WriteLine(msg);
|
||||
GetTestLog().Log(msg);
|
||||
}
|
||||
|
||||
public void Debug(string msg)
|
||||
{
|
||||
TestContext.Progress.WriteLine(msg);
|
||||
GetTestLog().Debug(msg);
|
||||
}
|
||||
|
||||
public PeerConnectionTestHelpers CreatePeerConnectionTestHelpers()
|
||||
{
|
||||
return new PeerConnectionTestHelpers(GetTestLog());
|
||||
}
|
||||
|
||||
public PeerDownloadTestHelpers CreatePeerDownloadTestHelpers()
|
||||
{
|
||||
return new PeerDownloadTestHelpers(GetTestLog(), Get().FileManager);
|
||||
}
|
||||
|
||||
public PeerFetchTestHelpers CreatePeerFetchTestHelpers()
|
||||
{
|
||||
return new PeerFetchTestHelpers(GetTestLog(), Get().FileManager);
|
||||
}
|
||||
|
||||
public void Measure(string name, Action action)
|
||||
{
|
||||
Stopwatch.Measure(Get().Log, name, action);
|
||||
}
|
||||
|
||||
protected CodexSetup CreateCodexSetup(int numberOfNodes)
|
||||
{
|
||||
return new CodexSetup(numberOfNodes, configuration.GetCodexLogLevel());
|
||||
}
|
||||
|
||||
private TestLifecycle Get()
|
||||
{
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
return lifecycles[GetCurrentTestName()];
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateNewTestLifecycle()
|
||||
{
|
||||
var testName = GetCurrentTestName();
|
||||
fixtureLog.WriteLogTag();
|
||||
Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () =>
|
||||
{
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
var testNamespace = Guid.NewGuid().ToString();
|
||||
var lifecycle = new TestLifecycle(fixtureLog.CreateTestLog(), configuration, GetTimeSet(), testNamespace);
|
||||
lifecycles.Add(testName, lifecycle);
|
||||
DefaultContainerRecipe.TestsType = TestsType;
|
||||
DefaultContainerRecipe.ApplicationIds = lifecycle.GetApplicationIds();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void DisposeTestLifecycle()
|
||||
{
|
||||
var lifecycle = Get();
|
||||
var testResult = GetTestResult();
|
||||
var testDuration = lifecycle.GetTestDuration();
|
||||
fixtureLog.Log($"{GetCurrentTestName()} = {testResult} ({testDuration})");
|
||||
statusLog.ConcludeTest(testResult, testDuration, lifecycle.GetApplicationIds());
|
||||
Stopwatch.Measure(fixtureLog, $"Teardown for {GetCurrentTestName()}", () =>
|
||||
{
|
||||
lifecycle.Log.EndTest();
|
||||
IncludeLogsAndMetricsOnTestFailure(lifecycle);
|
||||
lifecycle.DeleteAllResources();
|
||||
lifecycle = null!;
|
||||
});
|
||||
}
|
||||
|
||||
private ITimeSet GetTimeSet()
|
||||
{
|
||||
if (ShouldUseLongTimeouts()) return new LongTimeSet();
|
||||
return new DefaultTimeSet();
|
||||
}
|
||||
|
||||
private bool ShouldUseLongTimeouts()
|
||||
{
|
||||
// Don't be fooled! TestContext.CurrentTest.Test allows you easy access to the attributes of the current test.
|
||||
// But this doesn't work for tests making use of [TestCase]. So instead, we use reflection here to figure out
|
||||
// if the attribute is present.
|
||||
var currentTest = TestContext.CurrentContext.Test;
|
||||
var className = currentTest.ClassName;
|
||||
var methodName = currentTest.MethodName;
|
||||
|
||||
var testClasses = testAssemblies.SelectMany(a => a.GetTypes()).Where(c => c.FullName == className).ToArray();
|
||||
var testMethods = testClasses.SelectMany(c => c.GetMethods()).Where(m => m.Name == methodName).ToArray();
|
||||
|
||||
return testMethods.Any(m => m.GetCustomAttribute<UseLongTimeoutsAttribute>() != null);
|
||||
}
|
||||
|
||||
private void IncludeLogsAndMetricsOnTestFailure(TestLifecycle lifecycle)
|
||||
{
|
||||
var result = TestContext.CurrentContext.Result;
|
||||
if (result.Outcome.Status == NUnit.Framework.Interfaces.TestStatus.Failed)
|
||||
{
|
||||
fixtureLog.MarkAsFailed();
|
||||
|
||||
if (IsDownloadingLogsAndMetricsEnabled())
|
||||
{
|
||||
lifecycle.Log.Log("Downloading all CodexNode logs and metrics because of test failure...");
|
||||
DownloadAllLogs(lifecycle);
|
||||
DownloadAllMetrics(lifecycle);
|
||||
}
|
||||
else
|
||||
{
|
||||
lifecycle.Log.Log("Skipping download of all CodexNode logs and metrics due to [DontDownloadLogsAndMetricsOnFailure] attribute.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DownloadAllLogs(TestLifecycle lifecycle)
|
||||
{
|
||||
OnEachCodexNode(lifecycle, node =>
|
||||
{
|
||||
lifecycle.DownloadLog(node.CodexAccess.Container);
|
||||
});
|
||||
}
|
||||
|
||||
private void DownloadAllMetrics(TestLifecycle lifecycle)
|
||||
{
|
||||
var metricsDownloader = new MetricsDownloader(lifecycle.Log);
|
||||
|
||||
OnEachCodexNode(lifecycle, node =>
|
||||
{
|
||||
var m = node.Metrics as MetricsAccess;
|
||||
if (m != null)
|
||||
{
|
||||
metricsDownloader.DownloadAllMetricsForNode(node.GetName(), m);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnEachCodexNode(TestLifecycle lifecycle, Action<OnlineCodexNode> action)
|
||||
{
|
||||
var allNodes = lifecycle.CodexStarter.RunningGroups.SelectMany(g => g.Nodes);
|
||||
foreach (var node in allNodes)
|
||||
{
|
||||
action(node);
|
||||
}
|
||||
}
|
||||
|
||||
private string GetCurrentTestName()
|
||||
{
|
||||
return $"[{TestContext.CurrentContext.Test.Name}]";
|
||||
}
|
||||
|
||||
private string GetTestResult()
|
||||
{
|
||||
return TestContext.CurrentContext.Result.Outcome.Status.ToString();
|
||||
}
|
||||
|
||||
private bool IsDownloadingLogsAndMetricsEnabled()
|
||||
{
|
||||
var testProperties = TestContext.CurrentContext.Test.Properties;
|
||||
return !testProperties.ContainsKey(DontDownloadLogsAndMetricsOnFailureAttribute.DontDownloadKey);
|
||||
}
|
||||
}
|
||||
|
||||
public static class GlobalTestFailure
|
||||
{
|
||||
public static bool HasFailed { get; set; } = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<RootNamespace>DistTestCore</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsArm64 Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">true</IsArm64>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(IsArm64)'=='true'">
|
||||
<DefineConstants>Arm64</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Metrics\dashboard.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Metrics\dashboard.json">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\KubernetesWorkflow\KubernetesWorkflow.csproj" />
|
||||
<ProjectReference Include="..\Logging\Logging.csproj" />
|
||||
<ProjectReference Include="..\Nethereum\NethereumWorkflow.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,218 @@
|
||||
using Logging;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public interface IFileManager
|
||||
{
|
||||
TestFile CreateEmptyTestFile(string label = "");
|
||||
TestFile GenerateTestFile(ByteSize size, string label = "");
|
||||
void DeleteAllTestFiles();
|
||||
void PushFileSet();
|
||||
void PopFileSet();
|
||||
}
|
||||
|
||||
public class FileManager : IFileManager
|
||||
{
|
||||
public const int ChunkSize = 1024 * 1024 * 100;
|
||||
private static NumberSource folderNumberSource = new NumberSource(0);
|
||||
private readonly Random random = new Random();
|
||||
private readonly BaseLog log;
|
||||
private readonly string folder;
|
||||
private readonly List<List<TestFile>> fileSetStack = new List<List<TestFile>>();
|
||||
|
||||
public FileManager(BaseLog log, Configuration configuration)
|
||||
{
|
||||
folder = Path.Combine(configuration.GetFileManagerFolder(), folderNumberSource.GetNextNumber().ToString("D5"));
|
||||
|
||||
EnsureDirectory();
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public TestFile CreateEmptyTestFile(string label = "")
|
||||
{
|
||||
var path = Path.Combine(folder, Guid.NewGuid().ToString() + "_test.bin");
|
||||
var result = new TestFile(log, path, label);
|
||||
File.Create(result.Filename).Close();
|
||||
if (fileSetStack.Any()) fileSetStack.Last().Add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public TestFile GenerateTestFile(ByteSize size, string label)
|
||||
{
|
||||
var sw = Stopwatch.Begin(log);
|
||||
var result = GenerateFile(size, label);
|
||||
sw.End($"Generated file '{result.Describe()}'.");
|
||||
return result;
|
||||
}
|
||||
|
||||
public void DeleteAllTestFiles()
|
||||
{
|
||||
DeleteDirectory();
|
||||
}
|
||||
|
||||
public void PushFileSet()
|
||||
{
|
||||
fileSetStack.Add(new List<TestFile>());
|
||||
}
|
||||
|
||||
public void PopFileSet()
|
||||
{
|
||||
if (!fileSetStack.Any()) return;
|
||||
var pop = fileSetStack.Last();
|
||||
fileSetStack.Remove(pop);
|
||||
|
||||
foreach (var file in pop)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(file.Filename);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private TestFile GenerateFile(ByteSize size, string label)
|
||||
{
|
||||
var result = CreateEmptyTestFile(label);
|
||||
CheckSpaceAvailable(result, size);
|
||||
|
||||
GenerateFileBytes(result, size);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CheckSpaceAvailable(TestFile testFile, ByteSize size)
|
||||
{
|
||||
var file = new FileInfo(testFile.Filename);
|
||||
var drive = new DriveInfo(file.Directory!.Root.FullName);
|
||||
|
||||
var spaceAvailable = drive.TotalFreeSpace;
|
||||
|
||||
if (spaceAvailable < size.SizeInBytes)
|
||||
{
|
||||
var msg = $"Inconclusive: Not enough disk space to perform test. " +
|
||||
$"{Formatter.FormatByteSize(size.SizeInBytes)} required. " +
|
||||
$"{Formatter.FormatByteSize(spaceAvailable)} available.";
|
||||
|
||||
log.Log(msg);
|
||||
Assert.Inconclusive(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateFileBytes(TestFile result, ByteSize size)
|
||||
{
|
||||
long bytesLeft = size.SizeInBytes;
|
||||
int chunkSize = ChunkSize;
|
||||
while (bytesLeft > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var length = Math.Min(bytesLeft, chunkSize);
|
||||
AppendRandomBytesToFile(result, length);
|
||||
bytesLeft -= length;
|
||||
}
|
||||
catch
|
||||
{
|
||||
chunkSize = chunkSize / 2;
|
||||
if (chunkSize < 1024) throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendRandomBytesToFile(TestFile result, long length)
|
||||
{
|
||||
var bytes = new byte[length];
|
||||
random.NextBytes(bytes);
|
||||
using var stream = new FileStream(result.Filename, FileMode.Append);
|
||||
stream.Write(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
private void EnsureDirectory()
|
||||
{
|
||||
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
|
||||
}
|
||||
|
||||
private void DeleteDirectory()
|
||||
{
|
||||
Directory.Delete(folder, true);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestFile
|
||||
{
|
||||
private readonly BaseLog log;
|
||||
|
||||
public TestFile(BaseLog log, string filename, string label)
|
||||
{
|
||||
this.log = log;
|
||||
Filename = filename;
|
||||
Label = label;
|
||||
}
|
||||
|
||||
public string Filename { get; }
|
||||
public string Label { get; }
|
||||
|
||||
public void AssertIsEqual(TestFile? actual)
|
||||
{
|
||||
var sw = Stopwatch.Begin(log);
|
||||
try
|
||||
{
|
||||
AssertEqual(actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.End($"{nameof(TestFile)}.{nameof(AssertIsEqual)}");
|
||||
}
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
var sizePostfix = $" ({Formatter.FormatByteSize(GetFileSize())})";
|
||||
if (!string.IsNullOrEmpty(Label)) return Label + sizePostfix;
|
||||
return $"'{Filename}'{sizePostfix}";
|
||||
}
|
||||
|
||||
private void AssertEqual(TestFile? actual)
|
||||
{
|
||||
if (actual == null) Assert.Fail("TestFile is null.");
|
||||
if (actual == this || actual!.Filename == Filename) Assert.Fail("TestFile is compared to itself.");
|
||||
|
||||
Assert.That(actual.GetFileSize(), Is.EqualTo(GetFileSize()), "Files are not of equal length.");
|
||||
|
||||
using var streamExpected = new FileStream(Filename, FileMode.Open, FileAccess.Read);
|
||||
using var streamActual = new FileStream(actual.Filename, FileMode.Open, FileAccess.Read);
|
||||
|
||||
var bytesExpected = new byte[FileManager.ChunkSize];
|
||||
var bytesActual = new byte[FileManager.ChunkSize];
|
||||
|
||||
var readExpected = 0;
|
||||
var readActual = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
readExpected = streamExpected.Read(bytesExpected, 0, FileManager.ChunkSize);
|
||||
readActual = streamActual.Read(bytesActual, 0, FileManager.ChunkSize);
|
||||
|
||||
if (readExpected == 0 && readActual == 0)
|
||||
{
|
||||
log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.That(readActual, Is.EqualTo(readExpected), "Unable to read buffers of equal length.");
|
||||
|
||||
for (var i = 0; i < readActual; i++)
|
||||
{
|
||||
if (bytesExpected[i] != bytesActual[i]) Assert.Fail("File contents not equal.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private long GetFileSize()
|
||||
{
|
||||
var info = new FileInfo(Filename);
|
||||
return info.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using DistTestCore.Marketplace;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class GethStarter : BaseStarter
|
||||
{
|
||||
private readonly MarketplaceNetworkCache marketplaceNetworkCache;
|
||||
private readonly GethCompanionNodeStarter companionNodeStarter;
|
||||
|
||||
public GethStarter(TestLifecycle lifecycle)
|
||||
: base(lifecycle)
|
||||
{
|
||||
marketplaceNetworkCache = new MarketplaceNetworkCache(
|
||||
new GethBootstrapNodeStarter(lifecycle),
|
||||
new CodexContractsStarter(lifecycle));
|
||||
companionNodeStarter = new GethCompanionNodeStarter(lifecycle);
|
||||
}
|
||||
|
||||
public GethStartResult BringOnlineMarketplaceFor(CodexSetup codexSetup)
|
||||
{
|
||||
if (codexSetup.MarketplaceConfig == null) return CreateMarketplaceUnavailableResult();
|
||||
|
||||
var marketplaceNetwork = marketplaceNetworkCache.Get();
|
||||
var companionNode = StartCompanionNode(codexSetup, marketplaceNetwork);
|
||||
|
||||
LogStart("Setting up initial balance...");
|
||||
TransferInitialBalance(marketplaceNetwork, codexSetup.MarketplaceConfig, companionNode);
|
||||
LogEnd($"Initial balance of {codexSetup.MarketplaceConfig.InitialTestTokens} set for {codexSetup.NumberOfNodes} nodes.");
|
||||
|
||||
return CreateGethStartResult(marketplaceNetwork, companionNode);
|
||||
}
|
||||
|
||||
private void TransferInitialBalance(MarketplaceNetwork marketplaceNetwork, MarketplaceInitialConfig marketplaceConfig, GethCompanionNodeInfo companionNode)
|
||||
{
|
||||
if (marketplaceConfig.InitialTestTokens.Amount == 0) return;
|
||||
|
||||
var interaction = marketplaceNetwork.StartInteraction(lifecycle);
|
||||
var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress;
|
||||
|
||||
var accounts = companionNode.Accounts.Select(a => a.Account).ToArray();
|
||||
interaction.MintTestTokens(accounts, marketplaceConfig.InitialTestTokens.Amount, tokenAddress);
|
||||
}
|
||||
|
||||
private GethStartResult CreateGethStartResult(MarketplaceNetwork marketplaceNetwork, GethCompanionNodeInfo companionNode)
|
||||
{
|
||||
return new GethStartResult(CreateMarketplaceAccessFactory(marketplaceNetwork), marketplaceNetwork, companionNode);
|
||||
}
|
||||
|
||||
private GethStartResult CreateMarketplaceUnavailableResult()
|
||||
{
|
||||
return new GethStartResult(new MarketplaceUnavailableAccessFactory(), null!, null!);
|
||||
}
|
||||
|
||||
private IMarketplaceAccessFactory CreateMarketplaceAccessFactory(MarketplaceNetwork marketplaceNetwork)
|
||||
{
|
||||
return new GethMarketplaceAccessFactory(lifecycle, marketplaceNetwork);
|
||||
}
|
||||
|
||||
private GethCompanionNodeInfo StartCompanionNode(CodexSetup codexSetup, MarketplaceNetwork marketplaceNetwork)
|
||||
{
|
||||
return companionNodeStarter.StartCompanionNodeFor(codexSetup, marketplaceNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
public class MarketplaceNetworkCache
|
||||
{
|
||||
private readonly GethBootstrapNodeStarter bootstrapNodeStarter;
|
||||
private readonly CodexContractsStarter codexContractsStarter;
|
||||
private MarketplaceNetwork? network;
|
||||
|
||||
public MarketplaceNetworkCache(GethBootstrapNodeStarter bootstrapNodeStarter, CodexContractsStarter codexContractsStarter)
|
||||
{
|
||||
this.bootstrapNodeStarter = bootstrapNodeStarter;
|
||||
this.codexContractsStarter = codexContractsStarter;
|
||||
}
|
||||
|
||||
public MarketplaceNetwork Get()
|
||||
{
|
||||
if (network == null)
|
||||
{
|
||||
var bootstrapInfo = bootstrapNodeStarter.StartGethBootstrapNode();
|
||||
var marketplaceInfo = codexContractsStarter.Start(bootstrapInfo);
|
||||
network = new MarketplaceNetwork(bootstrapInfo, marketplaceInfo );
|
||||
}
|
||||
return network;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using DistTestCore.Metrics;
|
||||
using IdentityModel.Client;
|
||||
using KubernetesWorkflow;
|
||||
using Newtonsoft.Json;
|
||||
using System.Reflection;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class GrafanaStarter : BaseStarter
|
||||
{
|
||||
private const string StorageQuotaThresholdReplaceToken = "\"<CODEX_STORAGEQUOTA>\"";
|
||||
private const string BytesUsedGraphAxisSoftMaxReplaceToken = "\"<CODEX_BYTESUSED_SOFTMAX>\"";
|
||||
|
||||
public GrafanaStarter(TestLifecycle lifecycle)
|
||||
: base(lifecycle)
|
||||
{
|
||||
}
|
||||
|
||||
public GrafanaStartInfo StartDashboard(RunningContainer prometheusContainer, CodexSetup codexSetup)
|
||||
{
|
||||
LogStart($"Starting dashboard server");
|
||||
|
||||
var grafanaContainer = StartGrafanaContainer();
|
||||
var grafanaAddress = lifecycle.Configuration.GetAddress(grafanaContainer);
|
||||
|
||||
var http = new Http(lifecycle.Log, new DefaultTimeSet(), grafanaAddress, "api/", AddBasicAuth);
|
||||
|
||||
Log("Connecting datasource...");
|
||||
AddDataSource(http, prometheusContainer);
|
||||
|
||||
Log("Uploading dashboard configurations...");
|
||||
var jsons = ReadEachDashboardJsonFile(codexSetup);
|
||||
var dashboardUrls = jsons.Select(j => UploadDashboard(http, grafanaContainer, j)).ToArray();
|
||||
|
||||
LogEnd("Dashboard server started.");
|
||||
|
||||
return new GrafanaStartInfo(dashboardUrls, grafanaContainer);
|
||||
}
|
||||
|
||||
private RunningContainer StartGrafanaContainer()
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
|
||||
var workflow = lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
var grafanaContainers = workflow.Start(1, Location.Unspecified, new GrafanaContainerRecipe(), startupConfig);
|
||||
if (grafanaContainers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 dashboard container to be created.");
|
||||
|
||||
return grafanaContainers.Containers.First();
|
||||
}
|
||||
|
||||
private void AddBasicAuth(HttpClient client)
|
||||
{
|
||||
client.SetBasicAuthentication(
|
||||
GrafanaContainerRecipe.DefaultAdminUser,
|
||||
GrafanaContainerRecipe.DefaultAdminPassword);
|
||||
}
|
||||
|
||||
private static void AddDataSource(Http http, RunningContainer prometheusContainer)
|
||||
{
|
||||
var prometheusAddress = prometheusContainer.ClusterExternalAddress;
|
||||
var prometheusUrl = prometheusAddress.Host + ":" + prometheusAddress.Port;
|
||||
var response = http.HttpPostJson<GrafanaDataSourceRequest, GrafanaDataSourceResponse>("datasources", new GrafanaDataSourceRequest
|
||||
{
|
||||
uid = "c89eaad3-9184-429f-ac94-8ba0b1824dbb",
|
||||
name = "CodexPrometheus",
|
||||
type = "prometheus",
|
||||
url = prometheusUrl,
|
||||
access = "proxy",
|
||||
basicAuth = false,
|
||||
jsonData = new GrafanaDataSourceJsonData
|
||||
{
|
||||
httpMethod = "POST"
|
||||
}
|
||||
});
|
||||
|
||||
if (response.message != "Datasource added")
|
||||
{
|
||||
throw new Exception("Test infra failure: Failed to add datasource to dashboard: " + response.message);
|
||||
}
|
||||
}
|
||||
|
||||
public static string UploadDashboard(Http http, RunningContainer grafanaContainer, string dashboardJson)
|
||||
{
|
||||
var request = GetDashboardCreateRequest(dashboardJson);
|
||||
var response = http.HttpPostString("dashboards/db", request);
|
||||
var jsonResponse = JsonConvert.DeserializeObject<GrafanaPostDashboardResponse>(response);
|
||||
if (jsonResponse == null || string.IsNullOrEmpty(jsonResponse.url)) throw new Exception("Failed to upload dashboard.");
|
||||
|
||||
var grafanaAddress = grafanaContainer.ClusterExternalAddress;
|
||||
return grafanaAddress.Host + ":" + grafanaAddress.Port + jsonResponse.url;
|
||||
}
|
||||
|
||||
private static string[] ReadEachDashboardJsonFile(CodexSetup codexSetup)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var resourceNames = new[]
|
||||
{
|
||||
"DistTestCore.Metrics.dashboard.json"
|
||||
};
|
||||
|
||||
return resourceNames.Select(r => GetManifestResource(assembly, r, codexSetup)).ToArray();
|
||||
}
|
||||
|
||||
private static string GetManifestResource(Assembly assembly, string resourceName, CodexSetup codexSetup)
|
||||
{
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null) throw new Exception("Unable to find resource " + resourceName);
|
||||
using var reader = new StreamReader(stream);
|
||||
return ApplyReplacements(reader.ReadToEnd(), codexSetup);
|
||||
}
|
||||
|
||||
private static string ApplyReplacements(string input, CodexSetup codexSetup)
|
||||
{
|
||||
var quotaString = GetQuotaString(codexSetup);
|
||||
var softMaxString = GetSoftMaxString(codexSetup);
|
||||
|
||||
return input
|
||||
.Replace(StorageQuotaThresholdReplaceToken, quotaString)
|
||||
.Replace(BytesUsedGraphAxisSoftMaxReplaceToken, softMaxString);
|
||||
}
|
||||
|
||||
private static string GetQuotaString(CodexSetup codexSetup)
|
||||
{
|
||||
return GetCodexStorageQuotaInBytes(codexSetup).ToString();
|
||||
}
|
||||
|
||||
private static string GetSoftMaxString(CodexSetup codexSetup)
|
||||
{
|
||||
var quota = GetCodexStorageQuotaInBytes(codexSetup);
|
||||
var softMax = Convert.ToInt64(quota * 1.1); // + 10%, for nice viewing.
|
||||
return softMax.ToString();
|
||||
}
|
||||
|
||||
private static long GetCodexStorageQuotaInBytes(CodexSetup codexSetup)
|
||||
{
|
||||
if (codexSetup.StorageQuota != null) return codexSetup.StorageQuota.SizeInBytes;
|
||||
|
||||
// Codex default: 8GB
|
||||
return 8.GB().SizeInBytes;
|
||||
}
|
||||
|
||||
private static string GetDashboardCreateRequest(string dashboardJson)
|
||||
{
|
||||
return $"{{\"dashboard\": {dashboardJson} ,\"message\": \"Default Codex Dashboard\",\"overwrite\": false}}";
|
||||
}
|
||||
}
|
||||
|
||||
public class GrafanaStartInfo
|
||||
{
|
||||
public GrafanaStartInfo(string[] dashboardUrls, RunningContainer container)
|
||||
{
|
||||
DashboardUrls = dashboardUrls;
|
||||
Container = container;
|
||||
}
|
||||
|
||||
public string[] DashboardUrls { get; }
|
||||
public RunningContainer Container { get; }
|
||||
}
|
||||
|
||||
public class GrafanaDataSourceRequest
|
||||
{
|
||||
public string uid { get; set; } = string.Empty;
|
||||
public string name { get; set; } = string.Empty;
|
||||
public string type { get; set; } = string.Empty;
|
||||
public string url { get; set; } = string.Empty;
|
||||
public string access { get; set; } = string.Empty;
|
||||
public bool basicAuth { get; set; }
|
||||
public GrafanaDataSourceJsonData jsonData { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GrafanaDataSourceResponse
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string message { get; set; } = string.Empty;
|
||||
public string name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class GrafanaDataSourceJsonData
|
||||
{
|
||||
public string httpMethod { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class GrafanaPostDashboardResponse
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string slug { get; set; } = string.Empty;
|
||||
public string status { get; set; } = string.Empty;
|
||||
public string uid { get; set; } = string.Empty;
|
||||
public string url { get; set; } = string.Empty;
|
||||
public int version { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using NUnit.Framework.Constraints;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public static class AssertHelpers
|
||||
{
|
||||
public static void RetryAssert<T>(IResolveConstraint constraint, Func<T> actual, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
var c = constraint.Resolve();
|
||||
Time.WaitUntil(() => c.ApplyTo(actual()).IsSuccess);
|
||||
}
|
||||
catch (TimeoutException)
|
||||
{
|
||||
Assert.That(actual(), constraint, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
using DistTestCore.Codex;
|
||||
using Logging;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public interface IFullConnectivityImplementation
|
||||
{
|
||||
string Description();
|
||||
string ValidateEntry(FullConnectivityHelper.Entry entry, FullConnectivityHelper.Entry[] allEntries);
|
||||
FullConnectivityHelper.PeerConnectionState Check(FullConnectivityHelper.Entry from, FullConnectivityHelper.Entry to);
|
||||
}
|
||||
|
||||
public class FullConnectivityHelper
|
||||
{
|
||||
private static string Nl = Environment.NewLine;
|
||||
private readonly BaseLog log;
|
||||
private readonly IFullConnectivityImplementation implementation;
|
||||
|
||||
public FullConnectivityHelper(BaseLog log, IFullConnectivityImplementation implementation)
|
||||
{
|
||||
this.log = log;
|
||||
this.implementation = implementation;
|
||||
}
|
||||
|
||||
public void AssertFullyConnected(IEnumerable<CodexAccess> nodes)
|
||||
{
|
||||
AssertFullyConnected(nodes.ToArray());
|
||||
}
|
||||
|
||||
private void AssertFullyConnected(CodexAccess[] nodes)
|
||||
{
|
||||
Log($"Asserting '{implementation.Description()}' for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
|
||||
var entries = CreateEntries(nodes);
|
||||
var pairs = CreatePairs(entries);
|
||||
|
||||
RetryWhilePairs(pairs, () =>
|
||||
{
|
||||
CheckAndRemoveSuccessful(pairs);
|
||||
});
|
||||
|
||||
if (pairs.Any())
|
||||
{
|
||||
var pairDetails = string.Join(Nl, pairs.SelectMany(p => p.GetResultMessages()));
|
||||
|
||||
Log($"Connections failed:{Nl}{pairDetails}");
|
||||
|
||||
Assert.Fail(string.Join(Nl, pairs.SelectMany(p => p.GetResultMessages())));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"'{implementation.Description()}' = Success! for nodes: {string.Join(",", nodes.Select(n => n.GetName()))}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RetryWhilePairs(List<Pair> pairs, Action action)
|
||||
{
|
||||
var timeout = DateTime.UtcNow + TimeSpan.FromMinutes(2);
|
||||
while (pairs.Any(p => p.Inconclusive) && timeout > DateTime.UtcNow)
|
||||
{
|
||||
action();
|
||||
|
||||
Time.Sleep(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
|
||||
private void CheckAndRemoveSuccessful(List<Pair> pairs)
|
||||
{
|
||||
// For large sets, don't try and do all of them at once.
|
||||
var selectedPair = pairs.Take(20).ToArray();
|
||||
var pairDetails = new List<string>();
|
||||
|
||||
foreach (var pair in selectedPair)
|
||||
{
|
||||
pair.Check();
|
||||
|
||||
if (pair.Success)
|
||||
{
|
||||
pairDetails.AddRange(pair.GetResultMessages());
|
||||
pairs.Remove(pair);
|
||||
}
|
||||
}
|
||||
|
||||
Log($"Connections successful:{Nl}{string.Join(Nl, pairDetails)}");
|
||||
}
|
||||
|
||||
private Entry[] CreateEntries(CodexAccess[] nodes)
|
||||
{
|
||||
var entries = nodes.Select(n => new Entry(n)).ToArray();
|
||||
|
||||
var errors = entries
|
||||
.Select(e => implementation.ValidateEntry(e, entries))
|
||||
.Where(s => !string.IsNullOrEmpty(s))
|
||||
.ToArray();
|
||||
|
||||
if (errors.Any())
|
||||
{
|
||||
Assert.Fail("Some node entries failed to validate: " + string.Join(Nl, errors));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private List<Pair> CreatePairs(Entry[] entries)
|
||||
{
|
||||
return CreatePairsIterator(entries).ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<Pair> CreatePairsIterator(Entry[] entries)
|
||||
{
|
||||
for (var x = 0; x < entries.Length; x++)
|
||||
{
|
||||
for (var y = x + 1; y < entries.Length; y++)
|
||||
{
|
||||
yield return new Pair(implementation, entries[x], entries[y]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
log.Log(msg);
|
||||
}
|
||||
|
||||
public class Entry
|
||||
{
|
||||
public Entry(CodexAccess node)
|
||||
{
|
||||
Node = node;
|
||||
Response = node.GetDebugInfo();
|
||||
}
|
||||
|
||||
public CodexAccess Node { get; }
|
||||
public CodexDebugResponse Response { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (Response == null || string.IsNullOrEmpty(Response.id)) return "UNKNOWN";
|
||||
return Response.id;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PeerConnectionState
|
||||
{
|
||||
Unknown,
|
||||
Connection,
|
||||
NoConnection,
|
||||
}
|
||||
|
||||
public class Pair
|
||||
{
|
||||
private TimeSpan aToBTime = TimeSpan.FromSeconds(0);
|
||||
private TimeSpan bToATime = TimeSpan.FromSeconds(0);
|
||||
private readonly IFullConnectivityImplementation implementation;
|
||||
|
||||
public Pair(IFullConnectivityImplementation implementation, Entry a, Entry b)
|
||||
{
|
||||
this.implementation = implementation;
|
||||
A = a;
|
||||
B = b;
|
||||
}
|
||||
|
||||
public Entry A { get; }
|
||||
public Entry B { get; }
|
||||
public PeerConnectionState AKnowsB { get; private set; }
|
||||
public PeerConnectionState BKnowsA { get; private set; }
|
||||
public bool Success { get { return AKnowsB == PeerConnectionState.Connection && BKnowsA == PeerConnectionState.Connection; } }
|
||||
public bool Inconclusive { get { return AKnowsB == PeerConnectionState.Unknown || BKnowsA == PeerConnectionState.Unknown; } }
|
||||
|
||||
public void Check()
|
||||
{
|
||||
aToBTime = Measure(() => AKnowsB = Check(A, B));
|
||||
bToATime = Measure(() => BKnowsA = Check(B, A));
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{string.Join(",", GetResultMessages())}]";
|
||||
}
|
||||
|
||||
public string[] GetResultMessages()
|
||||
{
|
||||
var aName = A.ToString();
|
||||
var bName = B.ToString();
|
||||
|
||||
return new[]
|
||||
{
|
||||
$"[{aName} --> {bName}] = {AKnowsB} ({aToBTime.TotalSeconds} seconds)",
|
||||
$"[{aName} <-- {bName}] = {BKnowsA} ({bToATime.TotalSeconds} seconds)"
|
||||
};
|
||||
}
|
||||
|
||||
private static TimeSpan Measure(Action action)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
action();
|
||||
return DateTime.UtcNow - start;
|
||||
}
|
||||
|
||||
private PeerConnectionState Check(Entry from, Entry to)
|
||||
{
|
||||
Thread.Sleep(10);
|
||||
|
||||
try
|
||||
{
|
||||
return implementation.Check(from, to);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Didn't get a conclusive answer. Try again later.
|
||||
return PeerConnectionState.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using DistTestCore.Codex;
|
||||
using Logging;
|
||||
using static DistTestCore.Helpers.FullConnectivityHelper;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public class PeerConnectionTestHelpers : IFullConnectivityImplementation
|
||||
{
|
||||
private readonly FullConnectivityHelper helper;
|
||||
|
||||
public PeerConnectionTestHelpers(BaseLog log)
|
||||
{
|
||||
helper = new FullConnectivityHelper(log, this);
|
||||
}
|
||||
|
||||
public void AssertFullyConnected(IEnumerable<IOnlineCodexNode> nodes)
|
||||
{
|
||||
AssertFullyConnected(nodes.Select(n => ((OnlineCodexNode)n).CodexAccess));
|
||||
}
|
||||
|
||||
public void AssertFullyConnected(IEnumerable<CodexAccess> nodes)
|
||||
{
|
||||
helper.AssertFullyConnected(nodes);
|
||||
}
|
||||
|
||||
public string Description()
|
||||
{
|
||||
return "Peer Discovery";
|
||||
}
|
||||
|
||||
public string ValidateEntry(Entry entry, Entry[] allEntries)
|
||||
{
|
||||
var result = string.Empty;
|
||||
foreach (var peer in entry.Response.table.nodes)
|
||||
{
|
||||
var expected = GetExpectedDiscoveryEndpoint(allEntries, peer);
|
||||
if (expected != peer.address)
|
||||
{
|
||||
result += $"Node:{entry.Node.GetName()} has incorrect peer table entry. Was: '{peer.address}', expected: '{expected}'. ";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public PeerConnectionState Check(Entry from, Entry to)
|
||||
{
|
||||
var peerId = to.Response.id;
|
||||
|
||||
var response = from.Node.GetDebugPeer(peerId);
|
||||
if (!response.IsPeerFound)
|
||||
{
|
||||
return PeerConnectionState.NoConnection;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(response.peerId) && response.addresses.Any())
|
||||
{
|
||||
return PeerConnectionState.Connection;
|
||||
}
|
||||
return PeerConnectionState.Unknown;
|
||||
}
|
||||
|
||||
private static string GetExpectedDiscoveryEndpoint(Entry[] allEntries, CodexDebugTableNodeResponse node)
|
||||
{
|
||||
var peer = allEntries.SingleOrDefault(e => e.Response.table.localNode.peerId == node.peerId);
|
||||
if (peer == null) return $"peerId: {node.peerId} is not known.";
|
||||
|
||||
var ip = peer.Node.Container.Pod.PodInfo.Ip;
|
||||
var discPort = peer.Node.Container.Recipe.GetPortByTag(CodexContainerRecipe.DiscoveryPortTag);
|
||||
return $"{ip}:{discPort.Number}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using DistTestCore.Codex;
|
||||
using Logging;
|
||||
using static DistTestCore.Helpers.FullConnectivityHelper;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public class PeerDownloadTestHelpers : IFullConnectivityImplementation
|
||||
{
|
||||
private readonly FullConnectivityHelper helper;
|
||||
private readonly BaseLog log;
|
||||
private readonly FileManager fileManager;
|
||||
private ByteSize testFileSize;
|
||||
|
||||
public PeerDownloadTestHelpers(BaseLog log, FileManager fileManager)
|
||||
{
|
||||
helper = new FullConnectivityHelper(log, this);
|
||||
testFileSize = 1.MB();
|
||||
this.log = log;
|
||||
this.fileManager = fileManager;
|
||||
}
|
||||
|
||||
public void AssertFullDownloadInterconnectivity(IEnumerable<IOnlineCodexNode> nodes, ByteSize testFileSize)
|
||||
{
|
||||
AssertFullDownloadInterconnectivity(nodes.Select(n => ((OnlineCodexNode)n).CodexAccess), testFileSize);
|
||||
}
|
||||
|
||||
public void AssertFullDownloadInterconnectivity(IEnumerable<CodexAccess> nodes, ByteSize testFileSize)
|
||||
{
|
||||
this.testFileSize = testFileSize;
|
||||
helper.AssertFullyConnected(nodes);
|
||||
}
|
||||
|
||||
public string Description()
|
||||
{
|
||||
return "Download Connectivity";
|
||||
}
|
||||
|
||||
public string ValidateEntry(Entry entry, Entry[] allEntries)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public PeerConnectionState Check(Entry from, Entry to)
|
||||
{
|
||||
fileManager.PushFileSet();
|
||||
var expectedFile = GenerateTestFile(from.Node, to.Node);
|
||||
|
||||
using var uploadStream = File.OpenRead(expectedFile.Filename);
|
||||
var contentId = Stopwatch.Measure(log, "Upload", () => from.Node.UploadFile(uploadStream));
|
||||
|
||||
try
|
||||
{
|
||||
var downloadedFile = Stopwatch.Measure(log, "Download", () => DownloadFile(to.Node, contentId, expectedFile.Label + "_downloaded"));
|
||||
expectedFile.AssertIsEqual(downloadedFile);
|
||||
return PeerConnectionState.Connection;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Should an exception occur during the download or file-content assertion,
|
||||
// We consider that as no-connection for the purpose of this test.
|
||||
return PeerConnectionState.NoConnection;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileManager.PopFileSet();
|
||||
}
|
||||
|
||||
// Should an exception occur during upload, then this try is inconclusive and we try again next loop.
|
||||
}
|
||||
|
||||
private TestFile DownloadFile(CodexAccess node, string contentId, string label)
|
||||
{
|
||||
var downloadedFile = fileManager.CreateEmptyTestFile(label);
|
||||
using var downloadStream = File.OpenWrite(downloadedFile.Filename);
|
||||
using var stream = node.DownloadFile(contentId);
|
||||
stream.CopyTo(downloadStream);
|
||||
return downloadedFile;
|
||||
}
|
||||
|
||||
private TestFile GenerateTestFile(CodexAccess uploader, CodexAccess downloader)
|
||||
{
|
||||
var up = uploader.GetName().Replace("<", "").Replace(">", "");
|
||||
var down = downloader.GetName().Replace("<", "").Replace(">", "");
|
||||
var label = $"~from:{up}-to:{down}~";
|
||||
return fileManager.GenerateTestFile(testFileSize, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using DistTestCore.Codex;
|
||||
using Logging;
|
||||
using NUnit.Framework;
|
||||
using static DistTestCore.Helpers.FullConnectivityHelper;
|
||||
|
||||
namespace DistTestCore.Helpers
|
||||
{
|
||||
public class PeerFetchTestHelpers : IFullConnectivityImplementation
|
||||
{
|
||||
private readonly FullConnectivityHelper helper;
|
||||
private readonly BaseLog log;
|
||||
private readonly IFileManager fileManager;
|
||||
private readonly ByteSize testFileSize;
|
||||
private readonly int expectedNumberOfBlocks;
|
||||
|
||||
public PeerFetchTestHelpers(BaseLog log, IFileManager fileManager)
|
||||
{
|
||||
helper = new FullConnectivityHelper(log, this);
|
||||
testFileSize = 10.MB();
|
||||
expectedNumberOfBlocks = 161;
|
||||
this.log = log;
|
||||
this.fileManager = fileManager;
|
||||
}
|
||||
|
||||
public void AssertFullFetchInterconnectivity(IEnumerable<IOnlineCodexNode> nodes)
|
||||
{
|
||||
AssertFullFetchInterconnectivity(nodes.Select(n => ((OnlineCodexNode)n).CodexAccess));
|
||||
}
|
||||
|
||||
public void AssertFullFetchInterconnectivity(IEnumerable<CodexAccess> nodes)
|
||||
{
|
||||
helper.AssertFullyConnected(nodes);
|
||||
}
|
||||
|
||||
public string Description()
|
||||
{
|
||||
return "Fetch connectivity";
|
||||
}
|
||||
|
||||
public string ValidateEntry(Entry entry, Entry[] allEntries)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public PeerConnectionState Check(Entry from, Entry to)
|
||||
{
|
||||
fileManager.PushFileSet();
|
||||
var expectedFile = GenerateTestFile(from.Node, to.Node);
|
||||
|
||||
using var uploadStream = File.OpenRead(expectedFile.Filename);
|
||||
var contentId = Stopwatch.Measure(log, "Upload", () => from.Node.UploadFile(uploadStream));
|
||||
var originalFetch = from.Node.DebugFetch(contentId);
|
||||
Assert.That(Convert.ToInt32(originalFetch.numberOfBlocks), Is.EqualTo(expectedNumberOfBlocks));
|
||||
Assert.That(originalFetch.originalBytes.Replace("'NByte", ""), Is.EqualTo("10485760"));
|
||||
|
||||
try
|
||||
{
|
||||
var fetchResponse = Stopwatch.Measure(log, "Fetch", () => to.Node.DebugFetch(contentId));
|
||||
AssertEqual(originalFetch, fetchResponse);
|
||||
return PeerConnectionState.Connection;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return PeerConnectionState.NoConnection;
|
||||
}
|
||||
finally
|
||||
{
|
||||
fileManager.PopFileSet();
|
||||
}
|
||||
}
|
||||
|
||||
private void AssertEqual(CodexDebugFetchResponse expected, CodexDebugFetchResponse actual)
|
||||
{
|
||||
Assert.That(actual.originalBytes, Is.EqualTo(expected.originalBytes));
|
||||
Assert.That(actual.blockSize, Is.EqualTo(expected.blockSize));
|
||||
Assert.That(actual.numberOfBlocks, Is.EqualTo(expected.numberOfBlocks));
|
||||
Assert.That(actual.version, Is.EqualTo(expected.version));
|
||||
Assert.That(actual.hcodec, Is.EqualTo(expected.hcodec));
|
||||
Assert.That(actual.codec, Is.EqualTo(expected.codec));
|
||||
Assert.That(actual.@protected, Is.EqualTo(expected.@protected));
|
||||
Assert.That(actual.blocks.Length, Is.EqualTo(expected.blocks.Length));
|
||||
for (var i = 0; i < expected.blocks.Length; i++)
|
||||
{
|
||||
Assert.That(actual.blocks[i].cid, Is.EqualTo(expected.blocks[i].cid));
|
||||
}
|
||||
}
|
||||
|
||||
private TestFile GenerateTestFile(CodexAccess uploader, CodexAccess downloader)
|
||||
{
|
||||
var up = uploader.GetName().Replace("<", "").Replace(">", "");
|
||||
var down = downloader.GetName().Replace("<", "").Replace(">", "");
|
||||
var label = $"~from:{up}-to:{down}~";
|
||||
return fileManager.GenerateTestFile(testFileSize, label);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Serialization = Newtonsoft.Json.Serialization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore
|
||||
{
|
||||
public class Http
|
||||
{
|
||||
private readonly BaseLog log;
|
||||
private readonly ITimeSet timeSet;
|
||||
private readonly Address address;
|
||||
private readonly string baseUrl;
|
||||
private readonly Action<HttpClient> onClientCreated;
|
||||
private readonly string? logAlias;
|
||||
|
||||
public Http(BaseLog log, ITimeSet timeSet, Address address, string baseUrl, string? logAlias = null)
|
||||
: this(log, timeSet, address, baseUrl, DoNothing, logAlias)
|
||||
{
|
||||
}
|
||||
|
||||
public Http(BaseLog log, ITimeSet timeSet, Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
|
||||
{
|
||||
this.log = log;
|
||||
this.timeSet = timeSet;
|
||||
this.address = address;
|
||||
this.baseUrl = baseUrl;
|
||||
this.onClientCreated = onClientCreated;
|
||||
this.logAlias = logAlias;
|
||||
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
|
||||
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
|
||||
}
|
||||
|
||||
public string HttpGetString(string route)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
using var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
Log(url, "");
|
||||
var result = Time.Wait(client.GetAsync(url));
|
||||
var str = Time.Wait(result.Content.ReadAsStringAsync());
|
||||
Log(url, str);
|
||||
return str; ;
|
||||
}, $"HTTP-GET:{route}");
|
||||
}
|
||||
|
||||
public T HttpGetJson<T>(string route)
|
||||
{
|
||||
var json = HttpGetString(route);
|
||||
return TryJsonDeserialize<T>(json);
|
||||
}
|
||||
|
||||
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
|
||||
{
|
||||
var response = PostJson(route, body);
|
||||
var json = Time.Wait(response.Content.ReadAsStringAsync());
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException(json);
|
||||
}
|
||||
Log(GetUrl() + route, json);
|
||||
return TryJsonDeserialize<TResponse>(json);
|
||||
}
|
||||
|
||||
public string HttpPostJson<TRequest>(string route, TRequest body)
|
||||
{
|
||||
var response = PostJson<TRequest>(route, body);
|
||||
return Time.Wait(response.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
public string HttpPostString(string route, string body)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
using var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
Log(url, body);
|
||||
var content = new StringContent(body);
|
||||
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
|
||||
var result = Time.Wait(client.PostAsync(url, content));
|
||||
var str = Time.Wait(result.Content.ReadAsStringAsync());
|
||||
Log(url, str);
|
||||
return str;
|
||||
}, $"HTTP-POST-STRING: {route}");
|
||||
}
|
||||
|
||||
public string HttpPostStream(string route, Stream stream)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
using var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
Log(url, "~ STREAM ~");
|
||||
var content = new StreamContent(stream);
|
||||
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
|
||||
var response = Time.Wait(client.PostAsync(url, content));
|
||||
var str = Time.Wait(response.Content.ReadAsStringAsync());
|
||||
Log(url, str);
|
||||
return str;
|
||||
}, $"HTTP-POST-STREAM: {route}");
|
||||
}
|
||||
|
||||
public Stream HttpGetStream(string route)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
Log(url, "~ STREAM ~");
|
||||
return Time.Wait(client.GetStreamAsync(url));
|
||||
}, $"HTTP-GET-STREAM: {route}");
|
||||
}
|
||||
|
||||
public T TryJsonDeserialize<T>(string json)
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
|
||||
{
|
||||
Error = delegate(object? sender, Serialization.ErrorEventArgs args)
|
||||
{
|
||||
if (args.CurrentObject == args.ErrorContext.OriginalObject)
|
||||
{
|
||||
errors.Add($"""
|
||||
Member: '{args.ErrorContext.Member?.ToString() ?? "<null>"}'
|
||||
Path: {args.ErrorContext.Path}
|
||||
Error: {args.ErrorContext.Error.Message}
|
||||
""");
|
||||
args.ErrorContext.Handled = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (errors.Count() > 0)
|
||||
{
|
||||
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
|
||||
}
|
||||
else if (deserialized == null)
|
||||
{
|
||||
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
|
||||
}
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
private HttpResponseMessage PostJson<TRequest>(string route, TRequest body)
|
||||
{
|
||||
return Retry(() =>
|
||||
{
|
||||
using var client = GetClient();
|
||||
var url = GetUrl() + route;
|
||||
using var content = JsonContent.Create(body);
|
||||
Log(url, JsonConvert.SerializeObject(body));
|
||||
return Time.Wait(client.PostAsync(url, content));
|
||||
}, $"HTTP-POST-JSON: {route}");
|
||||
}
|
||||
|
||||
private string GetUrl()
|
||||
{
|
||||
return $"{address.Host}:{address.Port}{baseUrl}";
|
||||
}
|
||||
|
||||
private void Log(string url, string message)
|
||||
{
|
||||
if (logAlias != null)
|
||||
{
|
||||
log.Debug($"({logAlias})({url}) = '{message}'", 3);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.Debug($"({url}) = '{message}'", 3);
|
||||
}
|
||||
}
|
||||
|
||||
private T Retry<T>(Func<T> operation, string description)
|
||||
{
|
||||
return Time.Retry(operation, timeSet.HttpCallRetryTime(), timeSet.HttpCallRetryDelay(), description);
|
||||
}
|
||||
|
||||
private HttpClient GetClient()
|
||||
{
|
||||
var client = new HttpClient();
|
||||
client.Timeout = timeSet.HttpCallTimeout();
|
||||
onClientCreated(client);
|
||||
return client;
|
||||
}
|
||||
|
||||
private static void DoNothing(HttpClient client)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DistTestCore.Logs
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class DontDownloadLogsAndMetricsOnFailureAttribute : PropertyAttribute
|
||||
{
|
||||
public const string DontDownloadKey = "DontDownloadLogsAndMetrics";
|
||||
|
||||
public DontDownloadLogsAndMetricsOnFailureAttribute()
|
||||
: base(DontDownloadKey)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Logging;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace DistTestCore.Logs
|
||||
{
|
||||
public interface IDownloadedLog
|
||||
{
|
||||
void AssertLogContains(string expectedString);
|
||||
string[] FindLinesThatContain(params string[] tags);
|
||||
void DeleteFile();
|
||||
}
|
||||
|
||||
public class DownloadedLog : IDownloadedLog
|
||||
{
|
||||
private readonly LogFile logFile;
|
||||
private readonly string owner;
|
||||
|
||||
public DownloadedLog(LogFile logFile, string owner)
|
||||
{
|
||||
this.logFile = logFile;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public void AssertLogContains(string expectedString)
|
||||
{
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (line.Contains(expectedString)) return;
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
|
||||
Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}");
|
||||
}
|
||||
|
||||
public string[] FindLinesThatContain(params string[] tags)
|
||||
{
|
||||
var result = new List<string>();
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (tags.All(line.Contains))
|
||||
{
|
||||
result.Add(line);
|
||||
}
|
||||
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
public void DeleteFile()
|
||||
{
|
||||
File.Delete(logFile.FullFilename);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
|
||||
namespace DistTestCore.Logs
|
||||
{
|
||||
public class LogDownloadHandler : LogHandler, ILogHandler
|
||||
{
|
||||
private readonly RunningContainer container;
|
||||
private readonly LogFile log;
|
||||
|
||||
public LogDownloadHandler(RunningContainer container, string description, LogFile log)
|
||||
{
|
||||
this.container = container;
|
||||
this.log = log;
|
||||
|
||||
log.Write($"{description} -->> {log.FullFilename}");
|
||||
log.WriteRaw(description);
|
||||
}
|
||||
|
||||
public DownloadedLog DownloadLog()
|
||||
{
|
||||
return new DownloadedLog(log, container.Name);
|
||||
}
|
||||
|
||||
protected override void ProcessLine(string line)
|
||||
{
|
||||
log.WriteRaw(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class CodexContractsContainerConfig
|
||||
{
|
||||
public CodexContractsContainerConfig(string bootstrapNodeIp, Port jsonRpcPort)
|
||||
{
|
||||
BootstrapNodeIp = bootstrapNodeIp;
|
||||
JsonRpcPort = jsonRpcPort;
|
||||
}
|
||||
|
||||
public string BootstrapNodeIp { get; }
|
||||
public Port JsonRpcPort { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class CodexContractsContainerRecipe : DefaultContainerRecipe
|
||||
{
|
||||
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
|
||||
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
|
||||
|
||||
public override string AppName => "codex-contracts";
|
||||
public override string Image => "codexstorage/codex-contracts-eth:latest-dist-tests";
|
||||
|
||||
protected override void InitializeRecipe(StartupConfig startupConfig)
|
||||
{
|
||||
var config = startupConfig.Get<CodexContractsContainerConfig>();
|
||||
|
||||
var ip = config.BootstrapNodeIp;
|
||||
var port = config.JsonRpcPort.Number;
|
||||
|
||||
AddEnvVar("DISTTEST_NETWORK_URL", $"http://{ip}:{port}");
|
||||
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
|
||||
AddEnvVar("KEEP_ALIVE", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using KubernetesWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class CodexContractsStarter : BaseStarter
|
||||
{
|
||||
|
||||
public CodexContractsStarter(TestLifecycle lifecycle)
|
||||
: base(lifecycle)
|
||||
{
|
||||
}
|
||||
|
||||
public MarketplaceInfo Start(GethBootstrapNodeInfo bootstrapNode)
|
||||
{
|
||||
LogStart("Deploying Codex Marketplace...");
|
||||
|
||||
var workflow = lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
var startupConfig = CreateStartupConfig(bootstrapNode.RunningContainers.Containers[0]);
|
||||
|
||||
var containers = workflow.Start(1, Location.Unspecified, new CodexContractsContainerRecipe(), startupConfig);
|
||||
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
|
||||
var container = containers.Containers[0];
|
||||
|
||||
WaitUntil(() =>
|
||||
{
|
||||
var logHandler = new ContractsReadyLogHandler(Debug);
|
||||
workflow.DownloadContainerLog(container, logHandler, null);
|
||||
return logHandler.Found;
|
||||
});
|
||||
Log("Contracts deployed. Extracting addresses...");
|
||||
|
||||
var extractor = new ContainerInfoExtractor(lifecycle.Log, workflow, container);
|
||||
var marketplaceAddress = extractor.ExtractMarketplaceAddress();
|
||||
var abi = extractor.ExtractMarketplaceAbi();
|
||||
|
||||
var interaction = bootstrapNode.StartInteraction(lifecycle);
|
||||
var tokenAddress = interaction.GetTokenAddress(marketplaceAddress);
|
||||
|
||||
LogEnd("Extract completed. Marketplace deployed.");
|
||||
|
||||
return new MarketplaceInfo(marketplaceAddress, abi, tokenAddress);
|
||||
}
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
{
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
private StartupConfig CreateStartupConfig(RunningContainer bootstrapContainer)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
var contractsConfig = new CodexContractsContainerConfig(bootstrapContainer.Pod.PodInfo.Ip, bootstrapContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag));
|
||||
startupConfig.Add(contractsConfig);
|
||||
return startupConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public class MarketplaceInfo
|
||||
{
|
||||
public MarketplaceInfo(string address, string abi, string tokenAddress)
|
||||
{
|
||||
Address = address;
|
||||
Abi = abi;
|
||||
TokenAddress = tokenAddress;
|
||||
}
|
||||
|
||||
public string Address { get; }
|
||||
public string Abi { get; }
|
||||
public string TokenAddress { get; }
|
||||
}
|
||||
|
||||
public class ContractsReadyLogHandler : LogHandler
|
||||
{
|
||||
// Log should contain 'Compiled 15 Solidity files successfully' at some point.
|
||||
private const string RequiredCompiledString = "Solidity files successfully";
|
||||
// When script is done, it prints the ready-string.
|
||||
private const string ReadyString = "Done! Sleeping indefinitely...";
|
||||
private readonly Action<string> debug;
|
||||
|
||||
public ContractsReadyLogHandler(Action<string> debug)
|
||||
{
|
||||
this.debug = debug;
|
||||
debug($"Looking for '{RequiredCompiledString}' and '{ReadyString}' in container logs...");
|
||||
}
|
||||
|
||||
public bool SeenCompileString { get; private set; }
|
||||
public bool Found { get; private set; }
|
||||
|
||||
protected override void ProcessLine(string line)
|
||||
{
|
||||
debug(line);
|
||||
if (line.Contains(RequiredCompiledString)) SeenCompileString = true;
|
||||
if (line.Contains(ReadyString))
|
||||
{
|
||||
if (!SeenCompileString) throw new Exception("CodexContracts deployment failed. " +
|
||||
"Solidity files not compiled before process exited.");
|
||||
|
||||
Found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Utils;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class ContainerInfoExtractor
|
||||
{
|
||||
private readonly BaseLog log;
|
||||
private readonly StartupWorkflow workflow;
|
||||
private readonly RunningContainer container;
|
||||
|
||||
public ContainerInfoExtractor(BaseLog log, StartupWorkflow workflow, RunningContainer container)
|
||||
{
|
||||
this.log = log;
|
||||
this.workflow = workflow;
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
public AllGethAccounts ExtractAccounts()
|
||||
{
|
||||
log.Debug();
|
||||
var accountsCsv = Retry(() => FetchAccountsCsv());
|
||||
if (string.IsNullOrEmpty(accountsCsv)) throw new InvalidOperationException("Unable to fetch accounts.csv for geth node. Test infra failure.");
|
||||
|
||||
var lines = accountsCsv.Split('\n');
|
||||
return new AllGethAccounts(lines.Select(ParseLineToAccount).ToArray());
|
||||
}
|
||||
|
||||
public string ExtractPubKey()
|
||||
{
|
||||
log.Debug();
|
||||
var pubKey = Retry(FetchPubKey);
|
||||
if (string.IsNullOrEmpty(pubKey)) throw new InvalidOperationException("Unable to fetch enode from geth node. Test infra failure.");
|
||||
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
public string ExtractMarketplaceAddress()
|
||||
{
|
||||
log.Debug();
|
||||
var marketplaceAddress = Retry(FetchMarketplaceAddress);
|
||||
if (string.IsNullOrEmpty(marketplaceAddress)) throw new InvalidOperationException("Unable to fetch marketplace account from codex-contracts node. Test infra failure.");
|
||||
|
||||
return marketplaceAddress;
|
||||
}
|
||||
|
||||
public string ExtractMarketplaceAbi()
|
||||
{
|
||||
log.Debug();
|
||||
var marketplaceAbi = Retry(FetchMarketplaceAbi);
|
||||
if (string.IsNullOrEmpty(marketplaceAbi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
|
||||
|
||||
return marketplaceAbi;
|
||||
}
|
||||
|
||||
private string FetchAccountsCsv()
|
||||
{
|
||||
return workflow.ExecuteCommand(container, "cat", GethContainerRecipe.AccountsFilename);
|
||||
}
|
||||
|
||||
private string FetchMarketplaceAddress()
|
||||
{
|
||||
var json = workflow.ExecuteCommand(container, "cat", CodexContractsContainerRecipe.MarketplaceAddressFilename);
|
||||
var marketplace = JsonConvert.DeserializeObject<MarketplaceJson>(json);
|
||||
return marketplace!.address;
|
||||
}
|
||||
|
||||
private string FetchMarketplaceAbi()
|
||||
{
|
||||
var json = workflow.ExecuteCommand(container, "cat", CodexContractsContainerRecipe.MarketplaceArtifactFilename);
|
||||
|
||||
var artifact = JObject.Parse(json);
|
||||
var abi = artifact["abi"];
|
||||
return abi!.ToString(Formatting.None);
|
||||
}
|
||||
|
||||
private string FetchPubKey()
|
||||
{
|
||||
var enodeFinder = new PubKeyFinder(s => log.Debug(s));
|
||||
workflow.DownloadContainerLog(container, enodeFinder, null);
|
||||
return enodeFinder.GetPubKey();
|
||||
}
|
||||
|
||||
private GethAccount ParseLineToAccount(string l)
|
||||
{
|
||||
var tokens = l.Replace("\r", "").Split(',');
|
||||
if (tokens.Length != 2) throw new InvalidOperationException();
|
||||
var account = tokens[0];
|
||||
var privateKey = tokens[1];
|
||||
return new GethAccount(account, privateKey);
|
||||
}
|
||||
|
||||
private static string Retry(Func<string> fetch)
|
||||
{
|
||||
return Time.Retry(fetch, nameof(ContainerInfoExtractor));
|
||||
}
|
||||
}
|
||||
|
||||
public class PubKeyFinder : LogHandler, ILogHandler
|
||||
{
|
||||
private const string openTag = "self=enode://";
|
||||
private const string openTagQuote = "self=\"enode://";
|
||||
private readonly Action<string> debug;
|
||||
private string pubKey = string.Empty;
|
||||
|
||||
public PubKeyFinder(Action<string> debug)
|
||||
{
|
||||
this.debug = debug;
|
||||
debug($"Looking for '{openTag}' in container logs...");
|
||||
}
|
||||
|
||||
public string GetPubKey()
|
||||
{
|
||||
if (string.IsNullOrEmpty(pubKey)) throw new Exception("Not found yet exception.");
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
protected override void ProcessLine(string line)
|
||||
{
|
||||
debug(line);
|
||||
if (line.Contains(openTag))
|
||||
{
|
||||
ExtractPubKey(openTag, line);
|
||||
}
|
||||
else if (line.Contains(openTagQuote))
|
||||
{
|
||||
ExtractPubKey(openTagQuote, line);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExtractPubKey(string tag, string line)
|
||||
{
|
||||
var openIndex = line.IndexOf(tag) + tag.Length;
|
||||
var closeIndex = line.IndexOf("@");
|
||||
|
||||
pubKey = line.Substring(
|
||||
startIndex: openIndex,
|
||||
length: closeIndex - openIndex);
|
||||
}
|
||||
}
|
||||
|
||||
public class MarketplaceJson
|
||||
{
|
||||
public string address { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using KubernetesWorkflow;
|
||||
using NethereumWorkflow;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class GethBootstrapNodeInfo
|
||||
{
|
||||
public GethBootstrapNodeInfo(RunningContainers runningContainers, AllGethAccounts allAccounts, string pubKey, Port discoveryPort)
|
||||
{
|
||||
RunningContainers = runningContainers;
|
||||
AllAccounts = allAccounts;
|
||||
Account = allAccounts.Accounts[0];
|
||||
PubKey = pubKey;
|
||||
DiscoveryPort = discoveryPort;
|
||||
}
|
||||
|
||||
public RunningContainers RunningContainers { get; }
|
||||
public AllGethAccounts AllAccounts { get; }
|
||||
public GethAccount Account { get; }
|
||||
public string PubKey { get; }
|
||||
public Port DiscoveryPort { get; }
|
||||
|
||||
public NethereumInteraction StartInteraction(TestLifecycle lifecycle)
|
||||
{
|
||||
var address = lifecycle.Configuration.GetAddress(RunningContainers.Containers[0]);
|
||||
var account = Account;
|
||||
|
||||
var creator = new NethereumInteractionCreator(lifecycle.Log, address.Host, address.Port, account.PrivateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
}
|
||||
|
||||
public class AllGethAccounts
|
||||
{
|
||||
public GethAccount[] Accounts { get; }
|
||||
|
||||
public AllGethAccounts(GethAccount[] accounts)
|
||||
{
|
||||
Accounts = accounts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using KubernetesWorkflow;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class GethBootstrapNodeStarter : BaseStarter
|
||||
{
|
||||
public GethBootstrapNodeStarter(TestLifecycle lifecycle)
|
||||
: base(lifecycle)
|
||||
{
|
||||
}
|
||||
|
||||
public GethBootstrapNodeInfo StartGethBootstrapNode()
|
||||
{
|
||||
LogStart("Starting Geth bootstrap node...");
|
||||
var startupConfig = CreateBootstrapStartupConfig();
|
||||
|
||||
var workflow = lifecycle.WorkflowCreator.CreateWorkflow();
|
||||
var containers = workflow.Start(1, Location.Unspecified, new GethContainerRecipe(), startupConfig);
|
||||
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Geth bootstrap node to be created. Test infra failure.");
|
||||
var bootstrapContainer = containers.Containers[0];
|
||||
|
||||
var extractor = new ContainerInfoExtractor(lifecycle.Log, workflow, bootstrapContainer);
|
||||
var accounts = extractor.ExtractAccounts();
|
||||
var pubKey = extractor.ExtractPubKey();
|
||||
var discoveryPort = bootstrapContainer.Recipe.GetPortByTag(GethContainerRecipe.DiscoveryPortTag);
|
||||
var result = new GethBootstrapNodeInfo(containers, accounts, pubKey, discoveryPort);
|
||||
|
||||
LogEnd($"Geth bootstrap node started with account '{result.Account.Account}'");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private StartupConfig CreateBootstrapStartupConfig()
|
||||
{
|
||||
var config = new StartupConfig();
|
||||
config.Add(new GethStartupConfig(true, null!, 0, 0));
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using KubernetesWorkflow;
|
||||
using NethereumWorkflow;
|
||||
|
||||
namespace DistTestCore.Marketplace
|
||||
{
|
||||
public class GethCompanionNodeInfo
|
||||
{
|
||||
public GethCompanionNodeInfo(RunningContainer runningContainer, GethAccount[] accounts)
|
||||
{
|
||||
RunningContainer = runningContainer;
|
||||
Accounts = accounts;
|
||||
}
|
||||
|
||||
public RunningContainer RunningContainer { get; }
|
||||
public GethAccount[] Accounts { get; }
|
||||
|
||||
public NethereumInteraction StartInteraction(TestLifecycle lifecycle, GethAccount account)
|
||||
{
|
||||
var address = lifecycle.Configuration.GetAddress(RunningContainer);
|
||||
var privateKey = account.PrivateKey;
|
||||
|
||||
var creator = new NethereumInteractionCreator(lifecycle.Log, address.Host, address.Port, privateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
}
|
||||
|
||||
public class GethAccount
|
||||
{
|
||||
public GethAccount(string account, string privateKey)
|
||||
{
|
||||
Account = account;
|
||||
PrivateKey = privateKey;
|
||||
}
|
||||
|
||||
public string Account { get; }
|
||||
public string PrivateKey { get; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user