Compare commits
380
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50b7e2300d | ||
|
|
a6379d02f1 | ||
|
|
6545f3469f | ||
|
|
58f7f9384a | ||
|
|
7ec9934751 | ||
|
|
c4c3f61a23 | ||
|
|
c856f404e3 | ||
|
|
eed989cbf5 | ||
|
|
630dc2814a | ||
|
|
570b174a00 | ||
|
|
700fc0ea40 | ||
|
|
23ebd4166b | ||
|
|
3683044bf7 | ||
|
|
fb10906816 | ||
|
|
86074dab6a | ||
|
|
015d8da21d | ||
|
|
d847c4f3ec | ||
|
|
a2e4869403 | ||
|
|
5ffe34bb83 | ||
|
|
e3b16fd742 | ||
|
|
80261959e7 | ||
|
|
7db9360ba4 | ||
|
|
899d775873 | ||
|
|
581cc80d5d | ||
|
|
24cf6c70b8 | ||
|
|
ae25b58610 | ||
|
|
2cf5a26934 | ||
|
|
5ed78da30b | ||
|
|
3fb1b212b6 | ||
|
|
ca7258ef28 | ||
|
|
1dbb749732 | ||
|
|
525fdcf4a7 | ||
|
|
40208373e4 | ||
|
|
4a0885cf2d | ||
|
|
0224f17733 | ||
|
|
b7ab2f994e | ||
|
|
0182ce134f | ||
|
|
69666d3fee | ||
|
|
605004286a | ||
|
|
bbc975141f | ||
|
|
0b2dcef57e | ||
|
|
9f7e95c515 | ||
|
|
8db84bfe7d | ||
|
|
9c8151bdb7 | ||
|
|
43aaed225d | ||
|
|
b805c5f004 | ||
|
|
29344451d6 | ||
|
|
5511f8ed32 | ||
|
|
4cd22f3719 | ||
|
|
f5291517c1 | ||
|
|
ceed913143 | ||
|
|
3086aa78d5 | ||
|
|
6c4b9345cb | ||
|
|
2ab84e2a61 | ||
|
|
48e7f98956 | ||
|
|
d7c7d47a61 | ||
|
|
be268ace42 | ||
|
|
a6b0b16909 | ||
|
|
8c64352acf | ||
|
|
fd38cc336b | ||
|
|
f5429b9c14 | ||
|
|
1344a6e3d2 | ||
|
|
9a2e889ef9 | ||
|
|
a58c9aba59 | ||
|
|
0263cc4eff | ||
|
|
b40215dd36 | ||
|
|
4e978bd5b5 | ||
|
|
0a8083d4d5 | ||
|
|
376fd60974 | ||
|
|
8bc63c1fdb | ||
|
|
4f461e4cb3 | ||
|
|
b25c747522 | ||
|
|
01d6b8f227 | ||
|
|
330552563c | ||
|
|
581cf2a813 | ||
|
|
f6edf6cbd5 | ||
|
|
db6db4d38c | ||
|
|
127b7daecc | ||
|
|
6597728e5c | ||
|
|
9e9773f45d | ||
|
|
9fe97f5d40 | ||
|
|
a53d2de13b | ||
|
|
d9d8441b6e | ||
|
|
e72c1b037c | ||
|
|
bce9a2c124 | ||
|
|
f57188914e | ||
|
|
3bd297a22d | ||
|
|
85774847b7 | ||
|
|
8fcf351613 | ||
|
|
05d28d1d0a | ||
|
|
a3253b42d8 | ||
|
|
b69059fd37 | ||
|
|
c122aa9910 | ||
|
|
03a3ccb4de | ||
|
|
87ec67778b | ||
|
|
bd5fe0cae1 | ||
|
|
e8213b9515 | ||
|
|
617b656cf7 | ||
|
|
042285e664 | ||
|
|
7d28b62206 | ||
|
|
a27b294031 | ||
|
|
0a4c4c60c5 | ||
|
|
635849c37a | ||
|
|
3cc3a2a9dd | ||
|
|
6d4d6fcdb9 | ||
|
|
4fdb310ca4 | ||
|
|
9ce82cb011 | ||
|
|
db37143db6 | ||
|
|
2a5b3e0eec | ||
|
|
ef53fe02a3 | ||
|
|
a6c2bf5230 | ||
|
|
32f56e8213 | ||
|
|
f757e64ba4 | ||
|
|
188f765b5c | ||
|
|
8c8b1748ee | ||
|
|
c204ab09d1 | ||
|
|
db5c5444c5 | ||
|
|
209eb31c58 | ||
|
|
956cdbcfbe | ||
|
|
c5fb066c75 | ||
|
|
90b90be3cb | ||
|
|
a6a8c3f1e7 | ||
|
|
b7904f7ee0 | ||
|
|
23396f54e0 | ||
|
|
e42f1ddbd7 | ||
|
|
d1a70d2465 | ||
|
|
3c8efbd0a0 | ||
|
|
247e010fba | ||
|
|
f0167a800f | ||
|
|
b35cf3139d | ||
|
|
64ecf531bd | ||
|
|
ec0f7a6790 | ||
|
|
930c44f037 | ||
|
|
a6fce1084d | ||
|
|
23b7bbd548 | ||
|
|
42d3c5cd2c | ||
|
|
1bf693938c | ||
|
|
da4101a042 | ||
|
|
dc33f0655c | ||
|
|
3c210f96fc | ||
|
|
dd5baeda46 | ||
|
|
7d33c1c113 | ||
|
|
8a4d9fecea | ||
|
|
cb259a9086 | ||
|
|
9d8ec6f794 | ||
|
|
acc9526cd5 | ||
|
|
43a160a9cc | ||
|
|
4de2626a33 | ||
|
|
255e08a301 | ||
|
|
71409b06ae | ||
|
|
f25bca727d | ||
|
|
fb73312e80 | ||
|
|
9a626b7260 | ||
|
|
ede80410ea | ||
|
|
00c720137a | ||
|
|
890cff93d5 | ||
|
|
678b719cef | ||
|
|
1b7c11b849 | ||
|
|
c6a7489f11 | ||
|
|
f7fcef56c7 | ||
|
|
29fa554146 | ||
|
|
2f10b30283 | ||
|
|
391a2653d9 | ||
|
|
55be07d711 | ||
|
|
75757e37fb | ||
|
|
2be31a4d3b | ||
|
|
4b74a9d5fe | ||
|
|
4f2539c59f | ||
|
|
7df1f3da7b | ||
|
|
fb57998aa8 | ||
|
|
c4eceacafb | ||
|
|
974fc460c4 | ||
|
|
87e84ae90d | ||
|
|
2c026f99ca | ||
|
|
6298744918 | ||
|
|
5342261db0 | ||
|
|
fd540a53b1 | ||
|
|
03283414cb | ||
|
|
beaa67c280 | ||
|
|
d67ddab290 | ||
|
|
b143136590 | ||
|
|
8fc8a3f7cf | ||
|
|
2b21722cf3 | ||
|
|
c3fb7384e3 | ||
|
|
fde19383df | ||
|
|
777e414f0a | ||
|
|
6d7309cc9c | ||
|
|
ec03be6936 | ||
|
|
5dc918287c | ||
|
|
55811b20da | ||
|
|
09554da362 | ||
|
|
074f5ebfae | ||
|
|
260bc93414 | ||
|
|
ad05dc07f0 | ||
|
|
12ea002660 | ||
|
|
4c46a708ab | ||
|
|
b57a727195 | ||
|
|
00207de04d | ||
|
|
3761e236a3 | ||
|
|
ff52e8e841 | ||
|
|
b61f5d835c | ||
|
|
1a86d3459d | ||
|
|
6bd7098cfe | ||
|
|
06ff7c9760 | ||
|
|
703f410161 | ||
|
|
46ab3b31ca | ||
|
|
a84d6a3c22 | ||
|
|
54471d41d5 | ||
|
|
8b224f6922 | ||
|
|
005c9b4259 | ||
|
|
6f53e78e6a | ||
|
|
f4532f3801 | ||
|
|
b90c47a994 | ||
|
|
db55792f42 | ||
|
|
5996c0fa63 | ||
|
|
485e3cf02e | ||
|
|
ee319a6d0f | ||
|
|
b47b596062 | ||
|
|
7de0e5a1c4 | ||
|
|
7114fd1c00 | ||
|
|
4192952a37 | ||
|
|
b82c74865e | ||
|
|
ca350604e3 | ||
|
|
5a7608460b | ||
|
|
2565ced7fe | ||
|
|
a81f0b9145 | ||
|
|
1f841ca629 | ||
|
|
537c37351a | ||
|
|
0f9e01e393 | ||
|
|
90118b3481 | ||
|
|
412a4d3c7a | ||
|
|
960bef455a | ||
|
|
344d3cd44f | ||
|
|
6636ef3ac1 | ||
|
|
496f8fa370 | ||
|
|
25148db971 | ||
|
|
71ad471958 | ||
|
|
96ff3c38bb | ||
|
|
ed56d9edcc | ||
|
|
b78f527c39 | ||
|
|
096282ae1a | ||
|
|
0be6df46bc | ||
|
|
c5cc15daa0 | ||
|
|
0301c3b076 | ||
|
|
84382b4ab4 | ||
|
|
26ec841db1 | ||
|
|
1ed93bbdde | ||
|
|
ef83441b2f | ||
|
|
180bf3fd35 | ||
|
|
d1403c04c4 | ||
|
|
8d12bc45a1 | ||
|
|
9f0f7c374a | ||
|
|
b8ce4c49d6 | ||
|
|
90070de028 | ||
|
|
073f0d86a1 | ||
|
|
ec6c987ef9 | ||
|
|
3f8699a59b | ||
|
|
2b463d060d | ||
|
|
9d464708b5 | ||
|
|
3e49ce6ae9 | ||
|
|
6e60a8614c | ||
|
|
eecdcf308d | ||
|
|
3e500e8346 | ||
|
|
ea66b5b408 | ||
|
|
655f4895d0 | ||
|
|
dae4aa1f64 | ||
|
|
20f92633f7 | ||
|
|
d0bed64c68 | ||
|
|
1412dd84fe | ||
|
|
ad88560061 | ||
|
|
8d0b3feff7 | ||
|
|
6672427565 | ||
|
|
4f9c0e0ce7 | ||
|
|
dc9f3ab090 | ||
|
|
ead1ff7ec6 | ||
|
|
807e8b7b5f | ||
|
|
db0a21bc60 | ||
|
|
19d466d5d6 | ||
|
|
f9408ab3b5 | ||
|
|
438c4271e1 | ||
|
|
fe11a97458 | ||
|
|
b8774e7273 | ||
|
|
bc51fc2e30 | ||
|
|
5241144e99 | ||
|
|
49300273e0 | ||
|
|
fcb5a527a9 | ||
|
|
a6f7bc2393 | ||
|
|
ac07327d77 | ||
|
|
b5e5570145 | ||
|
|
c348ca9849 | ||
|
|
bfdbebb36e | ||
|
|
e87f255f48 | ||
|
|
85ad0b414f | ||
|
|
901259d0fa | ||
|
|
62cc9e0587 | ||
|
|
d7827a4531 | ||
|
|
f17d123af8 | ||
|
|
d5b87253ae | ||
|
|
529f923595 | ||
|
|
a362eb1e96 | ||
|
|
65473d8c15 | ||
|
|
9b1ab3185f | ||
|
|
939eed544d | ||
|
|
3914d58a6a | ||
|
|
4102ce0a04 | ||
|
|
69296577f8 | ||
|
|
bd9fc3a3cf | ||
|
|
f148598a21 | ||
|
|
6e82d6b1e6 | ||
|
|
6d44a0ccfc | ||
|
|
fb70daa2e9 | ||
|
|
14e8222dfe | ||
|
|
dc0edce251 | ||
|
|
b8b9e6b997 | ||
|
|
29672ece68 | ||
|
|
a0461a446e | ||
|
|
5aff8c6f6d | ||
|
|
dfe477d192 | ||
|
|
ade08a27fe | ||
|
|
cc8a860f41 | ||
|
|
d4522f0d9c | ||
|
|
b74349cc68 | ||
|
|
49f6c7e37e | ||
|
|
e073f7a881 | ||
|
|
3326c42f7a | ||
|
|
eb70fe612b | ||
|
|
b3ba39b2e5 | ||
|
|
abb9560b6d | ||
|
|
2fae9505d6 | ||
|
|
b2b338d0a5 | ||
|
|
0e087c6fee | ||
|
|
58816430e3 | ||
|
|
5a021a4bfe | ||
|
|
a68e849768 | ||
|
|
020865f5c0 | ||
|
|
4280f910ae | ||
|
|
ec8a041257 | ||
|
|
bcb690d143 | ||
|
|
840e794761 | ||
|
|
854325f10c | ||
|
|
7a6d7d787b | ||
|
|
cc2e8d5992 | ||
|
|
4adce837ec | ||
|
|
e11a7d1600 | ||
|
|
ad70394333 | ||
|
|
50fbf0ad52 | ||
|
|
45fbd699a9 | ||
|
|
bf18fa03a2 | ||
|
|
116f62e73e | ||
|
|
8ef2e6023e | ||
|
|
e16b1ce079 | ||
|
|
4aa4731480 | ||
|
|
8ad2dee67c | ||
|
|
869aeb9253 | ||
|
|
8910c7ff27 | ||
|
|
2b10f2ec58 | ||
|
|
991927b95f | ||
|
|
b1bd1de027 | ||
|
|
3b258c9e2e | ||
|
|
0fd6a6f06e | ||
|
|
2fea475237 | ||
|
|
45050c34e4 | ||
|
|
43fa57dc97 | ||
|
|
3a8bb760ef | ||
|
|
766e2f5c20 | ||
|
|
888b19d8e5 | ||
|
|
f33866efc1 | ||
|
|
8c7229504e | ||
|
|
bcb05cd0c9 | ||
|
|
7179c70463 | ||
|
|
b3da42522f | ||
|
|
6b1102efa7 | ||
|
|
8c82b4c527 | ||
|
|
d0cafb83a1 | ||
|
|
8e4d43b73b | ||
|
|
8f37b4cf38 | ||
|
|
1a277ef1b5 | ||
|
|
b81d574a4b | ||
|
|
7aae48d489 | ||
|
|
58016378c4 |
@@ -1,81 +0,0 @@
|
||||
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 (codex-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
namespace:
|
||||
description: Runner namespace (default)
|
||||
required: false
|
||||
type: string
|
||||
command:
|
||||
description: Runner command (dotnet test Tests)
|
||||
required: false
|
||||
type: string
|
||||
|
||||
|
||||
env:
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
|
||||
NAMEPREFIX: codex-dist-tests
|
||||
NAMESPACE: default
|
||||
COMMAND: dotnet test Tests/CodexTests
|
||||
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 "${{ github.event.inputs.branch }}" ]] && echo "BRANCH=${{ github.event.inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ github.event.inputs.source }}" ]] && echo "SOURCE=${{ github.event.inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ github.event.inputs.nameprefix }}" ]] && echo "NAMEPREFIX=${{ github.event.inputs.nameprefix }}" >>"$GITHUB_ENV" || echo "NAMEPREFIX=${{ env.NAMEPREFIX }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ github.event.inputs.namespace }}" ]] && echo "NAMESPACE=${{ github.event.inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ github.event.inputs.command }}" ]] && COMMAND="${{ github.event.inputs.command }}" || COMMAND="${{ env.COMMAND }}"
|
||||
echo "COMMAND=$(jq -c 'split(" ")' <<< '"'$COMMAND'"')" >>"$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,28 @@
|
||||
name: Docker - Discord Bot
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Tools/BiblioTech/**'
|
||||
- '!Tools/BiblioTech/docker/docker-compose.yaml'
|
||||
- 'Framework/**'
|
||||
- 'ProjectPlugins/**'
|
||||
- .github/workflows/docker-discordbot.yml
|
||||
- .github/workflows/docker-reusable.yml
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
uses: ./.github/workflows/docker-reusable.yml
|
||||
with:
|
||||
docker_file: Tools/BiblioTech/docker/Dockerfile
|
||||
docker_repo: codexstorage/codex-discordbot
|
||||
secrets: inherit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Docker - Reusable
|
||||
name: Reusable - Docker
|
||||
|
||||
|
||||
on:
|
||||
@@ -66,26 +66,26 @@ jobs:
|
||||
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Build and Push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKER_FILE }}
|
||||
@@ -101,10 +101,10 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Docker - Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests
|
||||
path: /tmp/digests/*
|
||||
name: digests-${{ matrix.target.arch }}
|
||||
path: /tmp/digests
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
@@ -139,17 +139,18 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Docker - Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
path: /tmp/digests
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
flavor: |
|
||||
@@ -161,7 +162,7 @@ jobs:
|
||||
type=sha,enable=${{ env.TAG_SHA }}
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Docker - Rewarder Bot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Tools/TestNetRewarder/**'
|
||||
- '!Tools/TestNetRewarder/docker/docker-compose.yaml'
|
||||
- 'Framework/**'
|
||||
- 'ProjectPlugins/**'
|
||||
- .github/workflows/docker-rewarder.yml
|
||||
- .github/workflows/docker-reusable.yml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
uses: ./.github/workflows/docker-reusable.yml
|
||||
with:
|
||||
docker_file: Tools/TestNetRewarder/docker/Dockerfile
|
||||
docker_repo: codexstorage/codex-rewarderbot
|
||||
secrets: inherit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Docker
|
||||
name: Docker - Runner
|
||||
|
||||
|
||||
on:
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
paths:
|
||||
- docker/Dockerfile
|
||||
- docker/docker-entrypoint.sh
|
||||
- .github/workflows/docker.yml
|
||||
- .github/workflows/docker-runner.yml
|
||||
- .github/workflows/docker-reusable.yml
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Report - HoldMyBeerTest
|
||||
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
# - cron: '30 1 */2 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: Call runner
|
||||
uses: ./.github/workflows/run-continuous-tests.yaml
|
||||
with:
|
||||
source: ${{ format('{0}/{1}', github.server_url, github.repository) }}
|
||||
branch: master
|
||||
nameprefix: c-tests-report-holdmybeertest-48h
|
||||
tests_target_duration: 48h
|
||||
tests_filter: HoldMyBeerTest
|
||||
tests_cleanup: true
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,20 @@
|
||||
name: Report - PeersTest
|
||||
|
||||
|
||||
on:
|
||||
# schedule:
|
||||
# - cron: '30 1 */2 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: Call runner
|
||||
uses: ./.github/workflows/run-continuous-tests.yaml
|
||||
with:
|
||||
source: ${{ format('{0}/{1}', github.server_url, github.repository) }}
|
||||
branch: master
|
||||
nameprefix: c-tests-report-peerstest-48h
|
||||
tests_target_duration: 48h
|
||||
tests_filter: PeersTest
|
||||
tests_cleanup: true
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,170 @@
|
||||
name: Run Continuous Tests
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source:
|
||||
description: Repository with tests (current)
|
||||
required: false
|
||||
type: string
|
||||
branch:
|
||||
description: Branch with tests (master)
|
||||
required: false
|
||||
type: string
|
||||
codexdockerimage:
|
||||
description: Codex Docker image (codexstorage/nim-codex:latest-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
nameprefix:
|
||||
description: Resources prefix (c-tests)
|
||||
required: false
|
||||
type: string
|
||||
tests_target_duration:
|
||||
description: Runner target duration (2d)
|
||||
required: false
|
||||
type: string
|
||||
tests_filter:
|
||||
description: Runner tests filter ("")
|
||||
required: false
|
||||
type: string
|
||||
tests_cleanup:
|
||||
description: Runner tests cleanup
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
source:
|
||||
description: Repository with tests (current)
|
||||
required: false
|
||||
type: string
|
||||
branch:
|
||||
description: Branch with tests (master)
|
||||
required: false
|
||||
type: string
|
||||
codexdockerimage:
|
||||
description: Codex Docker image (codexstorage/nim-codex:latest-dist-tests)
|
||||
required: false
|
||||
type: string
|
||||
nameprefix:
|
||||
description: Resources prefix (c-tests)
|
||||
required: false
|
||||
type: string
|
||||
tests_target_duration:
|
||||
description: Runner target duration (2d)
|
||||
required: false
|
||||
type: string
|
||||
tests_filter:
|
||||
description: Runner tests filter ("")
|
||||
required: false
|
||||
type: string
|
||||
tests_cleanup:
|
||||
description: Runner tests cleanup
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
workflow_source:
|
||||
description: Workflow source
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
|
||||
env:
|
||||
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
CODEXDOCKERIMAGE: codexstorage/nim-codex:latest-dist-tests
|
||||
NAMEPREFIX: c-tests
|
||||
NAMESPACE: default
|
||||
TESTS_TARGET_DURATION: 2d
|
||||
TESTS_FILTER: ""
|
||||
TESTS_CLEANUP: true
|
||||
JOB_MANIFEST: docker/continuous-tests-job.yaml
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
KUBE_VERSION: v1.28.2
|
||||
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: Run Continuous Tests ${{ inputs.tests_filter }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.workflow_source }}
|
||||
|
||||
- name: Variables
|
||||
run: |
|
||||
RUNID=$(date +%Y%m%d-%H%M%S)
|
||||
echo "RUNID=${RUNID}" >> $GITHUB_ENV
|
||||
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.codexdockerimage }}" ]] && echo "CODEXDOCKERIMAGE=${{ inputs.codexdockerimage }}" >>"$GITHUB_ENV" || echo "CODEXDOCKERIMAGE=${{ env.CODEXDOCKERIMAGE }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.nameprefix }}" ]] && NAMEPREFIX="`awk '{ print tolower($0) }' <<< ${{ inputs.nameprefix }}`" || NAMEPREFIX="`awk '{ print tolower($0) }' <<< ${{ env.NAMEPREFIX }}`"
|
||||
echo "NAMEPREFIX=${NAMEPREFIX}-${RUNID}" >>"$GITHUB_ENV"
|
||||
echo "DEPLOYMENT_NAMESPACE=${NAMEPREFIX}-${RUNID}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.tests_target_duration }}" ]] && echo "TESTS_TARGET_DURATION=${{ inputs.tests_target_duration }}" >>"$GITHUB_ENV" || echo "TESTS_TARGET_DURATION=${{ env.TESTS_TARGET_DURATION }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.tests_filter }}" ]] && echo "TESTS_FILTER=${{ inputs.tests_filter }}" >>"$GITHUB_ENV" || echo "TESTS_FILTERS=${{ env.TESTS_FILTERS }}" >>"$GITHUB_ENV"
|
||||
[[ -n "${{ inputs.tests_cleanup }}" ]] && echo "TESTS_CLEANUP=${{ inputs.tests_cleanup }}" >>"$GITHUB_ENV" || echo "TESTS_CLEANUP=${{ env.TESTS_CLEANUP }}" >>"$GITHUB_ENV"
|
||||
|
||||
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
|
||||
uses: azure/setup-kubectl@v4
|
||||
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 -
|
||||
|
||||
- name: Tests Identification
|
||||
run: |
|
||||
echo "----"
|
||||
echo "Repository: ${{ env.SOURCE }}"
|
||||
echo "Branch: ${{ env.BRANCH }}"
|
||||
echo "Runner job: ${{ env.NAMEPREFIX }}"
|
||||
echo "Runner pod: `kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -ojsonpath='{.items[0].metadata.name}'`"
|
||||
echo "Runner namespace: ${{ env.NAMESPACE }}"
|
||||
echo "----"
|
||||
echo "Tests runid: ${{ env.RUNID }}"
|
||||
echo "Tests codexdockerimage: ${{ env.CODEXDOCKERIMAGE }}"
|
||||
echo "Tests namespace: ${{ env.DEPLOYMENT_NAMESPACE }}"
|
||||
echo "Tests duration: ${{ env.TESTS_TARGET_DURATION }}"
|
||||
echo "Tests filter: ${{ env.TESTS_FILTER }}"
|
||||
echo "Tests cleanup: ${{ env.TESTS_CLEANUP }}"
|
||||
echo "----"
|
||||
|
||||
- name: Show Runner logs
|
||||
if: false
|
||||
run: |
|
||||
# Variables
|
||||
# We need more than 300 seconds because Auto Scaler may take 3 minutes to tun a node
|
||||
duration=600
|
||||
namespace="${{ env.NAMESPACE }}"
|
||||
pod=$(kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -o jsonpath="{.items[0].metadata.name}")
|
||||
|
||||
# Check Pod status
|
||||
WAIT=120
|
||||
SECONDS=0
|
||||
sleep=1
|
||||
while (( SECONDS < WAIT )); do
|
||||
phase=$(kubectl get pod ${pod} -n ${namespace} -o jsonpath="{.status.phase}")
|
||||
[[ "${phase}" == "Running" ]] && { echo "Pod $pod is in $phase state - Get the logs"; break; } || { echo "Pod $pod is in $phase state - Retry in $sleep seconds / $((WAIT - SECONDS))"; }
|
||||
sleep $sleep
|
||||
done
|
||||
|
||||
# Get logs
|
||||
timeout $duration \
|
||||
kubectl logs $pod \
|
||||
-n $namespace \
|
||||
-f \
|
||||
--tail=-1 \
|
||||
--timestamps || true
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Run Dist Tests
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source:
|
||||
description: Repository with tests (current)
|
||||
required: false
|
||||
type: string
|
||||
branch:
|
||||
description: Branch with tests (master)
|
||||
required: false
|
||||
type: string
|
||||
nameprefix:
|
||||
description: Runner name prefix (d-tests-runner)
|
||||
required: false
|
||||
type: string
|
||||
namespace:
|
||||
description: Runner namespace (default)
|
||||
required: false
|
||||
type: string
|
||||
command:
|
||||
description: Command (dotnet test Tests/CodexTests)
|
||||
required: false
|
||||
type: string
|
||||
|
||||
|
||||
env:
|
||||
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
NAMEPREFIX: d-tests-runner
|
||||
NAMESPACE: default
|
||||
COMMAND: dotnet test Tests/CodexTests
|
||||
JOB_MANIFEST: docker/dist-tests-job.yaml
|
||||
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
|
||||
KUBE_VERSION: v1.28.2
|
||||
|
||||
|
||||
jobs:
|
||||
run_tests:
|
||||
name: Run Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Variables
|
||||
run: |
|
||||
echo "COMMAND=$(jq -c 'split(" ")' <<< '"'$COMMAND'"')" >>"$GITHUB_ENV"
|
||||
echo "RUNID=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
|
||||
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
[[ -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"
|
||||
[[ -n "${{ inputs.command }}" ]] && COMMAND="${{ inputs.command }}" || COMMAND="${{ env.COMMAND }}"
|
||||
|
||||
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
|
||||
uses: azure/setup-kubectl@v4
|
||||
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 -
|
||||
|
||||
- name: Tests Identification
|
||||
run: |
|
||||
echo "----"
|
||||
echo "Repository: ${{ env.SOURCE }}"
|
||||
echo "Branch: ${{ env.BRANCH }}"
|
||||
echo "Runner job: ${{ env.NAMEPREFIX }}-${{ env.RUNID }}"
|
||||
echo "Runner namespace: ${{ env.NAMESPACE }}"
|
||||
echo "----"
|
||||
echo "Tests runid: ${{ env.RUNID }}"
|
||||
echo "Tests command: `jq -r '. | join(" ")' <<< '${{ env.COMMAND }}'`"
|
||||
echo "----"
|
||||
+86
-14
@@ -1,10 +1,26 @@
|
||||
# Distributed System Tests for Nim-Codex
|
||||
|
||||
## Contributing plugins
|
||||
The testing framework was created for testing Codex. However, it's been designed such that other distributed/containerized projects can 'easily' be added. In order to add your project to the framework you must:
|
||||
The testing framework was created for testing Codex. However, it's been designed such that other containerized projects can 'easily' be added.
|
||||
|
||||
In this file, you'll see 'users' (in quote) mentioned once or twice. This refers to code/projects/tests which end up making use of your plugin. 'Users' come in many shapes and sizes and tend to have many differen use-cases in mind. Please consider this when reading this document and writing your plugin.
|
||||
|
||||
## Checklist
|
||||
Your application must pass this checklist to be compatible with the framework:
|
||||
- It runs in a docker container.
|
||||
- It can be configured via environment variables. (You may need to create a docker image which contains a shell script, to pass some env-vars as CLI arguments to your application. Container command overrides do work, but are not equally reliable across container platforms. When in doubt: use env-var!)
|
||||
- It has network interaction:
|
||||
- It exposes one or more APIs via one or more ports, OR
|
||||
- It makes calls to other services. (OR both.)
|
||||
|
||||
If your application's use-cases rely primarily on shell interaction, this framework might not be for you. The framework allows you to execute commands in containers AND read stdout/stderr responses. However, its focus during development has always been webservice API interactions.
|
||||
|
||||
## Steps
|
||||
In order to add your project to the framework you must:
|
||||
1. Create a library assembly in the project plugins folder.
|
||||
1. It must contain a type that implements the `IProjectPlugin` interface from the `Core` assembly.
|
||||
1. If your plugin wants to expose any specific methods or objects to the code using the framework (the tests and tools), it must implement extensions for the `CoreInterface` type.
|
||||
1. If your plugin wants to expose any specific methods or objects to 'users', it must implement extensions for the `CoreInterface` type.
|
||||
1. If your plugin wants to run containers of its own project, it must provide a recipe.
|
||||
|
||||
## Constructors & Tools
|
||||
Your implementation of `IProjectPlugin` must have a public constructor with a single argument of type `IPluginTools`, for example:
|
||||
@@ -20,19 +36,34 @@ Your implementation of `IProjectPlugin` must have a public constructor with a si
|
||||
}
|
||||
```
|
||||
|
||||
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients for containers. (Without having to figure out addresses manually.)
|
||||
|
||||
## Plugin Interfaces
|
||||
The `IProjectPlugin` interface requires the implementation of two methods.
|
||||
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version information at this time if applicable.
|
||||
1. `Decommission` - Should your plugin have any active system resources, free them in this method.
|
||||
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version and/or configuration information at this time if applicable.
|
||||
1. `Decommission` - Should your plugin have any active system resources, free them in this method. Please note that resources managed by the framework (such as running containers and tracked data files) do *not* need to be manually disposed in this method. `Decommission` is to be used for resources not managed by the framework.
|
||||
|
||||
There are a few optional interfaces your plugin may choose to implement. The framework will automatically use these interfaces.
|
||||
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string with will be prepended to all log statements made by your plugin.
|
||||
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by code that uses your plugin.
|
||||
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string which will be prepended to all log statements made by your plugin. A polite thing to do.
|
||||
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by 'users' of your plugin. Often this data finds its way into log files and container-descriptors in order to help track versions/tests/deployments, etc.
|
||||
|
||||
## IPluginTools
|
||||
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients to make calls to containers. (Figure out addresses and ports for containers is handled by the framework.)
|
||||
|
||||
It is possible and allowed for your plugin to depend on and use other plugins. (For example, maybe your project wants to interact with Ethereum and wants to use the GethPlugin to talk to a Geth node.) `IPluginTools` is *not* what is used for accessing functionality of other plugins. See 'Core Interface' section.
|
||||
|
||||
ILog GetLog();
|
||||
IHttp CreateHttp(Action<HttpClient> onClientCreated);
|
||||
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
|
||||
IHttp CreateHttp();
|
||||
IFileManager GetFileManager();
|
||||
|
||||
The plugin tools provide:
|
||||
1. `Workflow` - This tool allows you to start and stop containers using "container recipes". (More on those below.) It also allows you to execute commands inside a container, access stdout/stderr, detect crashes, and access pod deployment information. The workflow tool also lets you inspect the locations available in the cluster, and decide where you want to run containers. (More on that below as well.)
|
||||
1. `Log` - Good logging is priceless. Use this tool to get a log object handle, and write useful debug/info/error statements.
|
||||
1. `Http` - This tool gives you a convenient way to access a standard dotnet HttpClient, and takes care of timeouts and retries (in accordance with the config). Additionally, it combos nicely with container objects created by `Workflow`, such that you never have to spend any time figuring out the addresses and ports of your containers.
|
||||
1. `FileManager` - Lets you use tracked temporary files. Even if the 'user' tests/application start crashing, the framework will make sure these are cleaned up.
|
||||
|
||||
## Core Interface
|
||||
Any functionality your plugin wants to expose to code which uses the framework will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
|
||||
Any functionality your plugin wants to expose to 'users' will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
|
||||
```C#
|
||||
public static class CoreInterfaceExtensions
|
||||
{
|
||||
@@ -48,12 +79,14 @@ Any functionality your plugin wants to expose to code which uses the framework w
|
||||
}
|
||||
```
|
||||
|
||||
If your plugin wants to access the functionality exposed by other plugins, then you can pass the argument `CoreInterface ci` to your plugin code in order to do so. (For example, if you want to start a Geth node, the Geth plugin adds `IGethNode StartGethNode(this CoreInterface ci, Action<IGethSetup> setup)` to the core interface.) Don't forget you'll need to add a project reference to each plugin project you wish to use.
|
||||
|
||||
While technically you can build whatever you like on top of the `CoreInterface` and your own plugin types, I recommend that you follow the approach explained below.
|
||||
|
||||
## Deploying, Wrapping, and Starting
|
||||
When building a plugin, it is important to make as few assumptions as possible about how it will be used by whoever is going to use the framework. For this reason, I recommend you expose three kinds of methods using your `CoreInterface` extensions:
|
||||
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning containers as a result. If your project requires additional information, you can create a new class type to contain both it and the containers created.
|
||||
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain.
|
||||
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning container objects as a result. If your project requires additional information, you can create a new class type to contain both it and the container objects created.
|
||||
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain. (For example, if my project has a REST API call that allows users to fetch some state information, the object returned by Wrap should have a convenient method to call that API and receive that state information.)
|
||||
1. Start - This kind of method does both, simply calling a Deploy method first, then a Wrap method, and returns the result.
|
||||
|
||||
Here's an example:
|
||||
@@ -69,8 +102,8 @@ public static class CoreInterfaceExtensions
|
||||
|
||||
public static IMyProjectNode WrapMyProjectContainer(this CoreInterface ci, RunningContainers container)
|
||||
{
|
||||
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` tool to create an HTTP client for the container, then wrap it in an object that
|
||||
// represents the API of your project.
|
||||
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` to create an HTTP client for the container, then wrap it in an object that
|
||||
// represents the API of your project, in this case 'IMyProjectNode'.
|
||||
}
|
||||
|
||||
public static IMyProjectNode StartMyProject(this CoreInterface ci, string someArgument)
|
||||
@@ -82,5 +115,44 @@ public static class CoreInterfaceExtensions
|
||||
}
|
||||
```
|
||||
|
||||
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
|
||||
Should your deploy methods not return framework-types like RunningContainers, please make sure that your custom times are serializable. (Decorate them with the `[Serializable]` attribute.) Tools have been built using this framework which rely on the ability to serialize and store deployment information for later use. Please don't break this possibility. (Consider using the `SerializeGate` type to help ensure compatibility.)
|
||||
|
||||
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinuousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
|
||||
|
||||
## Container Recipes
|
||||
In order to run a container of your application, the framework needs to know how to create that container. Think of a container recipe as being similar to a docker-compose.yaml file: You specify the docker image, ports, environment variables, persistent volumes, and secrets. However, container recipes are code. This allows you to add conditional behaviour to how your container is constructed. For example: The 'user' of your plugin specifies in their call input that they want to run your application in a certain mode. This would cause your container recipe to set certain environment variables, which cause the application to behave in the requested way.
|
||||
|
||||
### Addresses and ports
|
||||
In a docker-compose.yaml file, it is perfectly normal to specify which ports should be exposed on your container. However, in the framework there's more to consider. When your application container starts, who knows on what kind of machine it runs, and what other processes it's sharing space with? Well, Kubernetes knows. Therefore, it is recommended that container recipes *do not* specify exact port numbers. The framework allows container recipes to declare "a port" without specifying its port number. This allows the framework and Kubernetes to figure out which ports are available when it's time to deploy. In order to find out which port numbers were assigned post-deployment, you can look up the port by tag (which is just an identifying string). When you specify a port to be mapped in your container recipe, you must specify:
|
||||
1. `Tag` - An identifier.
|
||||
1. `Internal` or `External` - Whether this port should be accessible only inside the cluster (for other containers (k8s: "ClusterIP")) or outside the cluster as well (for external tools/applications (k8s: "NodePort")).
|
||||
1. `Protocol` - TCP or UDP. Both protocols on the same port is not universally supported by all container engines, and is therefore not supported by the framework.
|
||||
|
||||
If your application wants to listen for incoming traffic from inside its container, be sure to bind it to address "0.0.0.0".
|
||||
|
||||
Reminder: If you don't want to worry about addresses, and internal or external ports, you don't have to! The container objects returned by the `workflow` plugin tool have a method called `GetAddress`. Given a port tag, it returns and address object. The `Http` plugin tool can use that address object to set up connections.
|
||||
|
||||
## Locations
|
||||
The framework is designed to allow you to control instances of your application in multiple (physical) locations. It accomplishes this by using kubernetes, and the ability to deploy containers to specific hosts (nodes) inside a kubernetes cluster. Since Kubernetes allows you to build clusters cross-site, this framework in theory enables you to deploy and interact with containers running anywhere.
|
||||
|
||||
The `workflow` plugin tool provides you a list of all available locations in the cluster. When starting a container, you are able to pick one of those locations. If no location is selected, one will be chosen by kubernetes. Locations can be chosen explicitly by kubernetes node name, or, they can be picked from the array of available locations.
|
||||
|
||||
Example:
|
||||
```C#
|
||||
{
|
||||
var location = Ci.GetKnownLocations().Get("kbnode_euwest_paris1");
|
||||
var codex = Ci.StartCodexNode(s => s.At(location));
|
||||
}
|
||||
```
|
||||
In this example, 'Ci' is an instance of the core interface. The CodexPlugin exposes a function 'StartCodexNode', which allows its user to specify a location. This location is then passed to the `workflow` tool when the Codex plugin starts its container.
|
||||
|
||||
The available locations array guarantees that each entry corresponds to a different kubernetes host.
|
||||
```C#
|
||||
{
|
||||
var knownLocations = Ci.GetKnownLocations();
|
||||
// I don't care where exactly, as long as they are different locations.
|
||||
var codexAtZero = Ci.StartCodexNode(s => s.At(knownLocations.Get(0)));
|
||||
var codexAtOne = Ci.StartCodexNode(s => s.At(knownLocations.Get(1)));
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Do you want to write some tests using this distributed test setup? Great! Here's
|
||||
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 `CodexDistTest` instead.
|
||||
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.
|
||||
### Continuous tests
|
||||
1. Add new code files to `Tests/CodexContinousTests/Tests`
|
||||
1. Add new code files to `Tests/CodexContinuousTests/Tests`
|
||||
1. Inherrit from `ContinuousTest`
|
||||
1. Define one or more methods and decorate them with the `[TestMoment(...)]` attribute.
|
||||
1. The TestMoment takes a number of seconds as argument. Each moment will be executed by the continuous test runner applying the given seconds as delay. (Non-cumulative. So two moments at T:10 will be executed one after another without delay, in this case the order of execution should not be depended upon.)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
|
||||
@@ -4,7 +4,8 @@ namespace Core
|
||||
{
|
||||
public interface IDownloadedLog
|
||||
{
|
||||
bool DoesLogContain(string expectedString);
|
||||
void IterateLines(Action<string> action);
|
||||
string[] GetLinesContaining(string expectedString);
|
||||
string[] FindLinesThatContain(params string[] tags);
|
||||
void DeleteFile();
|
||||
}
|
||||
@@ -18,7 +19,7 @@ namespace Core
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
public bool DoesLogContain(string expectedString)
|
||||
public void IterateLines(Action<string> action)
|
||||
{
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
@@ -26,12 +27,28 @@ namespace Core
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (line.Contains(expectedString)) return true;
|
||||
action(line);
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
public string[] GetLinesContaining(string expectedString)
|
||||
{
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
var lines = new List<string>();
|
||||
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (line.Contains(expectedString))
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
|
||||
//Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}");
|
||||
return false;
|
||||
return lines.ToArray(); ;
|
||||
}
|
||||
|
||||
public string[] FindLinesThatContain(params string[] tags)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Serialization = Newtonsoft.Json.Serialization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Utils;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
public interface IEndpoint
|
||||
{
|
||||
string HttpGetString(string route);
|
||||
T HttpGetJson<T>(string route);
|
||||
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
|
||||
string HttpPostJson<TRequest>(string route, TRequest body);
|
||||
TResponse HttpPostString<TResponse>(string route, string body);
|
||||
string HttpPostStream(string route, Stream stream);
|
||||
Stream HttpGetStream(string route);
|
||||
T Deserialize<T>(string json);
|
||||
}
|
||||
|
||||
internal class Endpoint : IEndpoint
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly IHttp http;
|
||||
private readonly Address address;
|
||||
private readonly string baseUrl;
|
||||
private readonly string? logAlias;
|
||||
|
||||
public Endpoint(ILog log, IHttp http, Address address, string baseUrl, string? logAlias)
|
||||
{
|
||||
this.log = log;
|
||||
this.http = http;
|
||||
this.address = address;
|
||||
this.baseUrl = baseUrl;
|
||||
this.logAlias = logAlias;
|
||||
}
|
||||
|
||||
public string HttpGetString(string route)
|
||||
{
|
||||
return http.OnClient(client =>
|
||||
{
|
||||
return GetString(client, route);
|
||||
}, $"HTTP-GET:{route}");
|
||||
}
|
||||
|
||||
public T HttpGetJson<T>(string route)
|
||||
{
|
||||
return http.OnClient(client =>
|
||||
{
|
||||
var json = GetString(client, route);
|
||||
return Deserialize<T>(json);
|
||||
}, $"HTTP-GET:{route}");
|
||||
}
|
||||
|
||||
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
|
||||
{
|
||||
return http.OnClient(client =>
|
||||
{
|
||||
var response = PostJson(client, route, body);
|
||||
var json = Time.Wait(response.Content.ReadAsStringAsync());
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
throw new HttpRequestException(json);
|
||||
}
|
||||
Log(GetUrl() + route, json);
|
||||
return Deserialize<TResponse>(json);
|
||||
}, $"HTTP-POST-JSON: {route}");
|
||||
}
|
||||
|
||||
public string HttpPostJson<TRequest>(string route, TRequest body)
|
||||
{
|
||||
return http.OnClient(client =>
|
||||
{
|
||||
var response = PostJson(client, route, body);
|
||||
return Time.Wait(response.Content.ReadAsStringAsync());
|
||||
}, $"HTTP-POST-JSON: {route}");
|
||||
}
|
||||
|
||||
public TResponse HttpPostString<TResponse>(string route, string body)
|
||||
{
|
||||
return http.OnClient(client =>
|
||||
{
|
||||
var response = PostJsonString(client, route, body);
|
||||
if (response == null) throw new Exception("Received no response.");
|
||||
var result = Deserialize<TResponse>(response);
|
||||
if (result == null) throw new Exception("Failed to deserialize response");
|
||||
return result;
|
||||
}, $"HTTO-POST-JSON: {route}");
|
||||
}
|
||||
|
||||
public string HttpPostStream(string route, Stream stream)
|
||||
{
|
||||
return http.OnClient(client =>
|
||||
{
|
||||
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 http.OnClient(client =>
|
||||
{
|
||||
var url = GetUrl() + route;
|
||||
Log(url, "~ STREAM ~");
|
||||
return Time.Wait(client.GetStreamAsync(url));
|
||||
}, $"HTTP-GET-STREAM: {route}");
|
||||
}
|
||||
|
||||
public T Deserialize<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 string GetString(HttpClient client, string route)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private HttpResponseMessage PostJson<TRequest>(HttpClient client, string route, TRequest body)
|
||||
{
|
||||
var url = GetUrl() + route;
|
||||
using var content = JsonContent.Create(body);
|
||||
Log(url, JsonConvert.SerializeObject(body));
|
||||
return Time.Wait(client.PostAsync(url, content));
|
||||
}
|
||||
|
||||
private string PostJsonString(HttpClient client, string route, string body)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-165
@@ -1,202 +1,65 @@
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Serialization = Newtonsoft.Json.Serialization;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Utils;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
public interface IHttp
|
||||
{
|
||||
string HttpGetString(string route);
|
||||
T HttpGetJson<T>(string route);
|
||||
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
|
||||
string HttpPostJson<TRequest>(string route, TRequest body);
|
||||
string HttpPostString(string route, string body);
|
||||
TResponse HttpPostString<TResponse>(string route, string body);
|
||||
string HttpPostStream(string route, Stream stream);
|
||||
Stream HttpGetStream(string route);
|
||||
T TryJsonDeserialize<T>(string json);
|
||||
T OnClient<T>(Func<HttpClient, T> action);
|
||||
T OnClient<T>(Func<HttpClient, T> action, string description);
|
||||
IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null);
|
||||
}
|
||||
|
||||
internal class Http : IHttp
|
||||
{
|
||||
private static readonly object httpLock = new object();
|
||||
private readonly ILog log;
|
||||
private readonly ITimeSet timeSet;
|
||||
private readonly Address address;
|
||||
private readonly string baseUrl;
|
||||
private readonly Action<HttpClient> onClientCreated;
|
||||
private readonly string? logAlias;
|
||||
|
||||
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, string? logAlias = null)
|
||||
: this(log, timeSet, address, baseUrl, DoNothing, logAlias)
|
||||
internal Http(ILog log, ITimeSet timeSet)
|
||||
: this(log, timeSet, DoNothing)
|
||||
{
|
||||
}
|
||||
|
||||
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
|
||||
internal Http(ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
|
||||
{
|
||||
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)
|
||||
public T OnClient<T>(Func<HttpClient, T> action)
|
||||
{
|
||||
return Retry(() =>
|
||||
return OnClient(action, GetDescription());
|
||||
}
|
||||
|
||||
public T OnClient<T>(Func<HttpClient, T> action, string description)
|
||||
{
|
||||
var client = GetClient();
|
||||
|
||||
return LockRetry(() =>
|
||||
{
|
||||
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}");
|
||||
return action(client);
|
||||
}, description);
|
||||
}
|
||||
|
||||
public T HttpGetJson<T>(string route)
|
||||
public IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null)
|
||||
{
|
||||
var json = HttpGetString(route);
|
||||
return TryJsonDeserialize<T>(json);
|
||||
return new Endpoint(log, this, address, baseUrl, logAlias);
|
||||
}
|
||||
|
||||
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
|
||||
private string GetDescription()
|
||||
{
|
||||
var response = PostJson(route, body);
|
||||
var json = Time.Wait(response.Content.ReadAsStringAsync());
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return DebugStack.GetCallerName(skipFrames: 2);
|
||||
}
|
||||
|
||||
private T LockRetry<T>(Func<T> operation, string description)
|
||||
{
|
||||
lock (httpLock)
|
||||
{
|
||||
throw new HttpRequestException(json);
|
||||
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
|
||||
}
|
||||
Log(GetUrl() + route, json);
|
||||
return TryJsonDeserialize<TResponse>(json);
|
||||
}
|
||||
|
||||
public string HttpPostJson<TRequest>(string route, TRequest body)
|
||||
{
|
||||
var response = PostJson(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 TResponse HttpPostString<TResponse>(string route, string body)
|
||||
{
|
||||
var response = HttpPostString(route, body);
|
||||
if (response == null) throw new Exception("Received no response.");
|
||||
var result = JsonConvert.DeserializeObject<TResponse>(response);
|
||||
if (result == null) throw new Exception("Failed to deserialize response");
|
||||
return result;
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using FileUtils;
|
||||
using KubernetesWorkflow;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
@@ -22,8 +21,9 @@ namespace Core
|
||||
|
||||
public interface IHttpFactoryTool
|
||||
{
|
||||
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null);
|
||||
IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null);
|
||||
IHttp CreateHttp(Action<HttpClient> onClientCreated);
|
||||
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
|
||||
IHttp CreateHttp();
|
||||
}
|
||||
|
||||
public interface IFileTool
|
||||
@@ -36,11 +36,11 @@ namespace Core
|
||||
private readonly ITimeSet timeSet;
|
||||
private readonly WorkflowCreator workflowCreator;
|
||||
private readonly IFileManager fileManager;
|
||||
private ILog log;
|
||||
private readonly LogPrefixer log;
|
||||
|
||||
internal PluginTools(ILog log, WorkflowCreator workflowCreator, string fileManagerRootFolder, ITimeSet timeSet)
|
||||
{
|
||||
this.log = log;
|
||||
this.log = new LogPrefixer(log);
|
||||
this.workflowCreator = workflowCreator;
|
||||
this.timeSet = timeSet;
|
||||
fileManager = new FileManager(log, fileManagerRootFolder);
|
||||
@@ -48,17 +48,22 @@ namespace Core
|
||||
|
||||
public void ApplyLogPrefix(string prefix)
|
||||
{
|
||||
log = new LogPrefixer(log, prefix);
|
||||
log.Prefix = prefix;
|
||||
}
|
||||
|
||||
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
|
||||
public IHttp CreateHttp(Action<HttpClient> onClientCreated)
|
||||
{
|
||||
return new Http(log, timeSet, address, baseUrl, onClientCreated, logAlias);
|
||||
return CreateHttp(onClientCreated, timeSet);
|
||||
}
|
||||
|
||||
public IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null)
|
||||
public IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet ts)
|
||||
{
|
||||
return new Http(log, timeSet, address, baseUrl, logAlias);
|
||||
return new Http(log, ts, onClientCreated);
|
||||
}
|
||||
|
||||
public IHttp CreateHttp()
|
||||
{
|
||||
return new Http(log, timeSet);
|
||||
}
|
||||
|
||||
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
|
||||
|
||||
@@ -5,11 +5,9 @@ namespace Core
|
||||
public static class SerializeGate
|
||||
{
|
||||
/// <summary>
|
||||
/// SerializeGate was added to help ensure deployment objects are serializable
|
||||
/// and remain viable after deserialization.
|
||||
/// SerializeGate was added to help ensure deployment objects are serializable and remain viable after deserialization.
|
||||
/// Tools can be built on top of the core interface that rely on deployment objects being serializable.
|
||||
/// Insert the serialization gate after deployment but before wrapping to ensure any future changes
|
||||
/// don't break this requirement.
|
||||
/// Insert the serialization gate after deployment but before wrapping to ensure any future changes don't break this requirement.
|
||||
/// </summary>
|
||||
public static T Gate<T>(T anything)
|
||||
{
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
public interface ITimeSet
|
||||
{
|
||||
TimeSpan HttpCallTimeout();
|
||||
TimeSpan HttpCallRetryTime();
|
||||
int HttpMaxNumberOfRetries();
|
||||
TimeSpan HttpCallRetryDelay();
|
||||
TimeSpan WaitForK8sServiceDelay();
|
||||
TimeSpan K8sOperationRetryDelay();
|
||||
TimeSpan K8sOperationTimeout();
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
{
|
||||
public TimeSpan HttpCallTimeout()
|
||||
{
|
||||
return TimeSpan.FromMinutes(5);
|
||||
return TimeSpan.FromMinutes(3);
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryTime()
|
||||
public int HttpMaxNumberOfRetries()
|
||||
{
|
||||
return TimeSpan.FromMinutes(1);
|
||||
return 3;
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryDelay()
|
||||
@@ -26,7 +26,7 @@
|
||||
return TimeSpan.FromSeconds(1);
|
||||
}
|
||||
|
||||
public TimeSpan WaitForK8sServiceDelay()
|
||||
public TimeSpan K8sOperationRetryDelay()
|
||||
{
|
||||
return TimeSpan.FromSeconds(10);
|
||||
}
|
||||
@@ -36,4 +36,32 @@
|
||||
return TimeSpan.FromMinutes(30);
|
||||
}
|
||||
}
|
||||
|
||||
public class LongTimeSet : ITimeSet
|
||||
{
|
||||
public TimeSpan HttpCallTimeout()
|
||||
{
|
||||
return TimeSpan.FromHours(2);
|
||||
}
|
||||
|
||||
public int HttpMaxNumberOfRetries()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public TimeSpan HttpCallRetryDelay()
|
||||
{
|
||||
return TimeSpan.FromSeconds(2);
|
||||
}
|
||||
|
||||
public TimeSpan K8sOperationRetryDelay()
|
||||
{
|
||||
return TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
public TimeSpan K8sOperationTimeout()
|
||||
{
|
||||
return TimeSpan.FromHours(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Utils;
|
||||
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class CheckConfig
|
||||
{
|
||||
public CheckType Type { get; set; }
|
||||
public ulong MinNumberOfHosts { get; set; }
|
||||
public ByteSize MinSlotSize { get; set; } = 0.Bytes();
|
||||
public TimeSpan MinDuration { get; set; } = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
public enum CheckType
|
||||
{
|
||||
Uninitialized,
|
||||
FilledSlot,
|
||||
FinishedSlot,
|
||||
PostedContract,
|
||||
StartedContract,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class GiveRewardsCommand
|
||||
{
|
||||
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
|
||||
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
|
||||
public string[] EventsOverview { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public class RewardUsersCommand
|
||||
{
|
||||
public ulong RewardId { get; set; }
|
||||
public string[] UserAddresses { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public class MarketAverage
|
||||
{
|
||||
public int NumberOfFinished { get; set; }
|
||||
public int TimeRangeSeconds { get; set; }
|
||||
public float Price { get; set; }
|
||||
public float Size { get; set; }
|
||||
public float Duration { get; set; }
|
||||
public float Collateral { get; set; }
|
||||
public float ProofProbability { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class RewardConfig
|
||||
{
|
||||
public const string UsernameTag = "<USER>";
|
||||
|
||||
public RewardConfig(ulong roleId, string message, CheckConfig checkConfig)
|
||||
{
|
||||
RoleId = roleId;
|
||||
Message = message;
|
||||
CheckConfig = checkConfig;
|
||||
}
|
||||
|
||||
public ulong RoleId { get; }
|
||||
public string Message { get; }
|
||||
public CheckConfig CheckConfig { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Utils;
|
||||
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class RewardRepo
|
||||
{
|
||||
private static string Tag => RewardConfig.UsernameTag;
|
||||
|
||||
public RewardConfig[] Rewards { get; } = new RewardConfig[]
|
||||
{
|
||||
// Filled any slot
|
||||
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FilledSlot
|
||||
}),
|
||||
|
||||
// Finished any slot
|
||||
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot
|
||||
}),
|
||||
|
||||
// Finished a sizable slot
|
||||
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinSlotSize = 10.MB(),
|
||||
MinDuration = TimeSpan.FromMinutes(5.0),
|
||||
}),
|
||||
|
||||
// Posted any contract
|
||||
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.PostedContract
|
||||
}),
|
||||
|
||||
// Started any contract
|
||||
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.StartedContract
|
||||
}),
|
||||
|
||||
// Started a sizable contract
|
||||
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
|
||||
{
|
||||
Type = CheckType.StartedContract,
|
||||
MinNumberOfHosts = 4,
|
||||
MinSlotSize = 10.MB(),
|
||||
MinDuration = TimeSpan.FromMinutes(5.0),
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ namespace FileUtils
|
||||
{
|
||||
var sw = Stopwatch.Begin(log);
|
||||
var result = GenerateRandomFile(size, label);
|
||||
sw.End($"Generated file '{result.Describe()}'.");
|
||||
sw.End($"Generated file {result.Describe()}.");
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,11 @@ namespace FileUtils
|
||||
return $"'{Filename}'{sizePostfix}";
|
||||
}
|
||||
|
||||
public ByteSize GetFilesize()
|
||||
{
|
||||
return new ByteSize(GetFileSize());
|
||||
}
|
||||
|
||||
private void AssertEqual(TrackedFile? actual)
|
||||
{
|
||||
if (actual == null) FrameworkAssert.Fail("TestFile is null.");
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using CodexContractsPlugin;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
|
||||
namespace GethConnector
|
||||
{
|
||||
public class GethConnector
|
||||
{
|
||||
public IGethNode GethNode { get; }
|
||||
public ICodexContracts CodexContracts { get; }
|
||||
|
||||
public static GethConnector? Initialize(ILog log)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(GethInput.LoadError))
|
||||
{
|
||||
var msg = "Geth input incorrect: " + GethInput.LoadError;
|
||||
log.Error(msg);
|
||||
return null;
|
||||
}
|
||||
|
||||
var contractsDeployment = new CodexContractsDeployment(
|
||||
marketplaceAddress: GethInput.MarketplaceAddress,
|
||||
abi: GethInput.ABI,
|
||||
tokenAddress: GethInput.TokenAddress
|
||||
);
|
||||
|
||||
var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
|
||||
var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment);
|
||||
|
||||
return new GethConnector(gethNode, contracts);
|
||||
}
|
||||
|
||||
private GethConnector(IGethNode gethNode, ICodexContracts codexContracts)
|
||||
{
|
||||
GethNode = gethNode;
|
||||
CodexContracts = codexContracts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace GethConnector
|
||||
{
|
||||
public static class GethInput
|
||||
{
|
||||
private const string GethHostVar = "GETH_HOST";
|
||||
private const string GethPortVar = "GETH_HTTP_PORT";
|
||||
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
|
||||
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
|
||||
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
|
||||
private const string AbiVar = "CODEXCONTRACTS_ABI";
|
||||
|
||||
static GethInput()
|
||||
{
|
||||
var error = new List<string>();
|
||||
var gethHost = GetEnvVar(error, GethHostVar);
|
||||
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
|
||||
var privateKey = GetEnvVar(error, GethPrivKeyVar);
|
||||
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
|
||||
var tokenAddress = GetEnvVar(error, TokenAddressVar);
|
||||
var abi = GetEnvVar(error, AbiVar);
|
||||
|
||||
if (error.Any())
|
||||
{
|
||||
LoadError = string.Join(", ", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
GethHost = gethHost!;
|
||||
GethPort = gethPort;
|
||||
PrivateKey = privateKey!;
|
||||
MarketplaceAddress = marketplaceAddress!;
|
||||
TokenAddress = tokenAddress!;
|
||||
ABI = abi!;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GethHost { get; } = string.Empty;
|
||||
public static int GethPort { get; }
|
||||
public static string PrivateKey { get; } = string.Empty;
|
||||
public static string MarketplaceAddress { get; } = string.Empty;
|
||||
public static string TokenAddress { get; } = string.Empty;
|
||||
public static string ABI { get; } = string.Empty;
|
||||
public static string LoadError { get; } = string.Empty;
|
||||
|
||||
private static string? GetEnvVar(List<string> error, string name)
|
||||
{
|
||||
var result = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,17 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
private readonly K8sClient client;
|
||||
private readonly string k8sNamespace;
|
||||
private readonly RunningPod pod;
|
||||
private readonly string podName;
|
||||
private readonly string containerName;
|
||||
private readonly string command;
|
||||
private readonly string[] arguments;
|
||||
private readonly List<string> lines = new List<string>();
|
||||
|
||||
public CommandRunner(K8sClient client, string k8sNamespace, RunningPod pod, string containerName, string command, string[] arguments)
|
||||
public CommandRunner(K8sClient client, string k8sNamespace, string podName, string containerName, string command, string[] arguments)
|
||||
{
|
||||
this.client = client;
|
||||
this.k8sNamespace = k8sNamespace;
|
||||
this.pod = pod;
|
||||
this.podName = podName;
|
||||
this.containerName = containerName;
|
||||
this.command = command;
|
||||
this.arguments = arguments;
|
||||
@@ -27,7 +27,7 @@ namespace KubernetesWorkflow
|
||||
var input = new[] { command }.Concat(arguments).ToArray();
|
||||
|
||||
Time.Wait(client.Run(c => c.NamespacedPodExecAsync(
|
||||
pod.PodInfo.Name, k8sNamespace, containerName, input, false, Callback, new CancellationToken())));
|
||||
podName, k8sNamespace, containerName, input, false, Callback, new CancellationToken())));
|
||||
}
|
||||
|
||||
public string GetStdOut()
|
||||
|
||||
@@ -7,19 +7,23 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly KubernetesClientConfiguration config;
|
||||
private readonly string containerName;
|
||||
private readonly string podName;
|
||||
private readonly string recipeName;
|
||||
private readonly string k8sNamespace;
|
||||
private readonly RunningContainer container;
|
||||
private ILogHandler? logHandler;
|
||||
private CancellationTokenSource cts;
|
||||
private Task? worker;
|
||||
private Exception? workerException;
|
||||
|
||||
public CrashWatcher(ILog log, KubernetesClientConfiguration config, string k8sNamespace, RunningContainer container)
|
||||
public CrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace)
|
||||
{
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.containerName = containerName;
|
||||
this.podName = podName;
|
||||
this.recipeName = recipeName;
|
||||
this.k8sNamespace = k8sNamespace;
|
||||
this.container = container;
|
||||
cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
@@ -46,7 +50,7 @@ namespace KubernetesWorkflow
|
||||
public bool HasContainerCrashed()
|
||||
{
|
||||
using var client = new Kubernetes(config);
|
||||
return HasContainerBeenRestarted(client, container.Pod.PodInfo.Name);
|
||||
return HasContainerBeenRestarted(client);
|
||||
}
|
||||
|
||||
private void Worker()
|
||||
@@ -66,29 +70,26 @@ namespace KubernetesWorkflow
|
||||
using var client = new Kubernetes(config);
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1));
|
||||
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
|
||||
|
||||
var pod = container.Pod;
|
||||
var recipe = container.Recipe;
|
||||
var podName = pod.PodInfo.Name;
|
||||
if (HasContainerBeenRestarted(client, podName))
|
||||
if (HasContainerBeenRestarted(client))
|
||||
{
|
||||
DownloadCrashedContainerLogs(client, podName, recipe);
|
||||
DownloadCrashedContainerLogs(client);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasContainerBeenRestarted(Kubernetes client, string podName)
|
||||
private bool HasContainerBeenRestarted(Kubernetes client)
|
||||
{
|
||||
var podInfo = client.ReadNamespacedPod(podName, k8sNamespace);
|
||||
return podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
|
||||
}
|
||||
|
||||
private void DownloadCrashedContainerLogs(Kubernetes client, string podName, ContainerRecipe recipe)
|
||||
private void DownloadCrashedContainerLogs(Kubernetes client)
|
||||
{
|
||||
log.Log("Pod crash detected for " + container.Name);
|
||||
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipe.Name, previous: true);
|
||||
log.Log("Pod crash detected for " + containerName);
|
||||
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true);
|
||||
logHandler!.Log(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,16 +54,4 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class K8sNodeLabel
|
||||
{
|
||||
public K8sNodeLabel(string key, string value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
public string Value { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using k8s;
|
||||
using k8s.Models;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
@@ -9,15 +11,14 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly K8sCluster cluster;
|
||||
private readonly KnownK8sPods knownPods;
|
||||
private readonly WorkflowNumberSource workflowNumberSource;
|
||||
private readonly K8sClient client;
|
||||
public const string PodLabelKey = "pod-uuid";
|
||||
|
||||
public K8sController(ILog log, K8sCluster cluster, KnownK8sPods knownPods, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
|
||||
public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
|
||||
{
|
||||
this.log = log;
|
||||
this.cluster = cluster;
|
||||
this.knownPods = knownPods;
|
||||
this.workflowNumberSource = workflowNumberSource;
|
||||
client = new K8sClient(cluster.GetK8sClientConfig());
|
||||
|
||||
@@ -29,40 +30,59 @@ namespace KubernetesWorkflow
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
public RunningPod BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
|
||||
public StartResult BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
|
||||
{
|
||||
log.Debug();
|
||||
EnsureNamespace();
|
||||
|
||||
var deploymentName = CreateDeployment(containerRecipes, location);
|
||||
var (serviceName, servicePortsMap) = CreateService(containerRecipes);
|
||||
var podInfo = FetchNewPod();
|
||||
var podLabel = K8sNameUtils.Format(Guid.NewGuid().ToString());
|
||||
var deployment = CreateDeployment(containerRecipes, location, podLabel);
|
||||
var internalService = CreateInternalService(containerRecipes);
|
||||
var externalService = CreateExternalService(containerRecipes);
|
||||
|
||||
return new RunningPod(cluster, podInfo, deploymentName, serviceName, servicePortsMap.ToArray());
|
||||
return new StartResult(cluster, containerRecipes, deployment, internalService, externalService);
|
||||
}
|
||||
|
||||
public void Stop(RunningPod pod)
|
||||
public void WaitUntilOnline(RunningContainer container)
|
||||
{
|
||||
log.Debug();
|
||||
if (!string.IsNullOrEmpty(pod.ServiceName)) DeleteService(pod.ServiceName);
|
||||
DeleteDeployment(pod.DeploymentName);
|
||||
WaitUntilDeploymentOffline(pod.DeploymentName);
|
||||
WaitUntilPodOffline(pod.PodInfo.Name);
|
||||
WaitUntilDeploymentOnline(container.Recipe.Name);
|
||||
}
|
||||
|
||||
public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler, int? tailLines)
|
||||
public PodInfo GetPodInfo(RunningDeployment deployment)
|
||||
{
|
||||
var pod = GetPodForDeployment(deployment);
|
||||
return CreatePodInfo(pod);
|
||||
}
|
||||
|
||||
public void Stop(StartResult startResult, bool waitTillStopped)
|
||||
{
|
||||
log.Debug();
|
||||
using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.PodInfo.Name, K8sNamespace, recipe.Name, tailLines: tailLines));
|
||||
if (startResult.InternalService != null) DeleteService(startResult.InternalService);
|
||||
if (startResult.ExternalService != null) DeleteService(startResult.ExternalService);
|
||||
DeleteDeployment(startResult.Deployment);
|
||||
|
||||
if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
|
||||
}
|
||||
|
||||
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines)
|
||||
{
|
||||
log.Debug();
|
||||
|
||||
var podName = GetPodName(container);
|
||||
var recipeName = container.Recipe.Name;
|
||||
|
||||
using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines));
|
||||
logHandler.Log(stream);
|
||||
}
|
||||
|
||||
public string ExecuteCommand(RunningPod pod, string containerName, string command, params string[] args)
|
||||
public string ExecuteCommand(RunningContainer container, string command, params string[] args)
|
||||
{
|
||||
var containerName = container.Recipe.Name;
|
||||
var cmdAndArgs = $"{containerName}: {command} ({string.Join(",", args)})";
|
||||
log.Debug(cmdAndArgs);
|
||||
|
||||
var runner = new CommandRunner(client, K8sNamespace, pod, containerName, command, args);
|
||||
var podName = GetPodName(container);
|
||||
var runner = new CommandRunner(client, K8sNamespace, podName, containerName, command, args);
|
||||
runner.Run();
|
||||
var result = runner.GetStdOut();
|
||||
|
||||
@@ -70,6 +90,31 @@ namespace KubernetesWorkflow
|
||||
return result;
|
||||
}
|
||||
|
||||
public int[] GetUsedExternalPorts()
|
||||
{
|
||||
return client.Run(c =>
|
||||
{
|
||||
var result = new List<int>();
|
||||
|
||||
var services = c.ListServiceForAllNamespaces();
|
||||
var nodePorts = services.Items.Where(s => s.Spec.Type == "NodePort").ToArray();
|
||||
if (!nodePorts.Any()) return result.ToArray();
|
||||
|
||||
foreach (var service in nodePorts)
|
||||
{
|
||||
foreach (var port in service.Spec.Ports)
|
||||
{
|
||||
if (port.NodePort.HasValue)
|
||||
{
|
||||
result.Add(port.NodePort.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
});
|
||||
}
|
||||
|
||||
public void DeleteAllNamespacesStartingWith(string prefix)
|
||||
{
|
||||
log.Debug();
|
||||
@@ -81,10 +126,6 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
DeleteNamespace(ns);
|
||||
}
|
||||
foreach (var ns in namespaces)
|
||||
{
|
||||
WaitUntilNamespaceDeleted(ns);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteNamespace()
|
||||
@@ -94,7 +135,6 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0));
|
||||
}
|
||||
WaitUntilNamespaceDeleted();
|
||||
}
|
||||
|
||||
public void DeleteNamespace(string ns)
|
||||
@@ -304,7 +344,7 @@ namespace KubernetesWorkflow
|
||||
|
||||
#region Deployment management
|
||||
|
||||
private string CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location)
|
||||
private RunningDeployment CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location, string podLabel)
|
||||
{
|
||||
var deploymentSpec = new V1Deployment
|
||||
{
|
||||
@@ -321,11 +361,13 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
Metadata = new V1ObjectMeta
|
||||
{
|
||||
Labels = GetSelector(containerRecipes),
|
||||
Labels = GetSelector(containerRecipes, podLabel),
|
||||
Annotations = GetAnnotations(containerRecipes)
|
||||
},
|
||||
Spec = new V1PodSpec
|
||||
{
|
||||
PriorityClassName = GetPriorityClassName(containerRecipes),
|
||||
Affinity = CreatePodAffinity(containerRecipes),
|
||||
NodeSelector = CreateNodeSelector(location),
|
||||
Containers = CreateDeploymentContainers(containerRecipes),
|
||||
Volumes = CreateVolumes(containerRecipes)
|
||||
@@ -335,15 +377,15 @@ namespace KubernetesWorkflow
|
||||
};
|
||||
|
||||
client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sNamespace));
|
||||
WaitUntilDeploymentOnline(deploymentSpec.Metadata.Name);
|
||||
|
||||
return deploymentSpec.Metadata.Name;
|
||||
var name = deploymentSpec.Metadata.Name;
|
||||
return new RunningDeployment(name, podLabel);
|
||||
}
|
||||
|
||||
private void DeleteDeployment(string deploymentName)
|
||||
private void DeleteDeployment(RunningDeployment deployment)
|
||||
{
|
||||
client.Run(c => c.DeleteNamespacedDeployment(deploymentName, K8sNamespace));
|
||||
WaitUntilDeploymentOffline(deploymentName);
|
||||
client.Run(c => c.DeleteNamespacedDeployment(deployment.Name, K8sNamespace));
|
||||
WaitUntilDeploymentOffline(deployment.Name);
|
||||
}
|
||||
|
||||
private IDictionary<string, string> CreateNodeSelector(ILocation location)
|
||||
@@ -357,17 +399,69 @@ namespace KubernetesWorkflow
|
||||
};
|
||||
}
|
||||
|
||||
private V1Affinity? CreatePodAffinity(ContainerRecipe[] recipes)
|
||||
{
|
||||
var notIns = recipes
|
||||
.Select(r => r.SchedulingAffinity.NotIn)
|
||||
.Where(n => !string.IsNullOrEmpty(n))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (!notIns.Any()) return null;
|
||||
|
||||
return new V1Affinity
|
||||
{
|
||||
NodeAffinity = new V1NodeAffinity
|
||||
{
|
||||
RequiredDuringSchedulingIgnoredDuringExecution = new V1NodeSelector
|
||||
{
|
||||
NodeSelectorTerms = new List<V1NodeSelectorTerm>
|
||||
{
|
||||
new V1NodeSelectorTerm
|
||||
{
|
||||
MatchExpressions = new List<V1NodeSelectorRequirement>
|
||||
{
|
||||
new V1NodeSelectorRequirement
|
||||
{
|
||||
Key = "allow-tests-pods",
|
||||
OperatorProperty = "NotIn",
|
||||
Values = notIns
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private K8sNodeLabel? GetNodeLabelForLocation(ILocation location)
|
||||
{
|
||||
var l = (Location)location;
|
||||
return l.NodeLabel;
|
||||
}
|
||||
|
||||
private string GetPriorityClassName(ContainerRecipe[] containerRecipes)
|
||||
{
|
||||
if (containerRecipes.Any(c => c.SetCriticalPriority))
|
||||
{
|
||||
return "system-node-critical";
|
||||
}
|
||||
return null!;
|
||||
}
|
||||
|
||||
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes)
|
||||
{
|
||||
return containerRecipes.First().PodLabels.GetLabels();
|
||||
}
|
||||
|
||||
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes, string podLabel)
|
||||
{
|
||||
var labels = containerRecipes.First().PodLabels.Clone();
|
||||
labels.Add(PodLabelKey, podLabel);
|
||||
return labels.GetLabels();
|
||||
}
|
||||
|
||||
private IDictionary<string, string> GetRunnerNamespaceSelector()
|
||||
{
|
||||
return new Dictionary<string, string> { { "kubernetes.io/metadata.name", "default" } };
|
||||
@@ -409,10 +503,17 @@ namespace KubernetesWorkflow
|
||||
Ports = CreateContainerPorts(recipe),
|
||||
Env = CreateEnv(recipe),
|
||||
VolumeMounts = CreateContainerVolumeMounts(recipe),
|
||||
Resources = CreateResourceLimits(recipe)
|
||||
Resources = CreateResourceLimits(recipe),
|
||||
Command = CreateCommandList(recipe)
|
||||
};
|
||||
}
|
||||
|
||||
private IList<string> CreateCommandList(ContainerRecipe recipe)
|
||||
{
|
||||
if (recipe.CommandOverride == null || !recipe.CommandOverride.Command.Any()) return null!;
|
||||
return recipe.CommandOverride.Command.ToList();
|
||||
}
|
||||
|
||||
private V1ResourceRequirements CreateResourceLimits(ContainerRecipe recipe)
|
||||
{
|
||||
return new V1ResourceRequirements
|
||||
@@ -446,7 +547,8 @@ namespace KubernetesWorkflow
|
||||
return new V1VolumeMount
|
||||
{
|
||||
Name = v.VolumeName,
|
||||
MountPath = v.MountPath
|
||||
MountPath = v.MountPath,
|
||||
SubPath = v.SubPath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -462,28 +564,28 @@ namespace KubernetesWorkflow
|
||||
|
||||
private V1Volume CreateVolume(VolumeMount v)
|
||||
{
|
||||
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
|
||||
CreatePersistentVolumeClaimIfNeeded(v);
|
||||
|
||||
if (!string.IsNullOrEmpty(v.HostPath))
|
||||
{
|
||||
ApiVersion = "v1",
|
||||
Metadata = new V1ObjectMeta
|
||||
return new V1Volume
|
||||
{
|
||||
Name = v.VolumeName
|
||||
},
|
||||
Spec = new V1PersistentVolumeClaimSpec
|
||||
{
|
||||
AccessModes = new List<string>
|
||||
Name = v.VolumeName,
|
||||
HostPath = new V1HostPathVolumeSource
|
||||
{
|
||||
"ReadWriteOnce"
|
||||
},
|
||||
Resources = new V1ResourceRequirements
|
||||
{
|
||||
Requests = new Dictionary<string, ResourceQuantity>
|
||||
{
|
||||
{"storage", new ResourceQuantity(v.ResourceQuantity) }
|
||||
}
|
||||
Path = v.HostPath
|
||||
}
|
||||
}
|
||||
}, K8sNamespace));
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(v.Secret))
|
||||
{
|
||||
return new V1Volume
|
||||
{
|
||||
Name = v.VolumeName,
|
||||
Secret = CreateVolumeSecret(v)
|
||||
};
|
||||
}
|
||||
|
||||
return new V1Volume
|
||||
{
|
||||
@@ -495,6 +597,50 @@ namespace KubernetesWorkflow
|
||||
};
|
||||
}
|
||||
|
||||
private void CreatePersistentVolumeClaimIfNeeded(VolumeMount v)
|
||||
{
|
||||
var pvcs = client.Run(c => c.ListNamespacedPersistentVolumeClaim(K8sNamespace));
|
||||
if (pvcs != null && pvcs.Items.Any(i => i.Name() == v.VolumeName)) return;
|
||||
|
||||
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
|
||||
{
|
||||
ApiVersion = "v1",
|
||||
Metadata = new V1ObjectMeta
|
||||
{
|
||||
Name = v.VolumeName,
|
||||
},
|
||||
Spec = new V1PersistentVolumeClaimSpec
|
||||
{
|
||||
AccessModes = new List<string>
|
||||
{
|
||||
"ReadWriteOnce"
|
||||
},
|
||||
Resources = CreateVolumeResourceRequirements(v),
|
||||
},
|
||||
}, K8sNamespace));
|
||||
}
|
||||
|
||||
private V1SecretVolumeSource CreateVolumeSecret(VolumeMount v)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(v.Secret)) return null!;
|
||||
return new V1SecretVolumeSource
|
||||
{
|
||||
SecretName = v.Secret
|
||||
};
|
||||
}
|
||||
|
||||
private V1ResourceRequirements CreateVolumeResourceRequirements(VolumeMount v)
|
||||
{
|
||||
if (v.ResourceQuantity == null) return null!;
|
||||
return new V1ResourceRequirements
|
||||
{
|
||||
Requests = new Dictionary<string, ResourceQuantity>()
|
||||
{
|
||||
{"storage", new ResourceQuantity(v.ResourceQuantity) }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<V1EnvVar> CreateEnv(ContainerRecipe recipe)
|
||||
{
|
||||
return recipe.EnvVars.Select(CreateEnvVar).ToList();
|
||||
@@ -511,149 +657,218 @@ namespace KubernetesWorkflow
|
||||
|
||||
private List<V1ContainerPort> CreateContainerPorts(ContainerRecipe recipe)
|
||||
{
|
||||
var exposedPorts = recipe.ExposedPorts.Select(p => CreateContainerPort(recipe, p));
|
||||
var internalPorts = recipe.InternalPorts.Select(p => CreateContainerPort(recipe, p));
|
||||
var exposedPorts = recipe.ExposedPorts.SelectMany(p => CreateContainerPort(recipe, p));
|
||||
var internalPorts = recipe.InternalPorts.SelectMany(p => CreateContainerPort(recipe, p));
|
||||
return exposedPorts.Concat(internalPorts).ToList();
|
||||
}
|
||||
|
||||
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port)
|
||||
private List<V1ContainerPort> CreateContainerPort(ContainerRecipe recipe, Port port)
|
||||
{
|
||||
var result = new List<V1ContainerPort>();
|
||||
if (port.IsTcp()) CreateTcpContainerPort(result, recipe, port);
|
||||
if (port.IsUdp()) CreateUdpContainerPort(result, recipe, port);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CreateUdpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
|
||||
{
|
||||
result.Add(CreateContainerPort(recipe, port, "UDP"));
|
||||
}
|
||||
|
||||
private void CreateTcpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
|
||||
{
|
||||
result.Add(CreateContainerPort(recipe, port, "TCP"));
|
||||
}
|
||||
|
||||
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port, string protocol)
|
||||
{
|
||||
return new V1ContainerPort
|
||||
{
|
||||
Name = GetNameForPort(recipe, port),
|
||||
ContainerPort = port.Number
|
||||
ContainerPort = port.Number,
|
||||
Protocol = protocol
|
||||
};
|
||||
}
|
||||
|
||||
private string GetNameForPort(ContainerRecipe recipe, Port port)
|
||||
{
|
||||
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}";
|
||||
var inputs = new[]
|
||||
{
|
||||
$"p{workflowNumberSource.WorkflowNumber}",
|
||||
recipe.Number.ToString(),
|
||||
port.Number.ToString(),
|
||||
port.Protocol.ToString().ToLowerInvariant()
|
||||
};
|
||||
|
||||
return K8sNameUtils.FormatPortName(string.Join(",", inputs));
|
||||
}
|
||||
|
||||
private string GetPodName(RunningContainer container)
|
||||
{
|
||||
return GetPodForDeployment(container.RunningPod.StartResult.Deployment).Metadata.Name;
|
||||
}
|
||||
|
||||
private V1Pod GetPodForDeployment(RunningDeployment deployment)
|
||||
{
|
||||
return Time.Retry(() => GetPodForDeplomentInternal(deployment),
|
||||
// We will wait up to 1 minute, k8s might be moving pods around.
|
||||
maxRetries: 6,
|
||||
retryTime: TimeSpan.FromSeconds(10),
|
||||
description: "Find pod by label for deployment.");
|
||||
}
|
||||
|
||||
private V1Pod GetPodForDeplomentInternal(RunningDeployment deployment)
|
||||
{
|
||||
var allPods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
|
||||
var pods = allPods.Items.Where(p => p.GetLabel(PodLabelKey) == deployment.PodLabel).ToArray();
|
||||
|
||||
if (pods.Length != 1)
|
||||
{
|
||||
var allLabels = allPods.Items.Select(p =>
|
||||
{
|
||||
var labels = string.Join(",", p.Labels().Select(l => $"{l.Key}={l.Value}"));
|
||||
return $"pod:'{p.Name()}' has labels: [{labels}]";
|
||||
});
|
||||
throw new Exception($"Expected to find 1 pod by podLabel '{deployment.PodLabel}'. Found: {pods.Length}. " +
|
||||
$"Total number of pods: {allPods.Items.Count}. Their labels: {string.Join(Environment.NewLine, allLabels)}");
|
||||
}
|
||||
return pods[0];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Service management
|
||||
|
||||
private (string, List<ContainerRecipePortMapEntry>) CreateService(ContainerRecipe[] containerRecipes)
|
||||
private RunningService? CreateInternalService(ContainerRecipe[] recipes)
|
||||
{
|
||||
var result = new List<ContainerRecipePortMapEntry>();
|
||||
return CreateService(recipes, r => r.InternalPorts.Concat(r.ExposedPorts).ToArray(), "ClusterIP", "int", false);
|
||||
}
|
||||
|
||||
var ports = CreateServicePorts(containerRecipes);
|
||||
private RunningService? CreateExternalService(ContainerRecipe[] recipes)
|
||||
{
|
||||
return CreateService(recipes, r => r.ExposedPorts, "NodePort", "ext", true);
|
||||
}
|
||||
|
||||
if (!ports.Any())
|
||||
{
|
||||
// None of these container-recipes wish to expose anything via a service port.
|
||||
// So, we don't have to create a service.
|
||||
return (string.Empty, result);
|
||||
}
|
||||
private RunningService? CreateService(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, string serviceType, string namePostfix, bool isNodePort)
|
||||
{
|
||||
var ports = CreateServicePorts(recipes, portSelector, isNodePort);
|
||||
if (!ports.Any()) return null;
|
||||
|
||||
var serviceSpec = new V1Service
|
||||
{
|
||||
ApiVersion = "v1",
|
||||
Metadata = CreateServiceMetadata(),
|
||||
Metadata = CreateServiceMetadata(recipes, namePostfix),
|
||||
Spec = new V1ServiceSpec
|
||||
{
|
||||
Type = "NodePort",
|
||||
Selector = GetSelector(containerRecipes),
|
||||
Ports = ports
|
||||
Type = serviceType,
|
||||
Selector = GetSelector(recipes),
|
||||
Ports = ports,
|
||||
}
|
||||
};
|
||||
|
||||
client.Run(c => c.CreateNamespacedService(serviceSpec, K8sNamespace));
|
||||
|
||||
ReadBackServiceAndMapPorts(serviceSpec, containerRecipes, result);
|
||||
var result = ReadBackServiceAndMapPorts(serviceSpec, recipes);
|
||||
var name = serviceSpec.Metadata.Name;
|
||||
|
||||
return (serviceSpec.Metadata.Name, result);
|
||||
return new RunningService(name, result);
|
||||
}
|
||||
|
||||
private void ReadBackServiceAndMapPorts(V1Service serviceSpec, ContainerRecipe[] containerRecipes, List<ContainerRecipePortMapEntry> result)
|
||||
private List<ContainerRecipePortMapEntry> ReadBackServiceAndMapPorts(V1Service serviceSpec, ContainerRecipe[] containerRecipes)
|
||||
{
|
||||
// For each container-recipe, we need to figure out which service-ports it was assigned by K8s.
|
||||
var result = new List<ContainerRecipePortMapEntry>();
|
||||
|
||||
// For each container-recipe-port, we need to figure out which service-ports it was assigned by K8s.
|
||||
var readback = client.Run(c => c.ReadNamespacedService(serviceSpec.Metadata.Name, K8sNamespace));
|
||||
foreach (var r in containerRecipes)
|
||||
{
|
||||
if (r.ExposedPorts.Any())
|
||||
var recipePorts = r.ExposedPorts.Concat(r.InternalPorts).ToArray();
|
||||
foreach (var port in recipePorts)
|
||||
{
|
||||
var firstExposedPort = r.ExposedPorts.First();
|
||||
var portName = GetNameForPort(r, firstExposedPort);
|
||||
var portName = GetNameForPort(r, port);
|
||||
|
||||
var matchingServicePorts = readback.Spec.Ports.Where(p => p.Name == portName);
|
||||
if (matchingServicePorts.Any())
|
||||
{
|
||||
// These service ports belongs to this recipe.
|
||||
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, portName));
|
||||
var ports = optionals.Where(p => p != null).Select(p => p!).ToArray();
|
||||
var ports = matchingServicePorts.Select(p => MapPortIfAble(p, port.Tag, port.Protocol)).ToArray();
|
||||
|
||||
if (ports.Any())
|
||||
{
|
||||
result.Add(new ContainerRecipePortMapEntry(r.Number, ports));
|
||||
log.Debug($"Service Readback: {portName} found: {string.Join(",", ports.Select(p => p.ToString()))}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Port? MapNodePortIfAble(V1ServicePort p, string tag)
|
||||
private Port MapPortIfAble(V1ServicePort p, string tag, PortProtocol protocol)
|
||||
{
|
||||
if (p.NodePort == null) return null;
|
||||
return new Port(p.NodePort.Value, tag);
|
||||
if (p.NodePort != null) return new Port(p.NodePort.Value, tag, protocol);
|
||||
if (p.Port > 0) return new Port(p.Port, tag, protocol);
|
||||
throw new Exception("Unable to map port.");
|
||||
}
|
||||
|
||||
private void DeleteService(string serviceName)
|
||||
private void DeleteService(RunningService service)
|
||||
{
|
||||
client.Run(c => c.DeleteNamespacedService(serviceName, K8sNamespace));
|
||||
client.Run(c => c.DeleteNamespacedService(service.Name, K8sNamespace));
|
||||
}
|
||||
|
||||
private V1ObjectMeta CreateServiceMetadata()
|
||||
private V1ObjectMeta CreateServiceMetadata(ContainerRecipe[] containerRecipes, string namePostfix)
|
||||
{
|
||||
var recipeName = containerRecipes.First().Name;
|
||||
var name = K8sNameUtils.Format($"{recipeName}-{workflowNumberSource.WorkflowNumber}-{namePostfix}");
|
||||
log.Debug("Creating service: " + name);
|
||||
return new V1ObjectMeta
|
||||
{
|
||||
Name = "service-" + workflowNumberSource.WorkflowNumber,
|
||||
NamespaceProperty = K8sNamespace
|
||||
Name = name,
|
||||
NamespaceProperty = K8sNamespace,
|
||||
};
|
||||
}
|
||||
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes)
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, bool isNodePort)
|
||||
{
|
||||
var result = new List<V1ServicePort>();
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
result.AddRange(CreateServicePorts(recipe));
|
||||
var ports = portSelector(recipe);
|
||||
foreach (var port in ports)
|
||||
{
|
||||
result.AddRange(CreateServicePorts(recipe, port, isNodePort));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe)
|
||||
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe, Port recipePort, bool isNodePort)
|
||||
{
|
||||
var result = new List<V1ServicePort>();
|
||||
foreach (var port in recipe.ExposedPorts)
|
||||
{
|
||||
result.Add(new V1ServicePort
|
||||
{
|
||||
Name = GetNameForPort(recipe, port),
|
||||
Protocol = "TCP",
|
||||
Port = port.Number,
|
||||
TargetPort = GetNameForPort(recipe, port),
|
||||
});
|
||||
}
|
||||
|
||||
if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP", isNodePort);
|
||||
if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP", isNodePort);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol, bool isNodePort)
|
||||
{
|
||||
var p = new V1ServicePort
|
||||
{
|
||||
Name = GetNameForPort(recipe, port),
|
||||
Protocol = protocol,
|
||||
Port = port.Number,
|
||||
TargetPort = GetNameForPort(recipe, port)
|
||||
};
|
||||
|
||||
if (isNodePort) p.NodePort = port.Number;
|
||||
|
||||
result.Add(p);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Waiting
|
||||
|
||||
private void WaitUntilNamespaceCreated()
|
||||
{
|
||||
WaitUntil(() => IsNamespaceOnline(K8sNamespace));
|
||||
}
|
||||
|
||||
private void WaitUntilNamespaceDeleted()
|
||||
{
|
||||
WaitUntil(() => !IsNamespaceOnline(K8sNamespace));
|
||||
}
|
||||
|
||||
private void WaitUntilNamespaceDeleted(string name)
|
||||
{
|
||||
WaitUntil(() => !IsNamespaceOnline(name));
|
||||
WaitUntil(() => IsNamespaceOnline(K8sNamespace), nameof(WaitUntilNamespaceCreated));
|
||||
}
|
||||
|
||||
private void WaitUntilDeploymentOnline(string deploymentName)
|
||||
@@ -662,7 +877,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace));
|
||||
return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0;
|
||||
});
|
||||
}, nameof(WaitUntilDeploymentOnline));
|
||||
}
|
||||
|
||||
private void WaitUntilDeploymentOffline(string deploymentName)
|
||||
@@ -672,29 +887,28 @@ namespace KubernetesWorkflow
|
||||
var deployments = client.Run(c => c.ListNamespacedDeployment(K8sNamespace));
|
||||
var deployment = deployments.Items.SingleOrDefault(d => d.Metadata.Name == deploymentName);
|
||||
return deployment == null || deployment.Status.AvailableReplicas == 0;
|
||||
});
|
||||
}, nameof(WaitUntilDeploymentOffline));
|
||||
}
|
||||
|
||||
private void WaitUntilPodOffline(string podName)
|
||||
private void WaitUntilPodsForDeploymentAreOffline(RunningDeployment deployment)
|
||||
{
|
||||
WaitUntil(() =>
|
||||
{
|
||||
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace)).Items;
|
||||
var pod = pods.SingleOrDefault(p => p.Metadata.Name == podName);
|
||||
return pod == null;
|
||||
});
|
||||
var pods = FindPodsByLabel(deployment.PodLabel);
|
||||
return !pods.Any();
|
||||
}, nameof(WaitUntilPodsForDeploymentAreOffline));
|
||||
}
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
private void WaitUntil(Func<bool> predicate, string msg)
|
||||
{
|
||||
var sw = Stopwatch.Begin(log, true);
|
||||
try
|
||||
{
|
||||
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay());
|
||||
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay(), msg);
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.End("", 1);
|
||||
sw.End(msg, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,25 +916,28 @@ namespace KubernetesWorkflow
|
||||
|
||||
public CrashWatcher CreateCrashWatcher(RunningContainer container)
|
||||
{
|
||||
return new CrashWatcher(log, cluster.GetK8sClientConfig(), K8sNamespace, container);
|
||||
var containerName = container.Name;
|
||||
var podName = GetPodName(container);
|
||||
var recipeName = container.Recipe.Name;
|
||||
|
||||
return new CrashWatcher(log, cluster.GetK8sClientConfig(), containerName, podName, recipeName, K8sNamespace);
|
||||
}
|
||||
|
||||
private PodInfo FetchNewPod()
|
||||
private V1Pod[] FindPodsByLabel(string podLabel)
|
||||
{
|
||||
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace)).Items;
|
||||
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
|
||||
return pods.Items.Where(p => p.GetLabel(PodLabelKey) == podLabel).ToArray();
|
||||
}
|
||||
|
||||
var newPods = pods.Where(p => !knownPods.Contains(p.Name())).ToArray();
|
||||
if (newPods.Length != 1) throw new InvalidOperationException("Expected only 1 pod to be created. Test infra failure.");
|
||||
|
||||
var newPod = newPods.Single();
|
||||
var name = newPod.Name();
|
||||
var ip = newPod.Status.PodIP;
|
||||
var k8sNodeName = newPod.Spec.NodeName;
|
||||
private PodInfo CreatePodInfo(V1Pod pod)
|
||||
{
|
||||
var name = pod.Name();
|
||||
var ip = pod.Status.PodIP;
|
||||
var k8sNodeName = pod.Spec.NodeName;
|
||||
|
||||
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure.");
|
||||
if (string.IsNullOrEmpty(ip)) throw new InvalidOperationException("Invalid pod IP received. Test infra failure.");
|
||||
|
||||
knownPods.Add(name);
|
||||
return new PodInfo(name, ip, k8sNodeName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
namespace KubernetesWorkflow
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public interface IK8sHooks
|
||||
{
|
||||
void OnContainersStarted(RunningContainers runningContainers);
|
||||
void OnContainersStopped(RunningContainers runningContainers);
|
||||
void OnContainersStarted(RunningPod runningPod);
|
||||
void OnContainersStopped(RunningPod runningPod);
|
||||
void OnContainerRecipeCreated(ContainerRecipe recipe);
|
||||
}
|
||||
|
||||
public class DoNothingK8sHooks : IK8sHooks
|
||||
{
|
||||
public void OnContainersStarted(RunningContainers runningContainers)
|
||||
public void OnContainersStarted(RunningPod runningPod)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnContainersStopped(RunningContainers runningContainers)
|
||||
public void OnContainersStopped(RunningPod runningPod)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,19 @@
|
||||
public static class K8sNameUtils
|
||||
{
|
||||
public static string Format(string s)
|
||||
{
|
||||
return Format(s, 62);
|
||||
}
|
||||
|
||||
public static string FormatPortName(string s)
|
||||
{
|
||||
return Format(s, 15);
|
||||
}
|
||||
|
||||
private static string Format(string s, int maxLength)
|
||||
{
|
||||
var result = s.ToLowerInvariant()
|
||||
.Replace("_", "-")
|
||||
.Replace(" ", "-")
|
||||
.Replace(":", "-")
|
||||
.Replace("/", "-")
|
||||
@@ -14,7 +25,7 @@
|
||||
.Replace(",", "-");
|
||||
|
||||
result = result.Trim('-');
|
||||
if (result.Length > 62) result = result.Substring(0, 62);
|
||||
if (result.Length > maxLength) result = result.Substring(0, maxLength);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public class KnownK8sPods
|
||||
{
|
||||
private readonly List<string> knownActivePodNames = new List<string>();
|
||||
|
||||
public bool Contains(string name)
|
||||
{
|
||||
return knownActivePodNames.Contains(name);
|
||||
}
|
||||
|
||||
public void Add(string name)
|
||||
{
|
||||
knownActivePodNames.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace KubernetesWorkflow
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public interface ILocation
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Logging;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public interface ILogHandler
|
||||
{
|
||||
void Log(Stream log);
|
||||
}
|
||||
|
||||
public abstract class LogHandler : ILogHandler
|
||||
{
|
||||
public void Log(Stream log)
|
||||
{
|
||||
using var reader = new StreamReader(log);
|
||||
var line = reader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
ProcessLine(line);
|
||||
line = reader.ReadLine();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ProcessLine(string line);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public static class PublicIpService
|
||||
{
|
||||
public static string Address { get; } = "ip.codex.storage";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class CommandOverride
|
||||
{
|
||||
public CommandOverride(params string[] command)
|
||||
{
|
||||
Command = command;
|
||||
}
|
||||
|
||||
public string[] Command { get; }
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class ContainerAdditionals
|
||||
{
|
||||
@@ -20,7 +20,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
var typeName = GetTypeName(typeof(T));
|
||||
var userData = Additionals.SingleOrDefault(a => a.Type == typeName);
|
||||
if (userData == null) return default(T);
|
||||
if (userData == null) return default;
|
||||
var jobject = (JObject)userData.UserData;
|
||||
return jobject.ToObject<T>();
|
||||
}
|
||||
+49
-5
@@ -1,13 +1,16 @@
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class ContainerRecipe
|
||||
{
|
||||
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
|
||||
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
|
||||
{
|
||||
Number = number;
|
||||
NameOverride = nameOverride;
|
||||
Image = image;
|
||||
Resources = resources;
|
||||
SchedulingAffinity = schedulingAffinity;
|
||||
CommandOverride = commandOverride;
|
||||
SetCriticalPriority = setCriticalPriority;
|
||||
ExposedPorts = exposedPorts;
|
||||
InternalPorts = internalPorts;
|
||||
EnvVars = envVars;
|
||||
@@ -24,12 +27,17 @@
|
||||
{
|
||||
Name = $"ctnr{Number}";
|
||||
}
|
||||
|
||||
if (exposedPorts.Any(p => string.IsNullOrEmpty(p.Tag))) throw new Exception("Port tags are required for all exposed ports.");
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public int Number { get; }
|
||||
public string? NameOverride { get; }
|
||||
public ContainerResources Resources { get; }
|
||||
public SchedulingAffinity SchedulingAffinity { get; }
|
||||
public CommandOverride CommandOverride { get; }
|
||||
public bool SetCriticalPriority { get; }
|
||||
public string Image { get; }
|
||||
public Port[] ExposedPorts { get; }
|
||||
public Port[] InternalPorts { get; }
|
||||
@@ -51,20 +59,50 @@
|
||||
$"internalPorts: {string.Join(",", InternalPorts.Select(p => p.Number))}, " +
|
||||
$"envVars: {string.Join(",", EnvVars.Select(v => v.ToString()))}, " +
|
||||
$"limits: {Resources}, " +
|
||||
$"affinity: {SchedulingAffinity}, " +
|
||||
$"volumes: {string.Join(",", Volumes.Select(v => $"'{v.MountPath}'"))}";
|
||||
}
|
||||
}
|
||||
|
||||
public class Port
|
||||
{
|
||||
public Port(int number, string tag)
|
||||
public Port(int number, string tag, PortProtocol protocol)
|
||||
{
|
||||
Number = number;
|
||||
Tag = tag;
|
||||
Protocol = protocol;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Tag))
|
||||
{
|
||||
throw new Exception("A unique port tag is required");
|
||||
}
|
||||
}
|
||||
|
||||
public int Number { get; }
|
||||
public string Tag { get; }
|
||||
public PortProtocol Protocol { get; }
|
||||
|
||||
public bool IsTcp()
|
||||
{
|
||||
return Protocol == PortProtocol.TCP;
|
||||
}
|
||||
|
||||
public bool IsUdp()
|
||||
{
|
||||
return Protocol == PortProtocol.UDP;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Tag)) return $"untagged-port={Number}/{Protocol}";
|
||||
return $"{Tag}={Number}/{Protocol}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum PortProtocol
|
||||
{
|
||||
TCP,
|
||||
UDP
|
||||
}
|
||||
|
||||
public class EnvVar
|
||||
@@ -86,15 +124,21 @@
|
||||
|
||||
public class VolumeMount
|
||||
{
|
||||
public VolumeMount(string volumeName, string mountPath, string resourceQuantity)
|
||||
public VolumeMount(string volumeName, string mountPath, string? subPath = null, string? resourceQuantity = null, string? secret = null, string? hostPath = null)
|
||||
{
|
||||
VolumeName = volumeName;
|
||||
MountPath = mountPath;
|
||||
SubPath = subPath;
|
||||
ResourceQuantity = resourceQuantity;
|
||||
Secret = secret;
|
||||
HostPath = hostPath;
|
||||
}
|
||||
|
||||
public string VolumeName { get; }
|
||||
public string MountPath { get; }
|
||||
public string ResourceQuantity { get; }
|
||||
public string? SubPath { get; }
|
||||
public string? ResourceQuantity { get; }
|
||||
public string? Secret { get; }
|
||||
public string? HostPath { get; }
|
||||
}
|
||||
}
|
||||
+44
-20
@@ -1,6 +1,6 @@
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public abstract class ContainerRecipeFactory
|
||||
{
|
||||
@@ -13,6 +13,9 @@ namespace KubernetesWorkflow
|
||||
private readonly List<object> additionals = new List<object>();
|
||||
private RecipeComponentFactory factory = null!;
|
||||
private ContainerResources resources = new ContainerResources();
|
||||
private SchedulingAffinity schedulingAffinity = new SchedulingAffinity();
|
||||
private CommandOverride commandOverride = new CommandOverride();
|
||||
private bool setCriticalPriority;
|
||||
|
||||
public ContainerRecipe CreateRecipe(int index, int containerNumber, RecipeComponentFactory factory, StartupConfig config)
|
||||
{
|
||||
@@ -22,7 +25,7 @@ namespace KubernetesWorkflow
|
||||
|
||||
Initialize(config);
|
||||
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources,
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
|
||||
exposedPorts.ToArray(),
|
||||
internalPorts.ToArray(),
|
||||
envVars.ToArray(),
|
||||
@@ -40,6 +43,9 @@ namespace KubernetesWorkflow
|
||||
additionals.Clear();
|
||||
this.factory = null!;
|
||||
resources = new ContainerResources();
|
||||
schedulingAffinity = new SchedulingAffinity();
|
||||
commandOverride = new CommandOverride();
|
||||
setCriticalPriority = false;
|
||||
|
||||
return recipe;
|
||||
}
|
||||
@@ -50,31 +56,31 @@ namespace KubernetesWorkflow
|
||||
protected int Index { get; private set; } = 0;
|
||||
protected abstract void Initialize(StartupConfig config);
|
||||
|
||||
protected Port AddExposedPort(string tag = "")
|
||||
protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
return AddExposedPort(factory.CreatePort(tag));
|
||||
return AddExposedPort(factory.CreateExternalPort(tag, protocol));
|
||||
}
|
||||
|
||||
protected Port AddExposedPort(int number, string tag = "")
|
||||
protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
return AddExposedPort(factory.CreatePort(number, tag));
|
||||
return AddExposedPort(factory.CreateExternalPort(number, tag, protocol));
|
||||
}
|
||||
|
||||
protected Port AddInternalPort(string tag = "")
|
||||
protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
var p = factory.CreatePort(tag);
|
||||
var p = factory.CreateInternalPort(tag, protocol);
|
||||
internalPorts.Add(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
protected void AddExposedPortAndVar(string name, string tag = "")
|
||||
protected void AddExposedPortAndVar(string name, string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
AddEnvVar(name, AddExposedPort(tag));
|
||||
AddEnvVar(name, AddExposedPort(tag, protocol));
|
||||
}
|
||||
|
||||
protected void AddInternalPortAndVar(string name, string tag = "")
|
||||
protected void AddInternalPortAndVar(string name, string tag = "", PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
AddEnvVar(name, AddInternalPort(tag));
|
||||
AddEnvVar(name, AddInternalPort(tag, protocol));
|
||||
}
|
||||
|
||||
protected void AddEnvVar(string name, string value)
|
||||
@@ -97,12 +103,18 @@ namespace KubernetesWorkflow
|
||||
podAnnotations.Add(name, value);
|
||||
}
|
||||
|
||||
protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null)
|
||||
{
|
||||
var size = 10.MB().ToSuffixNotation();
|
||||
volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath));
|
||||
}
|
||||
|
||||
protected void AddVolume(string mountPath, ByteSize volumeSize)
|
||||
{
|
||||
volumeMounts.Add(new VolumeMount(
|
||||
$"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}",
|
||||
mountPath,
|
||||
volumeSize.ToSuffixNotation()));
|
||||
resourceQuantity: volumeSize.ToSuffixNotation()));
|
||||
}
|
||||
|
||||
protected void Additional(object userData)
|
||||
@@ -115,11 +127,28 @@ namespace KubernetesWorkflow
|
||||
SetResourcesRequest(new ContainerResourceSet(milliCPUs, memory));
|
||||
}
|
||||
|
||||
protected void SetResourceLimits(int milliCPUs, ByteSize memory)
|
||||
protected void SetSchedulingAffinity(string notIn)
|
||||
{
|
||||
SetResourceLimits(new ContainerResourceSet(milliCPUs, memory));
|
||||
schedulingAffinity = new SchedulingAffinity(notIn);
|
||||
}
|
||||
|
||||
protected void OverrideCommand(params string[] command)
|
||||
{
|
||||
commandOverride = new CommandOverride(command);
|
||||
}
|
||||
|
||||
protected void SetSystemCriticalPriority()
|
||||
{
|
||||
setCriticalPriority = true;
|
||||
}
|
||||
|
||||
// Disabled following a possible bug in the k8s cluster that will throttle containers much more than is
|
||||
// called for if they have resource limits defined.
|
||||
//protected void SetResourceLimits(int milliCPUs, ByteSize memory)
|
||||
//{
|
||||
// SetResourceLimits(new ContainerResourceSet(milliCPUs, memory));
|
||||
//}
|
||||
|
||||
protected void SetResourcesRequest(ContainerResourceSet requests)
|
||||
{
|
||||
resources.Requests = requests;
|
||||
@@ -132,11 +161,6 @@ namespace KubernetesWorkflow
|
||||
|
||||
private Port AddExposedPort(Port port)
|
||||
{
|
||||
if (exposedPorts.Any())
|
||||
{
|
||||
throw new NotImplementedException("Current implementation only support 1 exposed port per container recipe. " +
|
||||
$"Methods for determining container addresses in {nameof(StartupWorkflow)} currently rely on this constraint.");
|
||||
}
|
||||
exposedPorts.Add(port);
|
||||
return port;
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class ContainerResources
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class PodAnnotations
|
||||
{
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class PodLabels
|
||||
{
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Globalization;
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class RecipeComponentFactory
|
||||
{
|
||||
private readonly NumberSource internalNumberSource = new NumberSource(8080);
|
||||
private static readonly NumberSource externalNumberSource = new NumberSource(30000);
|
||||
private static int[] usedExternalPorts = Array.Empty<int>();
|
||||
|
||||
public void Update(K8sController controller)
|
||||
{
|
||||
usedExternalPorts = controller.GetUsedExternalPorts();
|
||||
}
|
||||
|
||||
public Port CreateInternalPort(string tag, PortProtocol protocol)
|
||||
{
|
||||
return new Port(internalNumberSource.GetNextNumber(), tag, protocol);
|
||||
}
|
||||
|
||||
public Port CreateExternalPort(int number, string tag, PortProtocol protocol)
|
||||
{
|
||||
if (usedExternalPorts.Contains(number)) throw new Exception($"External port number {number} is already in use by the cluster.");
|
||||
return new Port(number, tag, protocol);
|
||||
}
|
||||
|
||||
public Port CreateExternalPort(string tag, PortProtocol protocol)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var number = externalNumberSource.GetNextNumber();
|
||||
if (!usedExternalPorts.Contains(number))
|
||||
{
|
||||
return new Port(number, tag, protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public EnvVar CreateEnvVar(string name, int value)
|
||||
{
|
||||
return CreateEnvVar(name, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public EnvVar CreateEnvVar(string name, string value)
|
||||
{
|
||||
return new EnvVar(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class SchedulingAffinity
|
||||
{
|
||||
public SchedulingAffinity(string? notIn = null)
|
||||
{
|
||||
NotIn = notIn;
|
||||
}
|
||||
|
||||
public string? NotIn { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (string.IsNullOrEmpty(NotIn)) return "none";
|
||||
return "notIn:" + NotIn;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Globalization;
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public class RecipeComponentFactory
|
||||
{
|
||||
private NumberSource portNumberSource = new NumberSource(8080);
|
||||
|
||||
public Port CreatePort(int number, string tag)
|
||||
{
|
||||
return new Port(number, tag);
|
||||
}
|
||||
|
||||
public Port CreatePort(string tag)
|
||||
{
|
||||
return new Port(portNumberSource.GetNextNumber(), tag);
|
||||
}
|
||||
|
||||
public EnvVar CreateEnvVar(string name, int value)
|
||||
{
|
||||
return CreateEnvVar(name, value.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
public EnvVar CreateEnvVar(string name, string value)
|
||||
{
|
||||
return new EnvVar(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +1,38 @@
|
||||
using System.Net.NetworkInformation;
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
internal enum RunnerLocation
|
||||
{
|
||||
Unknown,
|
||||
ExternalToCluster,
|
||||
InternalToCluster,
|
||||
}
|
||||
|
||||
internal static class RunnerLocationUtils
|
||||
{
|
||||
private static RunnerLocation? knownLocation = null;
|
||||
private static RunnerLocation location = RunnerLocation.Unknown;
|
||||
|
||||
internal static RunnerLocation DetermineRunnerLocation(RunningContainer container)
|
||||
internal static RunnerLocation GetRunnerLocation()
|
||||
{
|
||||
if (knownLocation != null) return knownLocation.Value;
|
||||
|
||||
if (PingHost(container.Pod.PodInfo.Ip))
|
||||
{
|
||||
knownLocation = RunnerLocation.InternalToCluster;
|
||||
}
|
||||
else if (PingHost(Format(container.ClusterExternalAddress)))
|
||||
{
|
||||
knownLocation = RunnerLocation.ExternalToCluster;
|
||||
}
|
||||
|
||||
if (knownLocation == null) throw new Exception("Unable to determine location relative to kubernetes cluster.");
|
||||
return knownLocation.Value;
|
||||
DetermineRunnerLocation();
|
||||
if (location == RunnerLocation.Unknown) throw new Exception("Runner location is unknown.");
|
||||
return location;
|
||||
}
|
||||
|
||||
private static string Format(Address host)
|
||||
private static void DetermineRunnerLocation()
|
||||
{
|
||||
return host.Host
|
||||
.Replace("http://", "")
|
||||
.Replace("https://", "");
|
||||
}
|
||||
if (location != RunnerLocation.Unknown) return;
|
||||
|
||||
private static bool PingHost(string host)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var pinger = new Ping();
|
||||
PingReply reply = pinger.Send(host);
|
||||
return reply.Status == IPStatus.Success;
|
||||
}
|
||||
catch (PingException)
|
||||
{
|
||||
}
|
||||
var port = Environment.GetEnvironmentVariable("KUBERNETES_PORT");
|
||||
var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
|
||||
|
||||
return false;
|
||||
if (string.IsNullOrEmpty(port) || string.IsNullOrEmpty(host))
|
||||
{
|
||||
location = RunnerLocation.ExternalToCluster;
|
||||
}
|
||||
else
|
||||
{
|
||||
location = RunnerLocation.InternalToCluster;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public class RunningContainers
|
||||
{
|
||||
public RunningContainers(StartupConfig startupConfig, RunningPod runningPod, RunningContainer[] containers)
|
||||
{
|
||||
StartupConfig = startupConfig;
|
||||
RunningPod = runningPod;
|
||||
Containers = containers;
|
||||
}
|
||||
|
||||
public StartupConfig StartupConfig { get; }
|
||||
public RunningPod RunningPod { get; }
|
||||
public RunningContainer[] Containers { get; }
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return string.Join(",", Containers.Select(c => c.Name));
|
||||
}
|
||||
}
|
||||
|
||||
public class RunningContainer
|
||||
{
|
||||
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, string name, Address clusterExternalAddress, Address clusterInternalAddress)
|
||||
{
|
||||
Pod = pod;
|
||||
Recipe = recipe;
|
||||
ServicePorts = servicePorts;
|
||||
Name = name;
|
||||
ClusterExternalAddress = clusterExternalAddress;
|
||||
ClusterInternalAddress = clusterInternalAddress;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public RunningPod Pod { get; }
|
||||
public ContainerRecipe Recipe { get; }
|
||||
public Port[] ServicePorts { get; }
|
||||
public Address ClusterExternalAddress { get; }
|
||||
public Address ClusterInternalAddress { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Address Address
|
||||
{
|
||||
get
|
||||
{
|
||||
if (RunnerLocationUtils.DetermineRunnerLocation(this) == RunnerLocation.InternalToCluster)
|
||||
{
|
||||
return ClusterInternalAddress;
|
||||
}
|
||||
return ClusterExternalAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RunningContainersExtensions
|
||||
{
|
||||
public static RunningContainer[] Containers(this RunningContainers[] runningContainers)
|
||||
{
|
||||
return runningContainers.SelectMany(c => c.Containers).ToArray();
|
||||
}
|
||||
|
||||
public static string Describe(this RunningContainers[] runningContainers)
|
||||
{
|
||||
return string.Join(",", runningContainers.Select(c => c.Describe()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,69 @@
|
||||
namespace KubernetesWorkflow
|
||||
using k8s;
|
||||
using k8s.Models;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public class RunningPod
|
||||
public class StartResult
|
||||
{
|
||||
public RunningPod(K8sCluster cluster, PodInfo podInfo, string deploymentName, string serviceName, ContainerRecipePortMapEntry[] portMapEntries)
|
||||
public StartResult(
|
||||
K8sCluster cluster,
|
||||
ContainerRecipe[] containerRecipes,
|
||||
RunningDeployment deployment,
|
||||
RunningService? internalService,
|
||||
RunningService? externalService)
|
||||
{
|
||||
Cluster = cluster;
|
||||
PodInfo = podInfo;
|
||||
DeploymentName = deploymentName;
|
||||
ServiceName = serviceName;
|
||||
PortMapEntries = portMapEntries;
|
||||
ContainerRecipes = containerRecipes;
|
||||
Deployment = deployment;
|
||||
InternalService = internalService;
|
||||
ExternalService = externalService;
|
||||
}
|
||||
|
||||
public K8sCluster Cluster { get; }
|
||||
public PodInfo PodInfo { get; }
|
||||
public ContainerRecipePortMapEntry[] PortMapEntries { get; }
|
||||
public string DeploymentName { get; }
|
||||
public string ServiceName { get; }
|
||||
public ContainerRecipe[] ContainerRecipes { get; }
|
||||
public RunningDeployment Deployment { get; }
|
||||
public RunningService? InternalService { get; }
|
||||
public RunningService? ExternalService { get; }
|
||||
|
||||
public Port[] GetServicePortsForContainerRecipe(ContainerRecipe containerRecipe)
|
||||
public Port GetInternalServicePorts(ContainerRecipe recipe, string tag)
|
||||
{
|
||||
if (PortMapEntries.Any(p => p.ContainerNumber == containerRecipe.Number))
|
||||
if (InternalService != null)
|
||||
{
|
||||
return PortMapEntries.Single(p => p.ContainerNumber == containerRecipe.Number).Ports;
|
||||
var p = InternalService.GetServicePortForRecipeAndTag(recipe, tag);
|
||||
if (p != null) return p;
|
||||
}
|
||||
|
||||
throw new Exception($"Unable to find internal port by tag '{tag}' for recipe '{recipe.Name}'.");
|
||||
}
|
||||
|
||||
public Port GetExternalServicePorts(ContainerRecipe recipe, string tag)
|
||||
{
|
||||
if (ExternalService != null)
|
||||
{
|
||||
var p = ExternalService.GetServicePortForRecipeAndTag(recipe, tag);
|
||||
if (p != null) return p;
|
||||
}
|
||||
|
||||
throw new Exception($"Unable to find external port by tag '{tag}' for recipe '{recipe.Name}'.");
|
||||
}
|
||||
|
||||
public Port[] GetServicePortsForContainer(ContainerRecipe recipe)
|
||||
{
|
||||
if (InternalService != null)
|
||||
{
|
||||
var p = InternalService.GetServicePortsForRecipe(recipe);
|
||||
if (p.Any()) return p;
|
||||
}
|
||||
if (ExternalService != null)
|
||||
{
|
||||
var p = ExternalService.GetServicePortsForRecipe(recipe);
|
||||
if (p.Any()) return p;
|
||||
}
|
||||
|
||||
return Array.Empty<Port>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ContainerRecipePortMapEntry
|
||||
{
|
||||
public ContainerRecipePortMapEntry(int containerNumber, Port[] ports)
|
||||
{
|
||||
ContainerNumber = containerNumber;
|
||||
Ports = ports;
|
||||
}
|
||||
|
||||
public int ContainerNumber { get; }
|
||||
public Port[] Ports { get; }
|
||||
}
|
||||
|
||||
public class PodInfo
|
||||
{
|
||||
public PodInfo(string name, string ip, string k8sNodeName)
|
||||
{
|
||||
Name = name;
|
||||
Ip = ip;
|
||||
K8SNodeName = k8sNodeName;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Ip { get; }
|
||||
public string K8SNodeName { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Logging;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
@@ -6,10 +9,12 @@ namespace KubernetesWorkflow
|
||||
public interface IStartupWorkflow
|
||||
{
|
||||
IKnownLocations GetAvailableLocations();
|
||||
RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
PodInfo GetPodInfo(RunningContainer container);
|
||||
PodInfo GetPodInfo(RunningPod pod);
|
||||
CrashWatcher CreateCrashWatcher(RunningContainer container);
|
||||
void Stop(RunningContainers runningContainers);
|
||||
void Stop(RunningPod pod, bool waitTillStopped);
|
||||
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
|
||||
string ExecuteCommand(RunningContainer container, string command, params string[] args);
|
||||
void DeleteNamespace();
|
||||
@@ -21,17 +26,15 @@ namespace KubernetesWorkflow
|
||||
private readonly ILog log;
|
||||
private readonly WorkflowNumberSource numberSource;
|
||||
private readonly K8sCluster cluster;
|
||||
private readonly KnownK8sPods knownK8SPods;
|
||||
private readonly string k8sNamespace;
|
||||
private readonly RecipeComponentFactory componentFactory = new RecipeComponentFactory();
|
||||
private readonly LocationProvider locationProvider;
|
||||
|
||||
internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, KnownK8sPods knownK8SPods, string k8sNamespace)
|
||||
internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, string k8sNamespace)
|
||||
{
|
||||
this.log = log;
|
||||
this.numberSource = numberSource;
|
||||
this.cluster = cluster;
|
||||
this.knownK8SPods = knownK8SPods;
|
||||
this.k8sNamespace = k8sNamespace;
|
||||
|
||||
locationProvider = new LocationProvider(log, K8s);
|
||||
@@ -42,36 +45,64 @@ namespace KubernetesWorkflow
|
||||
return locationProvider.GetAvailableLocations();
|
||||
}
|
||||
|
||||
public RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
public FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
{
|
||||
return Start(numberOfContainers, KnownLocations.UnspecifiedLocation, recipeFactory, startupConfig);
|
||||
}
|
||||
|
||||
public RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
public FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
{
|
||||
return K8s(controller =>
|
||||
{
|
||||
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
|
||||
var runningPod = controller.BringOnline(recipes, location);
|
||||
var containers = CreateContainers(runningPod, recipes, startupConfig);
|
||||
componentFactory.Update(controller);
|
||||
|
||||
var rc = new RunningContainers(startupConfig, runningPod, containers);
|
||||
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
|
||||
var startResult = controller.BringOnline(recipes, location);
|
||||
var containers = CreateContainers(startResult, recipes, startupConfig);
|
||||
|
||||
var rc = new RunningPod(startupConfig, startResult, containers);
|
||||
cluster.Configuration.Hooks.OnContainersStarted(rc);
|
||||
return rc;
|
||||
|
||||
if (startResult.ExternalService != null)
|
||||
{
|
||||
componentFactory.Update(controller);
|
||||
}
|
||||
return new FutureContainers(rc, this);
|
||||
});
|
||||
}
|
||||
|
||||
public void WaitUntilOnline(RunningPod rc)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
foreach (var c in rc.Containers)
|
||||
{
|
||||
controller.WaitUntilOnline(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningContainer container)
|
||||
{
|
||||
return K8s(c => c.GetPodInfo(container.RunningPod.StartResult.Deployment));
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningPod pod)
|
||||
{
|
||||
return K8s(c => c.GetPodInfo(pod.StartResult.Deployment));
|
||||
}
|
||||
|
||||
public CrashWatcher CreateCrashWatcher(RunningContainer container)
|
||||
{
|
||||
return K8s(c => c.CreateCrashWatcher(container));
|
||||
}
|
||||
|
||||
public void Stop(RunningContainers runningContainers)
|
||||
public void Stop(RunningPod runningPod, bool waitTillStopped)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.Stop(runningContainers.RunningPod);
|
||||
cluster.Configuration.Hooks.OnContainersStopped(runningContainers);
|
||||
controller.Stop(runningPod.StartResult, waitTillStopped);
|
||||
cluster.Configuration.Hooks.OnContainersStopped(runningPod);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +110,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.DownloadPodLog(container.Pod, container.Recipe, logHandler, tailLines);
|
||||
controller.DownloadPodLog(container, logHandler, tailLines);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,7 +118,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
return K8s(controller =>
|
||||
{
|
||||
return controller.ExecuteCommand(container.Pod, container.Recipe.Name, command, args);
|
||||
return controller.ExecuteCommand(container, command, args);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,19 +138,16 @@ namespace KubernetesWorkflow
|
||||
});
|
||||
}
|
||||
|
||||
private RunningContainer[] CreateContainers(RunningPod runningPod, ContainerRecipe[] recipes, StartupConfig startupConfig)
|
||||
private RunningContainer[] CreateContainers(StartResult startResult, ContainerRecipe[] recipes, StartupConfig startupConfig)
|
||||
{
|
||||
log.Debug();
|
||||
return recipes.Select(r =>
|
||||
{
|
||||
var servicePorts = runningPod.GetServicePortsForContainerRecipe(r);
|
||||
log.Debug($"{r} -> service ports: {string.Join(",", servicePorts.Select(p => p.Number))}");
|
||||
|
||||
var name = GetContainerName(r, startupConfig);
|
||||
var addresses = CreateContainerAddresses(startResult, r);
|
||||
log.Debug($"{r}={name} -> container addresses: {string.Join(Environment.NewLine, addresses.Select(a => a.ToString()))}");
|
||||
|
||||
return new RunningContainer(runningPod, r, servicePorts, name,
|
||||
GetContainerExternalAddress(runningPod, servicePorts),
|
||||
GetContainerInternalAddress(r));
|
||||
return new RunningContainer(name, r, addresses);
|
||||
|
||||
}).ToArray();
|
||||
}
|
||||
@@ -137,35 +165,42 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
}
|
||||
|
||||
private Address GetContainerExternalAddress(RunningPod pod, Port[] servicePorts)
|
||||
private ContainerAddress[] CreateContainerAddresses(StartResult startResult, ContainerRecipe recipe)
|
||||
{
|
||||
return new Address(
|
||||
pod.Cluster.HostAddress,
|
||||
GetServicePort(servicePorts));
|
||||
var result = new List<ContainerAddress>();
|
||||
foreach (var exposedPort in recipe.ExposedPorts)
|
||||
{
|
||||
result.Add(new ContainerAddress(exposedPort.Tag, GetContainerExternalAddress(startResult, recipe, exposedPort.Tag), false));
|
||||
result.Add(new ContainerAddress(exposedPort.Tag, GetContainerInternalAddress(startResult, recipe, exposedPort.Tag), true));
|
||||
}
|
||||
foreach (var internalPort in recipe.InternalPorts)
|
||||
{
|
||||
result.Add(new ContainerAddress(internalPort.Tag, GetContainerInternalAddress(startResult, recipe, internalPort.Tag), true));
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private Address GetContainerInternalAddress(ContainerRecipe recipe)
|
||||
private static Address GetContainerExternalAddress(StartResult startResult, ContainerRecipe recipe, string tag)
|
||||
{
|
||||
var serviceName = "service-" + numberSource.WorkflowNumber;
|
||||
var port = GetInternalPort(recipe);
|
||||
var port = startResult.GetExternalServicePorts(recipe, tag);
|
||||
|
||||
return new Address(
|
||||
$"http://{serviceName}.{k8sNamespace}.svc.cluster.local",
|
||||
port);
|
||||
startResult.Cluster.HostAddress,
|
||||
port.Number);
|
||||
}
|
||||
|
||||
private static int GetServicePort(Port[] servicePorts)
|
||||
private Address GetContainerInternalAddress(StartResult startResult, ContainerRecipe recipe, string tag)
|
||||
{
|
||||
if (servicePorts.Any()) return servicePorts.First().Number;
|
||||
return 0;
|
||||
}
|
||||
var namespaceName = startResult.Cluster.Configuration.KubernetesNamespace;
|
||||
var serviceName = startResult.InternalService!.Name;
|
||||
var port = startResult.GetInternalServicePorts(recipe, tag);
|
||||
|
||||
private static int GetInternalPort(ContainerRecipe recipe)
|
||||
{
|
||||
if (recipe.ExposedPorts.Any()) return recipe.ExposedPorts.First().Number;
|
||||
return 0;
|
||||
return new Address(
|
||||
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
|
||||
port.Number);
|
||||
}
|
||||
|
||||
|
||||
private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
|
||||
{
|
||||
log.Debug();
|
||||
@@ -173,6 +208,8 @@ namespace KubernetesWorkflow
|
||||
for (var i = 0; i < numberOfContainers; i++)
|
||||
{
|
||||
var recipe = recipeFactory.CreateRecipe(i, numberSource.GetContainerNumber(), componentFactory, startupConfig);
|
||||
CheckPorts(recipe);
|
||||
|
||||
if (cluster.Configuration.AddAppPodLabel) recipe.PodLabels.Add("app", recipeFactory.AppName);
|
||||
cluster.Configuration.Hooks.OnContainerRecipeCreated(recipe);
|
||||
result.Add(recipe);
|
||||
@@ -181,40 +218,47 @@ namespace KubernetesWorkflow
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private void CheckPorts(ContainerRecipe recipe)
|
||||
{
|
||||
var allTags =
|
||||
recipe.ExposedPorts.Concat(recipe.InternalPorts)
|
||||
.Select(p => K8sNameUtils.Format(p.Tag)).ToArray();
|
||||
|
||||
if (allTags.Length != allTags.Distinct().Count())
|
||||
{
|
||||
throw new Exception("Duplicate port tags found in recipe for " + recipe.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private void K8s(Action<K8sController> action)
|
||||
{
|
||||
var controller = new K8sController(log, cluster, knownK8SPods, numberSource, k8sNamespace);
|
||||
action(controller);
|
||||
controller.Dispose();
|
||||
try
|
||||
{
|
||||
var controller = new K8sController(log, cluster, numberSource, k8sNamespace);
|
||||
action(controller);
|
||||
controller.Dispose();
|
||||
}
|
||||
catch (k8s.Autorest.HttpOperationException ex)
|
||||
{
|
||||
log.Error(JsonConvert.SerializeObject(ex.Response));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private T K8s<T>(Func<K8sController, T> action)
|
||||
{
|
||||
var controller = new K8sController(log, cluster, knownK8SPods, numberSource, k8sNamespace);
|
||||
var result = action(controller);
|
||||
controller.Dispose();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public interface ILogHandler
|
||||
{
|
||||
void Log(Stream log);
|
||||
}
|
||||
|
||||
public abstract class LogHandler : ILogHandler
|
||||
{
|
||||
public void Log(Stream log)
|
||||
{
|
||||
using var reader = new StreamReader(log);
|
||||
var line = reader.ReadLine();
|
||||
while (line != null)
|
||||
try
|
||||
{
|
||||
ProcessLine(line);
|
||||
line = reader.ReadLine();
|
||||
var controller = new K8sController(log, cluster, numberSource, k8sNamespace);
|
||||
var result = action(controller);
|
||||
controller.Dispose();
|
||||
return result;
|
||||
}
|
||||
catch (k8s.Autorest.HttpOperationException ex)
|
||||
{
|
||||
log.Error(JsonConvert.SerializeObject(ex.Response));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void ProcessLine(string line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class ContainerAddress
|
||||
{
|
||||
public ContainerAddress(string portTag, Address address, bool isInteral)
|
||||
{
|
||||
PortTag = portTag;
|
||||
Address = address;
|
||||
IsInteral = isInteral;
|
||||
}
|
||||
|
||||
public string PortTag { get; }
|
||||
public Address Address { get; }
|
||||
public bool IsInteral { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var indicator = IsInteral ? "int" : "ext";
|
||||
return $"{indicator} {PortTag} -> '{Address}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using KubernetesWorkflow.Recipe;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class ContainerRecipePortMapEntry
|
||||
{
|
||||
public ContainerRecipePortMapEntry(int recipeNumber, Port[] ports)
|
||||
{
|
||||
RecipeNumber = recipeNumber;
|
||||
Ports = ports;
|
||||
}
|
||||
|
||||
public int RecipeNumber { get; }
|
||||
public Port[] Ports { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class FutureContainers
|
||||
{
|
||||
private readonly RunningPod runningPod;
|
||||
private readonly StartupWorkflow workflow;
|
||||
|
||||
public FutureContainers(RunningPod runningPod, StartupWorkflow workflow)
|
||||
{
|
||||
this.runningPod = runningPod;
|
||||
this.workflow = workflow;
|
||||
}
|
||||
|
||||
public RunningPod WaitForOnline()
|
||||
{
|
||||
workflow.WaitUntilOnline(runningPod);
|
||||
return runningPod;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class K8sNodeLabel
|
||||
{
|
||||
public K8sNodeLabel(string key, string value)
|
||||
{
|
||||
Key = key;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public string Key { get; }
|
||||
public string Value { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class PodInfo
|
||||
{
|
||||
public PodInfo(string name, string ip, string k8sNodeName)
|
||||
{
|
||||
Name = name;
|
||||
Ip = ip;
|
||||
K8SNodeName = k8sNodeName;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Ip { get; }
|
||||
public string K8SNodeName { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningContainer
|
||||
{
|
||||
public RunningContainer(string name, ContainerRecipe recipe, ContainerAddress[] addresses)
|
||||
{
|
||||
Name = name;
|
||||
Recipe = recipe;
|
||||
Addresses = addresses;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public ContainerRecipe Recipe { get; }
|
||||
public ContainerAddress[] Addresses { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public RunningPod RunningPod { get; internal set; } = null!;
|
||||
|
||||
public Address GetAddress(ILog log, string portTag)
|
||||
{
|
||||
var addresses = Addresses.Where(a => a.PortTag == portTag).ToArray();
|
||||
if (!addresses.Any()) throw new Exception("No addresses found for portTag: " + portTag);
|
||||
|
||||
var select = SelectAddress(addresses);
|
||||
log.Debug($"Container '{Name}' selected for tag '{portTag}' address: '{select}'");
|
||||
return select.Address;
|
||||
}
|
||||
|
||||
public Address GetInternalAddress(string portTag)
|
||||
{
|
||||
var containerAddress = Addresses.Single(a => a.PortTag == portTag && a.IsInteral);
|
||||
return containerAddress.Address;
|
||||
}
|
||||
|
||||
private ContainerAddress SelectAddress(ContainerAddress[] addresses)
|
||||
{
|
||||
var location = RunnerLocationUtils.GetRunnerLocation();
|
||||
if (location == RunnerLocation.InternalToCluster)
|
||||
{
|
||||
return addresses.Single(a => a.IsInteral);
|
||||
}
|
||||
if (location == RunnerLocation.ExternalToCluster)
|
||||
{
|
||||
return addresses.Single(a => !a.IsInteral);
|
||||
}
|
||||
throw new Exception("Running location not known.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningDeployment
|
||||
{
|
||||
public RunningDeployment(string name, string podLabel)
|
||||
{
|
||||
Name = name;
|
||||
PodLabel = podLabel;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string PodLabel { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningPod
|
||||
{
|
||||
public RunningPod(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
|
||||
{
|
||||
StartupConfig = startupConfig;
|
||||
StartResult = startResult;
|
||||
Containers = containers;
|
||||
|
||||
foreach (var c in containers) c.RunningPod = this;
|
||||
}
|
||||
|
||||
public StartupConfig StartupConfig { get; }
|
||||
public StartResult StartResult { get; }
|
||||
public RunningContainer[] Containers { get; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string Name
|
||||
{
|
||||
get { return $"{Containers.Length}x '{Containers.First().Name}'"; }
|
||||
}
|
||||
|
||||
public string Describe()
|
||||
{
|
||||
return string.Join(",", Containers.Select(c => c.Name));
|
||||
}
|
||||
}
|
||||
|
||||
public static class RunningContainersExtensions
|
||||
{
|
||||
public static string Describe(this RunningPod[] runningContainers)
|
||||
{
|
||||
return string.Join(",", runningContainers.Select(c => c.Describe()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using KubernetesWorkflow.Recipe;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningService
|
||||
{
|
||||
public RunningService(string name, List<ContainerRecipePortMapEntry> result)
|
||||
{
|
||||
Name = name;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public List<ContainerRecipePortMapEntry> Result { get; }
|
||||
|
||||
public Port? GetServicePortForRecipeAndTag(ContainerRecipe recipe, string tag)
|
||||
{
|
||||
return GetServicePortsForRecipe(recipe).SingleOrDefault(p => p.Tag == tag);
|
||||
}
|
||||
|
||||
public Port[] GetServicePortsForRecipe(ContainerRecipe recipe)
|
||||
{
|
||||
return Result
|
||||
.Where(p => p.RecipeNumber == recipe.Number)
|
||||
.SelectMany(p => p.Ports)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
private readonly NumberSource numberSource = new NumberSource(0);
|
||||
private readonly NumberSource containerNumberSource = new NumberSource(0);
|
||||
private readonly KnownK8sPods knownPods = new KnownK8sPods();
|
||||
private readonly K8sCluster cluster;
|
||||
private readonly ILog log;
|
||||
private readonly Configuration configuration;
|
||||
@@ -26,7 +25,7 @@ namespace KubernetesWorkflow
|
||||
var workflowNumberSource = new WorkflowNumberSource(numberSource.GetNextNumber(),
|
||||
containerNumberSource);
|
||||
|
||||
return new StartupWorkflow(log, workflowNumberSource, cluster, knownPods, GetNamespace(namespaceOverride));
|
||||
return new StartupWorkflow(log, workflowNumberSource, cluster, GetNamespace(namespaceOverride));
|
||||
}
|
||||
|
||||
private string GetNamespace(string? namespaceOverride)
|
||||
|
||||
@@ -13,16 +13,21 @@ namespace Logging
|
||||
|
||||
public abstract class BaseLog : ILog
|
||||
{
|
||||
public static bool EnableDebugLogging { get; set; } = false;
|
||||
|
||||
private readonly NumberSource subfileNumberSource = new NumberSource(0);
|
||||
private readonly bool debug;
|
||||
private readonly List<BaseLogStringReplacement> replacements = new List<BaseLogStringReplacement>();
|
||||
private LogFile? logFile;
|
||||
|
||||
protected BaseLog(bool debug)
|
||||
public BaseLog()
|
||||
{
|
||||
this.debug = debug;
|
||||
IsDebug =
|
||||
EnableDebugLogging ||
|
||||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("LOGDEBUG")) ||
|
||||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DEBUGLOG"));
|
||||
}
|
||||
|
||||
protected bool IsDebug { get; private set; }
|
||||
protected abstract string GetFullName();
|
||||
|
||||
public LogFile LogFile
|
||||
@@ -39,19 +44,20 @@ namespace Logging
|
||||
LogFile.Write(ApplyReplacements(message));
|
||||
}
|
||||
|
||||
public virtual void Debug(string message = "", int skipFrames = 0)
|
||||
public void Debug(string message = "", int skipFrames = 0)
|
||||
{
|
||||
if (debug)
|
||||
if (IsDebug)
|
||||
{
|
||||
var callerName = DebugStack.GetCallerName(skipFrames);
|
||||
// We don't use Log because in the debug output we should not have any replacements.
|
||||
LogFile.Write($"(debug)({callerName}) {message}");
|
||||
Log($"(debug)({callerName}) {message}");
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Error(string message)
|
||||
{
|
||||
Log($"[ERROR] {message}");
|
||||
var msg = $"[ERROR] {message}";
|
||||
Console.WriteLine(msg);
|
||||
Log(msg);
|
||||
}
|
||||
|
||||
public virtual void AddStringReplace(string from, string to)
|
||||
@@ -73,6 +79,7 @@ namespace Logging
|
||||
|
||||
private string ApplyReplacements(string str)
|
||||
{
|
||||
if (IsDebug) return str;
|
||||
foreach (var replacement in replacements)
|
||||
{
|
||||
str = replacement.Apply(str);
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
{
|
||||
public class ConsoleLog : BaseLog
|
||||
{
|
||||
public ConsoleLog() : base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override string GetFullName()
|
||||
{
|
||||
return "CONSOLE";
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Logging
|
||||
{
|
||||
public class FileLog : BaseLog
|
||||
{
|
||||
public FileLog(string fullFilename)
|
||||
{
|
||||
FullFilename = fullFilename;
|
||||
}
|
||||
|
||||
public string FullFilename { get; }
|
||||
|
||||
protected override string GetFullName()
|
||||
{
|
||||
return FullFilename;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,11 @@
|
||||
{
|
||||
public class LogConfig
|
||||
{
|
||||
public LogConfig(string logRoot, bool debugEnabled)
|
||||
public LogConfig(string logRoot)
|
||||
{
|
||||
LogRoot = logRoot;
|
||||
DebugEnabled = debugEnabled;
|
||||
}
|
||||
|
||||
public string LogRoot { get; }
|
||||
public bool DebugEnabled { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,21 @@
|
||||
public class LogPrefixer : ILog
|
||||
{
|
||||
private readonly ILog backingLog;
|
||||
private readonly string prefix;
|
||||
|
||||
public LogPrefixer(ILog backingLog)
|
||||
{
|
||||
this.backingLog = backingLog;
|
||||
}
|
||||
|
||||
public LogPrefixer(ILog backingLog, string prefix)
|
||||
{
|
||||
this.backingLog = backingLog;
|
||||
this.prefix = prefix;
|
||||
Prefix = prefix;
|
||||
}
|
||||
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
|
||||
public LogFile CreateSubfile(string ext = "log")
|
||||
{
|
||||
return backingLog.CreateSubfile(ext);
|
||||
@@ -18,17 +25,17 @@
|
||||
|
||||
public void Debug(string message = "", int skipFrames = 0)
|
||||
{
|
||||
backingLog.Debug(prefix + message, skipFrames);
|
||||
backingLog.Debug(Prefix + message, skipFrames);
|
||||
}
|
||||
|
||||
public void Error(string message)
|
||||
{
|
||||
backingLog.Error(prefix + message);
|
||||
backingLog.Error(Prefix + message);
|
||||
}
|
||||
|
||||
public void Log(string message)
|
||||
{
|
||||
backingLog.Log(prefix + message);
|
||||
backingLog.Log(Prefix + message);
|
||||
}
|
||||
|
||||
public void AddStringReplace(string from, string to)
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
{
|
||||
public class NullLog : BaseLog
|
||||
{
|
||||
public NullLog() : base(false)
|
||||
{
|
||||
}
|
||||
|
||||
public string FullFilename { get; set; } = "NULL";
|
||||
|
||||
protected override string GetFullName()
|
||||
@@ -15,15 +11,13 @@
|
||||
|
||||
public override void Log(string message)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Debug(string message = "", int skipFrames = 0)
|
||||
{
|
||||
if (IsDebug) base.Log(message);
|
||||
}
|
||||
|
||||
public override void Error(string message)
|
||||
{
|
||||
Console.WriteLine("Error: " + message);
|
||||
base.Error(message);
|
||||
}
|
||||
|
||||
public override void AddStringReplace(string from, string to)
|
||||
|
||||
@@ -16,19 +16,19 @@ namespace Logging
|
||||
this.debug = debug;
|
||||
}
|
||||
|
||||
public static void Measure(ILog log, string name, Action action, bool debug = false)
|
||||
public static TimeSpan Measure(ILog log, string name, Action action, bool debug = false)
|
||||
{
|
||||
var sw = Begin(log, name, debug);
|
||||
action();
|
||||
sw.End();
|
||||
return sw.End();
|
||||
}
|
||||
|
||||
public static T Measure<T>(ILog log, string name, Func<T> action, bool debug = false)
|
||||
public static StopwatchResult<T> Measure<T>(ILog log, string name, Func<T> action, bool debug = false)
|
||||
{
|
||||
var sw = Begin(log, name, debug);
|
||||
var result = action();
|
||||
sw.End();
|
||||
return result;
|
||||
var duration = sw.End();
|
||||
return new StopwatchResult<T>(result, duration);
|
||||
}
|
||||
|
||||
public static Stopwatch Begin(ILog log)
|
||||
@@ -68,4 +68,16 @@ namespace Logging
|
||||
return duration;
|
||||
}
|
||||
}
|
||||
|
||||
public class StopwatchResult<T>
|
||||
{
|
||||
public StopwatchResult(T value, TimeSpan duration)
|
||||
{
|
||||
Value = value;
|
||||
Duration = duration;
|
||||
}
|
||||
|
||||
public T Value { get; }
|
||||
public TimeSpan Duration { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockCache
|
||||
{
|
||||
public delegate void CacheClearedEvent();
|
||||
|
||||
private const int MaxEntries = 1024 * 1024 * 5;
|
||||
private readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
|
||||
|
||||
public event CacheClearedEvent? OnCacheCleared;
|
||||
|
||||
public BlockTimeEntry Add(ulong number, DateTime dateTime)
|
||||
{
|
||||
return Add(new BlockTimeEntry(number, dateTime));
|
||||
}
|
||||
|
||||
public BlockTimeEntry Add(BlockTimeEntry entry)
|
||||
{
|
||||
if (!entries.ContainsKey(entry.BlockNumber))
|
||||
{
|
||||
if (entries.Count > MaxEntries)
|
||||
{
|
||||
entries.Clear();
|
||||
var e = OnCacheCleared;
|
||||
if (e != null) e();
|
||||
}
|
||||
entries.Add(entry.BlockNumber, entry);
|
||||
}
|
||||
|
||||
return entries[entry.BlockNumber];
|
||||
}
|
||||
|
||||
public BlockTimeEntry? Get(ulong number)
|
||||
{
|
||||
if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int Size { get { return entries.Count; } }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockTimeEntry
|
||||
{
|
||||
public BlockTimeEntry(ulong blockNumber, DateTime utc)
|
||||
{
|
||||
BlockNumber = blockNumber;
|
||||
Utc = utc;
|
||||
}
|
||||
|
||||
public ulong BlockNumber { get; }
|
||||
public DateTime Utc { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{BlockNumber}] @ {Utc.ToString("o")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using Logging;
|
||||
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockTimeFinder
|
||||
{
|
||||
private readonly BlockCache cache;
|
||||
private readonly BlockchainBounds bounds;
|
||||
private readonly IWeb3Blocks web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public BlockTimeFinder(BlockCache cache, IWeb3Blocks web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
|
||||
this.cache = cache;
|
||||
bounds = new BlockchainBounds(cache, web3);
|
||||
}
|
||||
|
||||
public BlockTimeEntry Get(ulong blockNumber)
|
||||
{
|
||||
var b = cache.Get(blockNumber);
|
||||
if (b != null) return b;
|
||||
return GetBlock(blockNumber);
|
||||
}
|
||||
|
||||
public ulong? GetHighestBlockNumberBefore(DateTime moment)
|
||||
{
|
||||
bounds.Initialize();
|
||||
if (moment <= bounds.Genesis.Utc) return null;
|
||||
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
|
||||
|
||||
return Log(() => Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector));
|
||||
}
|
||||
|
||||
public ulong? GetLowestBlockNumberAfter(DateTime moment)
|
||||
{
|
||||
bounds.Initialize();
|
||||
if (moment >= bounds.Current.Utc) return null;
|
||||
if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
|
||||
|
||||
return Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ;
|
||||
}
|
||||
|
||||
private ulong Log(Func<ulong> operation)
|
||||
{
|
||||
var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder));
|
||||
var result = operation();
|
||||
sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func<DateTime, BlockTimeEntry, bool> isWhatIwant)
|
||||
{
|
||||
var middle = GetMiddle(lower, upper);
|
||||
if (middle.BlockNumber == lower.BlockNumber)
|
||||
{
|
||||
if (isWhatIwant(target, upper)) return upper.BlockNumber;
|
||||
}
|
||||
|
||||
if (isWhatIwant(target, middle))
|
||||
{
|
||||
return middle.BlockNumber;
|
||||
}
|
||||
|
||||
if (middle.Utc > target)
|
||||
{
|
||||
return Search(lower, middle, target, isWhatIwant);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Search(middle, upper, target, isWhatIwant);
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry GetMiddle(BlockTimeEntry lower, BlockTimeEntry upper)
|
||||
{
|
||||
ulong range = upper.BlockNumber - lower.BlockNumber;
|
||||
ulong number = lower.BlockNumber + range / 2;
|
||||
return GetBlock(number);
|
||||
}
|
||||
|
||||
private bool HighestBeforeSelector(DateTime target, BlockTimeEntry entry)
|
||||
{
|
||||
var next = GetBlock(entry.BlockNumber + 1);
|
||||
return
|
||||
entry.Utc <= target &&
|
||||
next.Utc > target;
|
||||
}
|
||||
|
||||
private bool LowestAfterSelector(DateTime target, BlockTimeEntry entry)
|
||||
{
|
||||
var previous = GetBlock(entry.BlockNumber - 1);
|
||||
return
|
||||
entry.Utc >= target &&
|
||||
previous.Utc < target;
|
||||
}
|
||||
|
||||
private BlockTimeEntry GetBlock(ulong number)
|
||||
{
|
||||
if (number < bounds.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis.");
|
||||
if (number > bounds.Current.BlockNumber) throw new Exception("Can't fetch block after current.");
|
||||
|
||||
var dateTime = web3.GetTimestampForBlock(number);
|
||||
if (dateTime == null) throw new Exception("Failed to get dateTime for block that should exist.");
|
||||
return cache.Add(number, dateTime.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockchainBounds
|
||||
{
|
||||
private readonly BlockCache cache;
|
||||
private readonly IWeb3Blocks web3;
|
||||
|
||||
public BlockTimeEntry Genesis { get; private set; } = null!;
|
||||
public BlockTimeEntry Current { get; private set; } = null!;
|
||||
|
||||
public BlockchainBounds(BlockCache cache, IWeb3Blocks web3)
|
||||
{
|
||||
this.cache = cache;
|
||||
this.web3 = web3;
|
||||
|
||||
cache.OnCacheCleared += Initialize;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
AddCurrentBlock();
|
||||
LookForGenesisBlock();
|
||||
|
||||
if (Current.BlockNumber == Genesis.BlockNumber)
|
||||
{
|
||||
throw new Exception("Unsupported condition: Current block is genesis block.");
|
||||
}
|
||||
}
|
||||
|
||||
private void LookForGenesisBlock()
|
||||
{
|
||||
if (Genesis != null)
|
||||
{
|
||||
cache.Add(Genesis);
|
||||
return;
|
||||
}
|
||||
|
||||
var blockTime = web3.GetTimestampForBlock(0);
|
||||
if (blockTime != null)
|
||||
{
|
||||
AddGenesisBlock(0, blockTime.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
LookForGenesisBlock(0, Current);
|
||||
}
|
||||
|
||||
private void LookForGenesisBlock(ulong lower, BlockTimeEntry upper)
|
||||
{
|
||||
if (Genesis != null) return;
|
||||
|
||||
var range = upper.BlockNumber - lower;
|
||||
if (range == 1)
|
||||
{
|
||||
var lowTime = web3.GetTimestampForBlock(lower);
|
||||
if (lowTime != null)
|
||||
{
|
||||
AddGenesisBlock(lower, lowTime.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddGenesisBlock(upper);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var current = lower + range / 2;
|
||||
|
||||
var blockTime = web3.GetTimestampForBlock(current);
|
||||
if (blockTime != null)
|
||||
{
|
||||
var newUpper = cache.Add(current, blockTime.Value);
|
||||
LookForGenesisBlock(lower, newUpper);
|
||||
}
|
||||
else
|
||||
{
|
||||
LookForGenesisBlock(current, upper);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCurrentBlock()
|
||||
{
|
||||
var currentBlockNumber = web3.GetCurrentBlockNumber();
|
||||
var blockTime = web3.GetTimestampForBlock(currentBlockNumber);
|
||||
if (blockTime == null) throw new Exception("Unable to get dateTime for current block.");
|
||||
AddCurrentBlock(currentBlockNumber, blockTime.Value);
|
||||
}
|
||||
|
||||
private void AddCurrentBlock(ulong currentBlockNumber, DateTime dateTime)
|
||||
{
|
||||
Current = new BlockTimeEntry(currentBlockNumber, dateTime);
|
||||
cache.Add(Current);
|
||||
}
|
||||
|
||||
private void AddGenesisBlock(ulong number, DateTime dateTime)
|
||||
{
|
||||
AddGenesisBlock(new BlockTimeEntry(number, dateTime));
|
||||
}
|
||||
|
||||
private void AddGenesisBlock(BlockTimeEntry entry)
|
||||
{
|
||||
Genesis = entry;
|
||||
cache.Add(Genesis);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public class NethereumInteraction
|
||||
{
|
||||
// BlockCache is a static instance: It stays alive for the duration of the application runtime.
|
||||
private readonly static BlockCache blockCache = new BlockCache();
|
||||
|
||||
private readonly ILog log;
|
||||
private readonly Web3 web3;
|
||||
|
||||
@@ -17,34 +22,47 @@ namespace NethereumWorkflow
|
||||
this.web3 = web3;
|
||||
}
|
||||
|
||||
public void SendEth(string toAddress, decimal ethAmount)
|
||||
public string SendEth(string toAddress, decimal ethAmount)
|
||||
{
|
||||
log.Debug();
|
||||
var receipt = Time.Wait(web3.Eth.GetEtherTransferService().TransferEtherAndWaitForReceiptAsync(toAddress, ethAmount));
|
||||
if (!receipt.Succeeded()) throw new Exception("Unable to send Eth");
|
||||
return receipt.TransactionHash;
|
||||
}
|
||||
|
||||
public decimal GetEthBalance()
|
||||
{
|
||||
log.Debug();
|
||||
return GetEthBalance(web3.TransactionManager.Account.Address);
|
||||
}
|
||||
|
||||
public decimal GetEthBalance(string address)
|
||||
{
|
||||
log.Debug();
|
||||
var balance = Time.Wait(web3.Eth.GetBalance.SendRequestAsync(address));
|
||||
return Web3.Convert.FromWei(balance.Value);
|
||||
}
|
||||
|
||||
public TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
|
||||
{
|
||||
log.Debug(typeof(TFunction).ToString());
|
||||
var handler = web3.Eth.GetContractQueryHandler<TFunction>();
|
||||
return Time.Wait(handler.QueryAsync<TResult>(contractAddress, function));
|
||||
}
|
||||
|
||||
public void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
|
||||
public string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
|
||||
{
|
||||
log.Debug();
|
||||
var handler = web3.Eth.GetContractTransactionHandler<TFunction>();
|
||||
var receipt = Time.Wait(handler.SendRequestAndWaitForReceiptAsync(contractAddress, function));
|
||||
if (!receipt.Succeeded()) throw new Exception("Unable to perform contract transaction.");
|
||||
return receipt.TransactionHash;
|
||||
}
|
||||
|
||||
public Transaction GetTransaction(string transactionHash)
|
||||
{
|
||||
log.Debug();
|
||||
return Time.Wait(web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash));
|
||||
}
|
||||
|
||||
public decimal? GetSyncedBlockNumber()
|
||||
@@ -70,5 +88,45 @@ namespace NethereumWorkflow
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, BlockInterval blockRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
return GetEvents<TEvent>(address, blockRange.From, blockRange.To);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
|
||||
{
|
||||
var eventHandler = web3.Eth.GetEvent<TEvent>(address);
|
||||
var from = new BlockParameter(fromBlockNumber);
|
||||
var to = new BlockParameter(toBlockNumber);
|
||||
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
|
||||
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
|
||||
}
|
||||
|
||||
public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange)
|
||||
{
|
||||
var wrapper = new Web3Wrapper(web3, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
|
||||
|
||||
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
|
||||
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
|
||||
|
||||
if (fromBlock == null || toBlock == null)
|
||||
{
|
||||
throw new Exception("Failed to convert time range to block range.");
|
||||
}
|
||||
|
||||
return new BlockInterval(
|
||||
from: fromBlock.Value,
|
||||
to: toBlock.Value
|
||||
);
|
||||
}
|
||||
|
||||
public BlockTimeEntry GetBlockForNumber(ulong number)
|
||||
{
|
||||
var wrapper = new Web3Wrapper(web3, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
|
||||
return blockTimeFinder.Get(number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace NethereumWorkflow
|
||||
|
||||
public NethereumInteraction CreateWorkflow()
|
||||
{
|
||||
log.Debug("Starting interaction to " + ip + ":" + port);
|
||||
return new NethereumInteraction(log, CreateWeb3());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using Logging;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public interface IWeb3Blocks
|
||||
{
|
||||
ulong GetCurrentBlockNumber();
|
||||
DateTime? GetTimestampForBlock(ulong blockNumber);
|
||||
}
|
||||
|
||||
public class Web3Wrapper : IWeb3Blocks
|
||||
{
|
||||
private readonly Web3 web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public Web3Wrapper(Web3 web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public ulong GetCurrentBlockNumber()
|
||||
{
|
||||
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
|
||||
return Convert.ToUInt64(number.ToDecimal());
|
||||
}
|
||||
|
||||
public DateTime? GetTimestampForBlock(ulong blockNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
|
||||
if (block == null) return null;
|
||||
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error("Exception while getting timestamp for block: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,5 +10,15 @@
|
||||
|
||||
public string Host { get; }
|
||||
public int Port { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Host}:{Port}";
|
||||
}
|
||||
|
||||
public bool IsValid()
|
||||
{
|
||||
return !string.IsNullOrEmpty(Host) && Port > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
namespace Utils
|
||||
{
|
||||
public class BlockInterval
|
||||
{
|
||||
public BlockInterval(ulong from, ulong to)
|
||||
{
|
||||
if (from < to)
|
||||
{
|
||||
From = from;
|
||||
To = to;
|
||||
}
|
||||
else
|
||||
{
|
||||
From = to;
|
||||
To = from;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong From { get; }
|
||||
public ulong To { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{From} - {To}]";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@
|
||||
{
|
||||
public class ByteSize
|
||||
{
|
||||
public static readonly ByteSize Zero = new ByteSize(0);
|
||||
public const double DefaultSecondsPerMB = 10.0;
|
||||
|
||||
public ByteSize(long sizeInBytes)
|
||||
{
|
||||
if (sizeInBytes < 0) throw new ArgumentException("Cannot create ByteSize object with size less than 0. Was: " + sizeInBytes);
|
||||
@@ -10,7 +13,6 @@
|
||||
|
||||
public long SizeInBytes { get; }
|
||||
|
||||
public const double DefaultSecondsPerMB = 10.0;
|
||||
|
||||
public long ToMB()
|
||||
{
|
||||
@@ -46,6 +48,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
public class BytesPerSecond : ByteSize
|
||||
{
|
||||
public BytesPerSecond(long sizeInBytes) : base(sizeInBytes)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return base.ToString() + "/s";
|
||||
}
|
||||
}
|
||||
|
||||
public static class ByteSizeIntExtensions
|
||||
{
|
||||
private const long Kilo = 1024;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Utils
|
||||
using System.Globalization;
|
||||
|
||||
namespace Utils
|
||||
{
|
||||
public static class Formatter
|
||||
{
|
||||
@@ -10,7 +12,7 @@
|
||||
|
||||
var sizeOrder = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
|
||||
var digit = Math.Round(bytes / Math.Pow(1024, sizeOrder), 1);
|
||||
return digit.ToString() + sizeSuffixes[sizeOrder];
|
||||
return digit.ToString(CultureInfo.InvariantCulture) + sizeSuffixes[sizeOrder];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public class NumberSource
|
||||
{
|
||||
private readonly object @lock = new object();
|
||||
private int number;
|
||||
|
||||
public NumberSource(int start)
|
||||
@@ -11,8 +12,12 @@
|
||||
|
||||
public int GetNextNumber()
|
||||
{
|
||||
var n = number;
|
||||
number++;
|
||||
var n = -1;
|
||||
lock (@lock)
|
||||
{
|
||||
n = number;
|
||||
number++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
namespace ContinuousTests
|
||||
namespace Utils
|
||||
{
|
||||
public class TaskFactory
|
||||
{
|
||||
private readonly object taskLock = new();
|
||||
private readonly List<Task> activeTasks = new List<Task>();
|
||||
|
||||
public void Run(Action action)
|
||||
public void Run(Action action, string name)
|
||||
{
|
||||
lock (taskLock)
|
||||
{
|
||||
activeTasks.Add(Task.Run(action).ContinueWith(CleanupTask, null));
|
||||
activeTasks.Add(Task.Run(() => CatchException(action, name)).ContinueWith(CleanupTask, null));
|
||||
}
|
||||
}
|
||||
|
||||
private void CatchException(Action action, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Exception in task '{name}': " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
+60
-17
@@ -13,6 +13,11 @@
|
||||
return task.Result;
|
||||
}
|
||||
|
||||
public static void Wait(Task task)
|
||||
{
|
||||
task.Wait();
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan d)
|
||||
{
|
||||
var result = "";
|
||||
@@ -23,56 +28,90 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void WaitUntil(Func<bool> predicate)
|
||||
public static TimeSpan ParseTimespan(string span)
|
||||
{
|
||||
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1));
|
||||
span = span.Replace(" ", "").Replace(",", "");
|
||||
var result = TimeSpan.Zero;
|
||||
var number = "";
|
||||
foreach (var c in span)
|
||||
{
|
||||
if (char.IsNumber(c)) number += c;
|
||||
else
|
||||
{
|
||||
var value = Convert.ToInt32(number);
|
||||
number = "";
|
||||
|
||||
if (c == 'd') result += TimeSpan.FromDays(value);
|
||||
else if (c == 'h') result += TimeSpan.FromHours(value);
|
||||
else if (c == 'm') result += TimeSpan.FromMinutes(value);
|
||||
else if (c == 's') result += TimeSpan.FromSeconds(value);
|
||||
else throw new Exception("Unknown time modifier: " + c);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(number))
|
||||
{
|
||||
var value = Convert.ToInt32(number);
|
||||
result += TimeSpan.FromSeconds(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay)
|
||||
public static void WaitUntil(Func<bool> predicate, string msg)
|
||||
{
|
||||
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1), msg);
|
||||
}
|
||||
|
||||
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay, string msg)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var tries = 1;
|
||||
var state = predicate();
|
||||
while (!state)
|
||||
{
|
||||
if (DateTime.UtcNow - start > timeout)
|
||||
var duration = DateTime.UtcNow - start;
|
||||
if (duration > timeout)
|
||||
{
|
||||
throw new TimeoutException("Operation timed out.");
|
||||
throw new TimeoutException($"Operation timed out after {tries} tries over (total) {FormatDuration(duration)}. '{msg}'");
|
||||
}
|
||||
|
||||
Sleep(retryDelay);
|
||||
state = predicate();
|
||||
tries++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Retry(Action action, string description)
|
||||
{
|
||||
Retry(action, TimeSpan.FromMinutes(1), description);
|
||||
Retry(action, 1, description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, string description)
|
||||
{
|
||||
return Retry(action, TimeSpan.FromMinutes(1), description);
|
||||
return Retry(action, 1, description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, TimeSpan timeout, string description)
|
||||
public static void Retry(Action action, int maxRetries, string description)
|
||||
{
|
||||
Retry(action, timeout, TimeSpan.FromSeconds(1), description);
|
||||
Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, TimeSpan timeout, string description)
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, string description)
|
||||
{
|
||||
return Retry(action, timeout, TimeSpan.FromSeconds(1), description);
|
||||
return Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, TimeSpan timeout, TimeSpan retryTime, string description)
|
||||
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var retries = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.UtcNow - start > timeout)
|
||||
if (retries > maxRetries)
|
||||
{
|
||||
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
|
||||
var duration = DateTime.UtcNow - start;
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -83,21 +122,24 @@
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
retries++;
|
||||
}
|
||||
|
||||
Sleep(retryTime);
|
||||
}
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, TimeSpan timeout, TimeSpan retryTime, string description)
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, TimeSpan retryTime, string description)
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
var retries = 0;
|
||||
var exceptions = new List<Exception>();
|
||||
while (true)
|
||||
{
|
||||
if (DateTime.UtcNow - start > timeout)
|
||||
if (retries > maxRetries)
|
||||
{
|
||||
throw new TimeoutException($"Retry '{description}' of {timeout.TotalSeconds} seconds timed out.", new AggregateException(exceptions));
|
||||
var duration = DateTime.UtcNow - start;
|
||||
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
|
||||
}
|
||||
|
||||
try
|
||||
@@ -107,6 +149,7 @@
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(ex);
|
||||
retries++;
|
||||
}
|
||||
|
||||
Sleep(retryTime);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Utils
|
||||
{
|
||||
public class TimeRange
|
||||
{
|
||||
public TimeRange(DateTime from, DateTime to)
|
||||
{
|
||||
if (from < to)
|
||||
{
|
||||
From = from;
|
||||
To = to;
|
||||
}
|
||||
else
|
||||
{
|
||||
From = to;
|
||||
To = from;
|
||||
}
|
||||
Duration = To - From;
|
||||
}
|
||||
|
||||
public DateTime From { get; }
|
||||
public DateTime To { get; }
|
||||
public TimeSpan Duration { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
using GethPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.ABI;
|
||||
using Nethereum.Hex.HexTypes;
|
||||
using Nethereum.Util;
|
||||
using NethereumWorkflow;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
@@ -7,45 +16,174 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
CodexContractsDeployment Deployment { get; }
|
||||
|
||||
void MintTestTokens(IGethNode gethNode, IHasEthAddress owner, TestToken testTokens);
|
||||
void MintTestTokens(IGethNode gethNode, EthAddress ethAddress, TestToken testTokens);
|
||||
TestToken GetTestTokenBalance(IGethNode gethNode, IHasEthAddress owner);
|
||||
TestToken GetTestTokenBalance(IGethNode gethNode, EthAddress ethAddress);
|
||||
bool IsDeployed();
|
||||
string MintTestTokens(IHasEthAddress owner, TestToken testTokens);
|
||||
string MintTestTokens(EthAddress ethAddress, TestToken testTokens);
|
||||
TestToken GetTestTokenBalance(IHasEthAddress owner);
|
||||
TestToken GetTestTokenBalance(EthAddress ethAddress);
|
||||
|
||||
Request[] GetStorageRequests(BlockInterval blockRange);
|
||||
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
|
||||
RequestState GetRequestState(Request request);
|
||||
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange);
|
||||
RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange);
|
||||
SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange);
|
||||
SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange);
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
public enum RequestState
|
||||
{
|
||||
New,
|
||||
Started,
|
||||
Cancelled,
|
||||
Finished,
|
||||
Failed
|
||||
}
|
||||
|
||||
public class CodexContractsAccess : ICodexContracts
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly IGethNode gethNode;
|
||||
|
||||
public CodexContractsAccess(ILog log, CodexContractsDeployment deployment)
|
||||
public CodexContractsAccess(ILog log, IGethNode gethNode, CodexContractsDeployment deployment)
|
||||
{
|
||||
this.log = log;
|
||||
this.gethNode = gethNode;
|
||||
Deployment = deployment;
|
||||
}
|
||||
|
||||
public CodexContractsDeployment Deployment { get; }
|
||||
|
||||
public void MintTestTokens(IGethNode gethNode, IHasEthAddress owner, TestToken testTokens)
|
||||
public bool IsDeployed()
|
||||
{
|
||||
MintTestTokens(gethNode, owner.EthAddress, testTokens);
|
||||
return !string.IsNullOrEmpty(StartInteraction().GetTokenName(Deployment.TokenAddress));
|
||||
}
|
||||
|
||||
public void MintTestTokens(IGethNode gethNode, EthAddress ethAddress, TestToken testTokens)
|
||||
public string MintTestTokens(IHasEthAddress owner, TestToken testTokens)
|
||||
{
|
||||
var interaction = new ContractInteractions(log, gethNode);
|
||||
interaction.MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
|
||||
return MintTestTokens(owner.EthAddress, testTokens);
|
||||
}
|
||||
|
||||
public TestToken GetTestTokenBalance(IGethNode gethNode, IHasEthAddress owner)
|
||||
public string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
|
||||
{
|
||||
return GetTestTokenBalance(gethNode, owner.EthAddress);
|
||||
return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
|
||||
}
|
||||
|
||||
public TestToken GetTestTokenBalance(IGethNode gethNode, EthAddress ethAddress)
|
||||
public TestToken GetTestTokenBalance(IHasEthAddress owner)
|
||||
{
|
||||
var interaction = new ContractInteractions(log, gethNode);
|
||||
var balance = interaction.GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
return GetTestTokenBalance(owner.EthAddress);
|
||||
}
|
||||
|
||||
public TestToken GetTestTokenBalance(EthAddress ethAddress)
|
||||
{
|
||||
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
return balance.TestTokens();
|
||||
}
|
||||
|
||||
public Request[] GetStorageRequests(BlockInterval blockRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange);
|
||||
var i = StartInteraction();
|
||||
return events
|
||||
.Select(e =>
|
||||
{
|
||||
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
|
||||
var result = requestEvent.ReturnValue1;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
result.RequestId = e.Event.RequestId;
|
||||
return result;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, blockRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, blockRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
|
||||
{
|
||||
var encoder = new ABIEncode();
|
||||
var encoded = encoder.GetABIEncoded(
|
||||
new ABIValue("bytes32", storageRequest.RequestId),
|
||||
new ABIValue("uint256", slotIndex.ToBig())
|
||||
);
|
||||
|
||||
var hashed = Sha3Keccack.Current.CalculateHash(encoded);
|
||||
|
||||
var func = new GetHostFunction
|
||||
{
|
||||
SlotId = hashed
|
||||
};
|
||||
var address = gethNode.Call<GetHostFunction, string>(Deployment.MarketplaceAddress, func);
|
||||
if (string.IsNullOrEmpty(address)) return null;
|
||||
return new EthAddress(address);
|
||||
}
|
||||
|
||||
public RequestState GetRequestState(Request request)
|
||||
{
|
||||
var func = new RequestStateFunction
|
||||
{
|
||||
RequestId = request.RequestId
|
||||
};
|
||||
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
|
||||
}
|
||||
|
||||
private BlockTimeEntry GetBlock(ulong number)
|
||||
{
|
||||
return gethNode.GetBlockForNumber(number);
|
||||
}
|
||||
|
||||
private EthAddress GetEthAddressFromTransaction(string transactionHash)
|
||||
{
|
||||
var transaction = gethNode.GetTransaction(transactionHash);
|
||||
return new EthAddress(transaction.From);
|
||||
}
|
||||
|
||||
private ContractInteractions StartInteraction()
|
||||
{
|
||||
return new ContractInteractions(log, gethNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using KubernetesWorkflow;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using Logging;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
@@ -16,10 +19,11 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
var config = startupConfig.Get<CodexContractsContainerConfig>();
|
||||
|
||||
var ip = config.GethNode.StartResult.Container.Pod.PodInfo.Ip;
|
||||
var port = config.GethNode.StartResult.HttpPort.Number;
|
||||
var address = config.GethNode.StartResult.Container.GetAddress(new NullLog(), GethContainerRecipe.HttpPortTag);
|
||||
|
||||
AddEnvVar("DISTTEST_NETWORK_URL", $"http://{ip}:{port}");
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
|
||||
AddEnvVar("DISTTEST_NETWORK_URL", address.ToString());
|
||||
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
|
||||
AddEnvVar("KEEP_ALIVE", "1");
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ namespace CodexContractsPlugin
|
||||
return starter.Deploy(ci, gethNode);
|
||||
}
|
||||
|
||||
public ICodexContracts WrapDeploy(CodexContractsDeployment deployment)
|
||||
public ICodexContracts WrapDeploy(IGethNode gethNode, CodexContractsDeployment deployment)
|
||||
{
|
||||
deployment = SerializeGate.Gate(deployment);
|
||||
return starter.Wrap(deployment);
|
||||
return starter.Wrap(gethNode, deployment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
@@ -23,7 +24,7 @@ namespace CodexContractsPlugin
|
||||
var startupConfig = CreateStartupConfig(gethNode);
|
||||
startupConfig.NameOverride = "codex-contracts";
|
||||
|
||||
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig);
|
||||
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig).WaitForOnline();
|
||||
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
|
||||
var container = containers.Containers[0];
|
||||
|
||||
@@ -31,20 +32,23 @@ namespace CodexContractsPlugin
|
||||
|
||||
try
|
||||
{
|
||||
return DeployContract(container, workflow, gethNode);
|
||||
var result = DeployContract(container, workflow, gethNode);
|
||||
workflow.Stop(containers, waitTillStopped: false);
|
||||
Log("Container stopped.");
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Failed to deploy contract.");
|
||||
Log("Failed to deploy contract: " + ex);
|
||||
Log("Downloading Codex SmartContracts container log...");
|
||||
ci.DownloadLog(container);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public ICodexContracts Wrap(CodexContractsDeployment deployment)
|
||||
public ICodexContracts Wrap(IGethNode gethNode, CodexContractsDeployment deployment)
|
||||
{
|
||||
return new CodexContractsAccess(tools.GetLog(), deployment);
|
||||
return new CodexContractsAccess(tools.GetLog(), gethNode, deployment);
|
||||
}
|
||||
|
||||
private CodexContractsDeployment DeployContract(RunningContainer container, IStartupWorkflow workflow, IGethNode gethNode)
|
||||
@@ -55,7 +59,7 @@ namespace CodexContractsPlugin
|
||||
var logHandler = new ContractsReadyLogHandler(tools.GetLog());
|
||||
workflow.DownloadContainerLog(container, logHandler, 100);
|
||||
return logHandler.Found;
|
||||
});
|
||||
}, nameof(DeployContract));
|
||||
Log("Contracts deployed. Extracting addresses...");
|
||||
|
||||
var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container);
|
||||
@@ -67,7 +71,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
Log("Extract completed. Checking sync...");
|
||||
|
||||
Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi));
|
||||
Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi), nameof(DeployContract));
|
||||
|
||||
Log("Synced. Codex SmartContracts deployed.");
|
||||
|
||||
@@ -79,9 +83,9 @@ namespace CodexContractsPlugin
|
||||
tools.GetLog().Log(msg);
|
||||
}
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
private void WaitUntil(Func<bool> predicate, string msg)
|
||||
{
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2));
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2), msg);
|
||||
}
|
||||
|
||||
private StartupConfig CreateStartupConfig(IGethNode gethNode)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using GethPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.Hex.HexConvertors.Extensions;
|
||||
using NethereumWorkflow;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -26,9 +28,26 @@ namespace CodexContractsPlugin
|
||||
return gethNode.Call<GetTokenFunction, string>(marketplaceAddress, function);
|
||||
}
|
||||
|
||||
public void MintTestTokens(EthAddress address, decimal amount, string tokenAddress)
|
||||
public string GetTokenName(string tokenAddress)
|
||||
{
|
||||
MintTokens(address.Address, amount, tokenAddress);
|
||||
try
|
||||
{
|
||||
log.Debug(tokenAddress);
|
||||
var function = new GetTokenNameFunction();
|
||||
|
||||
return gethNode.Call<GetTokenNameFunction, string>(tokenAddress, function);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Log("Failed to get token name: " + ex);
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public string MintTestTokens(EthAddress address, decimal amount, string tokenAddress)
|
||||
{
|
||||
log.Debug($"{amount} -> {address} (token: {tokenAddress})");
|
||||
return MintTokens(address.Address, amount, tokenAddress);
|
||||
}
|
||||
|
||||
public decimal GetBalance(string tokenAddress, string account)
|
||||
@@ -42,8 +61,20 @@ namespace CodexContractsPlugin
|
||||
return gethNode.Call<GetTokenBalanceFunction, BigInteger>(tokenAddress, function).ToDecimal();
|
||||
}
|
||||
|
||||
public GetRequestOutputDTO GetRequest(string marketplaceAddress, byte[] requestId)
|
||||
{
|
||||
|
||||
log.Debug($"({marketplaceAddress}) {requestId.ToHex(true)}");
|
||||
var func = new GetRequestFunction
|
||||
{
|
||||
RequestId = requestId
|
||||
};
|
||||
return gethNode.Call<GetRequestFunction, GetRequestOutputDTO>(marketplaceAddress, func);
|
||||
}
|
||||
|
||||
public bool IsSynced(string marketplaceAddress, string marketplaceAbi)
|
||||
{
|
||||
log.Debug();
|
||||
try
|
||||
{
|
||||
return IsBlockNumberOK() && IsContractAvailable(marketplaceAddress, marketplaceAbi);
|
||||
@@ -54,7 +85,7 @@ namespace CodexContractsPlugin
|
||||
}
|
||||
}
|
||||
|
||||
private void MintTokens(string account, decimal amount, string tokenAddress)
|
||||
private string MintTokens(string account, decimal amount, string tokenAddress)
|
||||
{
|
||||
log.Debug($"({tokenAddress}) {amount} --> {account}");
|
||||
if (string.IsNullOrEmpty(account)) throw new ArgumentException("Invalid arguments for MintTestTokens");
|
||||
@@ -65,7 +96,7 @@ namespace CodexContractsPlugin
|
||||
Amount = amount.ToBig()
|
||||
};
|
||||
|
||||
gethNode.SendTransaction(tokenAddress, function);
|
||||
return gethNode.SendTransaction(tokenAddress, function);
|
||||
}
|
||||
|
||||
private bool IsBlockNumberOK()
|
||||
@@ -85,6 +116,11 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
}
|
||||
|
||||
[Function("name", "string")]
|
||||
public class GetTokenNameFunction : FunctionMessage
|
||||
{
|
||||
}
|
||||
|
||||
[Function("mint")]
|
||||
public class MintTokensFunction : FunctionMessage
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using KubernetesWorkflow;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -25,6 +27,7 @@ namespace CodexContractsPlugin
|
||||
var marketplaceAddress = Retry(FetchMarketplaceAddress);
|
||||
if (string.IsNullOrEmpty(marketplaceAddress)) throw new InvalidOperationException("Unable to fetch marketplace account from codex-contracts node. Test infra failure.");
|
||||
|
||||
log.Debug("Got MarketplaceAddress: " + marketplaceAddress);
|
||||
return marketplaceAddress;
|
||||
}
|
||||
|
||||
@@ -34,6 +37,7 @@ namespace CodexContractsPlugin
|
||||
var marketplaceAbi = Retry(FetchMarketplaceAbi);
|
||||
if (string.IsNullOrEmpty(marketplaceAbi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
|
||||
|
||||
log.Debug("Got Marketplace ABI: " + marketplaceAbi);
|
||||
return marketplaceAbi;
|
||||
}
|
||||
|
||||
@@ -50,7 +54,18 @@ namespace CodexContractsPlugin
|
||||
|
||||
var artifact = JObject.Parse(json);
|
||||
var abi = artifact["abi"];
|
||||
return abi!.ToString(Formatting.None);
|
||||
var byteCode = artifact["bytecode"];
|
||||
var abiResult = abi!.ToString(Formatting.None);
|
||||
var byteCodeResult = byteCode!.ToString(Formatting.None);
|
||||
|
||||
if (byteCodeResult
|
||||
.ToLowerInvariant()
|
||||
.Replace("\"", "") != MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant())
|
||||
{
|
||||
throw new Exception("BYTECODE in CodexContractsPlugin does not match BYTECODE deployed by container. Update Marketplace.cs generated code?");
|
||||
}
|
||||
|
||||
return abiResult;
|
||||
}
|
||||
|
||||
private static string Retry(Func<string> fetch)
|
||||
|
||||
@@ -10,15 +10,15 @@ namespace CodexContractsPlugin
|
||||
return Plugin(ci).DeployContracts(ci, gethNode);
|
||||
}
|
||||
|
||||
public static ICodexContracts WrapCodexContractsDeployment(this CoreInterface ci, CodexContractsDeployment deployment)
|
||||
public static ICodexContracts WrapCodexContractsDeployment(this CoreInterface ci, IGethNode gethNode, CodexContractsDeployment deployment)
|
||||
{
|
||||
return Plugin(ci).WrapDeploy(deployment);
|
||||
return Plugin(ci).WrapDeploy(gethNode, deployment);
|
||||
}
|
||||
|
||||
public static ICodexContracts StartCodexContracts(this CoreInterface ci, IGethNode gethNode)
|
||||
{
|
||||
var deployment = DeployCodexContracts(ci, gethNode);
|
||||
return WrapCodexContractsDeployment(ci, deployment);
|
||||
return WrapCodexContractsDeployment(ci, gethNode, deployment);
|
||||
}
|
||||
|
||||
private static CodexContractsPlugin Plugin(CoreInterface ci)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
using GethPlugin;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CodexContractsPlugin.Marketplace
|
||||
{
|
||||
public partial class Request : RequestBase
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
public byte[] RequestId { get; set; }
|
||||
|
||||
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
|
||||
}
|
||||
|
||||
public partial class RequestFulfilledEventDTO
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestCancelledEventDTO
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
public EthAddress Host { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFreedEventDTO
|
||||
{
|
||||
[JsonIgnore]
|
||||
public BlockTimeEntry Block { get; set; }
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
This code was generated using the Nethereum code generator, here: http://playground.nethereum.com
|
||||
@@ -0,0 +1,59 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
|
||||
{
|
||||
private readonly IPluginTools tools;
|
||||
|
||||
public CodexDiscordBotPlugin(IPluginTools tools)
|
||||
{
|
||||
this.tools = tools;
|
||||
}
|
||||
|
||||
public string LogPrefix => "(DiscordBot) ";
|
||||
|
||||
public void Announce()
|
||||
{
|
||||
tools.GetLog().Log($"Codex DiscordBot (BiblioTech) loaded.");
|
||||
}
|
||||
|
||||
public void AddMetadata(IAddMetadata metadata)
|
||||
{
|
||||
metadata.Add("codexdiscordbotid", new DiscordBotContainerRecipe().Image);
|
||||
}
|
||||
|
||||
public void Decommission()
|
||||
{
|
||||
}
|
||||
|
||||
public RunningPod Deploy(DiscordBotStartupConfig config)
|
||||
{
|
||||
var workflow = tools.CreateWorkflow();
|
||||
return StartContainer(workflow, config);
|
||||
}
|
||||
|
||||
public RunningPod DeployRewarder(RewarderBotStartupConfig config)
|
||||
{
|
||||
var workflow = tools.CreateWorkflow();
|
||||
return StartRewarderContainer(workflow, config);
|
||||
}
|
||||
|
||||
private RunningPod StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.NameOverride = config.Name;
|
||||
startupConfig.Add(config);
|
||||
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
|
||||
}
|
||||
|
||||
private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.Add(config);
|
||||
return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
|
||||
<ProjectReference Include="..\CodexPlugin\CodexPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public static class CoreInterfaceExtensions
|
||||
{
|
||||
public static RunningPod DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
|
||||
{
|
||||
return Plugin(ci).Deploy(config);
|
||||
}
|
||||
|
||||
public static RunningPod DeployRewarderBot(this CoreInterface ci, RewarderBotStartupConfig config)
|
||||
{
|
||||
return Plugin(ci).DeployRewarder(config);
|
||||
}
|
||||
|
||||
private static CodexDiscordBotPlugin Plugin(CoreInterface ci)
|
||||
{
|
||||
return ci.GetPlugin<CodexDiscordBotPlugin>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using Utils;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public class DiscordBotContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public override string AppName => "discordbot-bibliotech";
|
||||
public override string Image => "codexstorage/codex-discordbot:sha-8c64352";
|
||||
|
||||
public static string RewardsPort = "bot_rewards_port";
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
var config = startupConfig.Get<DiscordBotStartupConfig>();
|
||||
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
|
||||
AddEnvVar("TOKEN", config.Token);
|
||||
AddEnvVar("SERVERNAME", config.ServerName);
|
||||
AddEnvVar("ADMINROLE", config.AdminRoleName);
|
||||
AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName);
|
||||
AddEnvVar("REWARDSCHANNELNAME", config.RewardChannelName);
|
||||
AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml");
|
||||
AddEnvVar("KUBENAMESPACE", config.KubeNamespace);
|
||||
|
||||
var gethInfo = config.GethInfo;
|
||||
AddEnvVar("GETH_HOST", gethInfo.Host);
|
||||
AddEnvVar("GETH_HTTP_PORT", gethInfo.Port.ToString());
|
||||
AddEnvVar("GETH_PRIVATE_KEY", gethInfo.PrivKey);
|
||||
AddEnvVar("CODEXCONTRACTS_MARKETPLACEADDRESS", gethInfo.MarketplaceAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
|
||||
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
|
||||
|
||||
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
|
||||
|
||||
if (!string.IsNullOrEmpty(config.DataPath))
|
||||
{
|
||||
AddEnvVar("DATAPATH", config.DataPath);
|
||||
AddVolume(config.DataPath, 1.GB());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public class DiscordBotStartupConfig
|
||||
{
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo, string rewardChannelName)
|
||||
{
|
||||
Name = name;
|
||||
Token = token;
|
||||
ServerName = serverName;
|
||||
AdminRoleName = adminRoleName;
|
||||
AdminChannelName = adminChannelName;
|
||||
KubeNamespace = kubeNamespace;
|
||||
GethInfo = gethInfo;
|
||||
RewardChannelName = rewardChannelName;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string Token { get; }
|
||||
public string ServerName { get; }
|
||||
public string AdminRoleName { get; }
|
||||
public string AdminChannelName { get; }
|
||||
public string RewardChannelName { get; }
|
||||
public string KubeNamespace { get; }
|
||||
public DiscordBotGethInfo GethInfo { get; }
|
||||
public string? DataPath { get; set; }
|
||||
}
|
||||
|
||||
public class RewarderBotStartupConfig
|
||||
{
|
||||
public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
|
||||
{
|
||||
DiscordBotHost = discordBotHost;
|
||||
DiscordBotPort = discordBotPort;
|
||||
IntervalMinutes = intervalMinutes;
|
||||
HistoryStartUtc = historyStartUtc;
|
||||
GethInfo = gethInfo;
|
||||
DataPath = dataPath;
|
||||
}
|
||||
|
||||
public string DiscordBotHost { get; }
|
||||
public int DiscordBotPort { get; }
|
||||
public string IntervalMinutes { get; }
|
||||
public DateTime HistoryStartUtc { get; }
|
||||
public DiscordBotGethInfo GethInfo { get; }
|
||||
public string? DataPath { get; set; }
|
||||
}
|
||||
|
||||
public class DiscordBotGethInfo
|
||||
{
|
||||
public DiscordBotGethInfo(string host, int port, string privKey, string marketplaceAddress, string tokenAddress, string abi)
|
||||
{
|
||||
Host = host;
|
||||
Port = port;
|
||||
PrivKey = privKey;
|
||||
MarketplaceAddress = marketplaceAddress;
|
||||
TokenAddress = tokenAddress;
|
||||
Abi = abi;
|
||||
}
|
||||
|
||||
public string Host { get; }
|
||||
public int Port { get; }
|
||||
public string PrivKey { get; }
|
||||
public string MarketplaceAddress { get; }
|
||||
public string TokenAddress { get; }
|
||||
public string Abi { get; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user