Compare commits
141
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17564957cb | ||
|
|
64ecf531bd | ||
|
|
ec0f7a6790 | ||
|
|
930c44f037 | ||
|
|
dc33f0655c | ||
|
|
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 |
@@ -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,9 +101,9 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Docker - Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: 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@releases/v4.0.0
|
||||
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@releases/v4.0.0
|
||||
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 "----"
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
.vs
|
||||
obj
|
||||
bin
|
||||
.vscode
|
||||
.vscode
|
||||
Tools/CommitBlamer/CodexTestLogs
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace Core
|
||||
{
|
||||
|
||||
@@ -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,13 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class GiveRewardsCommand
|
||||
{
|
||||
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
|
||||
}
|
||||
|
||||
public class RewardUsersCommand
|
||||
{
|
||||
public ulong RewardId { get; set; }
|
||||
public string[] UserAddresses { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -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!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinSlotSize = 1.GB(),
|
||||
MinDuration = TimeSpan.FromHours(24.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!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinNumberOfHosts = 4,
|
||||
MinSlotSize = 1.GB(),
|
||||
MinDuration = TimeSpan.FromHours(24.0),
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +13,7 @@ namespace KubernetesWorkflow
|
||||
private readonly K8sCluster cluster;
|
||||
private readonly WorkflowNumberSource workflowNumberSource;
|
||||
private readonly K8sClient client;
|
||||
private const string podLabelKey = "pod-uuid";
|
||||
public const string PodLabelKey = "pod-uuid";
|
||||
|
||||
public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
|
||||
{
|
||||
@@ -28,57 +30,53 @@ namespace KubernetesWorkflow
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
public RunningPod BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
|
||||
public StartResult BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
|
||||
{
|
||||
log.Debug();
|
||||
EnsureNamespace();
|
||||
|
||||
var podLabel = K8sNameUtils.Format(Guid.NewGuid().ToString());
|
||||
var deploymentName = CreateDeployment(containerRecipes, location, podLabel);
|
||||
var (serviceName, servicePortsMap) = CreateService(containerRecipes);
|
||||
var deployment = CreateDeployment(containerRecipes, location, podLabel);
|
||||
var internalService = CreateInternalService(containerRecipes);
|
||||
var externalService = CreateExternalService(containerRecipes);
|
||||
|
||||
var pod = FindPodByLabel(podLabel);
|
||||
var podInfo = CreatePodInfo(pod);
|
||||
|
||||
return new RunningPod(cluster, podInfo, deploymentName, serviceName, servicePortsMap.ToArray());
|
||||
return new StartResult(cluster, containerRecipes, deployment, internalService, externalService);
|
||||
}
|
||||
|
||||
private V1Pod FindPodByLabel(string podLabel)
|
||||
public PodInfo GetPodInfo(RunningDeployment deployment)
|
||||
{
|
||||
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
|
||||
foreach (var pod in pods.Items)
|
||||
{
|
||||
var label = pod.GetLabel(podLabelKey);
|
||||
if (label == podLabel)
|
||||
{
|
||||
return pod;
|
||||
}
|
||||
}
|
||||
throw new Exception("Unable to find pod by label.");
|
||||
var pod = GetPodForDeployment(deployment);
|
||||
return CreatePodInfo(pod);
|
||||
}
|
||||
|
||||
public void Stop(RunningPod pod)
|
||||
public void Stop(StartResult startResult)
|
||||
{
|
||||
log.Debug();
|
||||
if (!string.IsNullOrEmpty(pod.ServiceName)) DeleteService(pod.ServiceName);
|
||||
DeleteDeployment(pod.DeploymentName);
|
||||
WaitUntilDeploymentOffline(pod.DeploymentName);
|
||||
WaitUntilPodOffline(pod.PodInfo.Name);
|
||||
if (startResult.InternalService != null) DeleteService(startResult.InternalService);
|
||||
if (startResult.ExternalService != null) DeleteService(startResult.ExternalService);
|
||||
DeleteDeployment(startResult.Deployment);
|
||||
WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
|
||||
}
|
||||
|
||||
public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler, int? tailLines)
|
||||
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines)
|
||||
{
|
||||
log.Debug();
|
||||
using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.PodInfo.Name, K8sNamespace, recipe.Name, tailLines: tailLines));
|
||||
|
||||
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();
|
||||
|
||||
@@ -86,6 +84,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();
|
||||
@@ -315,7 +338,7 @@ namespace KubernetesWorkflow
|
||||
|
||||
#region Deployment management
|
||||
|
||||
private string CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location, string podLabel)
|
||||
private RunningDeployment CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location, string podLabel)
|
||||
{
|
||||
var deploymentSpec = new V1Deployment
|
||||
{
|
||||
@@ -337,6 +360,8 @@ namespace KubernetesWorkflow
|
||||
},
|
||||
Spec = new V1PodSpec
|
||||
{
|
||||
PriorityClassName = GetPriorityClassName(containerRecipes),
|
||||
Affinity = CreatePodAffinity(containerRecipes),
|
||||
NodeSelector = CreateNodeSelector(location),
|
||||
Containers = CreateDeploymentContainers(containerRecipes),
|
||||
Volumes = CreateVolumes(containerRecipes)
|
||||
@@ -348,13 +373,14 @@ 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)
|
||||
@@ -368,12 +394,57 @@ 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();
|
||||
@@ -382,7 +453,7 @@ namespace KubernetesWorkflow
|
||||
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes, string podLabel)
|
||||
{
|
||||
var labels = containerRecipes.First().PodLabels.Clone();
|
||||
labels.Add(podLabelKey, podLabel);
|
||||
labels.Add(PodLabelKey, podLabel);
|
||||
return labels.GetLabels();
|
||||
}
|
||||
|
||||
@@ -609,118 +680,174 @@ namespace KubernetesWorkflow
|
||||
|
||||
private string GetNameForPort(ContainerRecipe recipe, Port port)
|
||||
{
|
||||
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}-{port.Protocol.ToString().ToLowerInvariant()}";
|
||||
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.RunningContainers.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)
|
||||
{
|
||||
foreach (var port in r.ExposedPorts)
|
||||
var recipePorts = r.ExposedPorts.Concat(r.InternalPorts).ToArray();
|
||||
foreach (var port in recipePorts)
|
||||
{
|
||||
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, port.Tag, port.Protocol));
|
||||
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, PortProtocol protocol)
|
||||
private Port MapPortIfAble(V1ServicePort p, string tag, PortProtocol protocol)
|
||||
{
|
||||
if (p.NodePort == null) return null;
|
||||
return new Port(p.NodePort.Value, tag, protocol);
|
||||
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)
|
||||
{
|
||||
if (port.IsTcp()) CreateServicePort(result, recipe, port, "TCP");
|
||||
if (port.IsUdp()) CreateServicePort(result, recipe, port, "UDP");
|
||||
}
|
||||
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)
|
||||
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol, bool isNodePort)
|
||||
{
|
||||
result.Add(new V1ServicePort
|
||||
var p = new V1ServicePort
|
||||
{
|
||||
Name = GetNameForPort(recipe, port),
|
||||
Protocol = protocol,
|
||||
Port = port.Number,
|
||||
TargetPort = GetNameForPort(recipe, port),
|
||||
});
|
||||
TargetPort = GetNameForPort(recipe, port)
|
||||
};
|
||||
|
||||
if (isNodePort) p.NodePort = port.Number;
|
||||
|
||||
result.Add(p);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -751,13 +878,12 @@ namespace KubernetesWorkflow
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -778,7 +904,17 @@ 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 V1Pod[] FindPodsByLabel(string podLabel)
|
||||
{
|
||||
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
|
||||
return pods.Items.Where(p => p.GetLabel(PodLabelKey) == podLabel).ToArray();
|
||||
}
|
||||
|
||||
private PodInfo CreatePodInfo(V1Pod pod)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
namespace KubernetesWorkflow
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
{
|
||||
public interface IK8sHooks
|
||||
{
|
||||
|
||||
@@ -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,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";
|
||||
}
|
||||
}
|
||||
+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>();
|
||||
}
|
||||
+12
-2
@@ -1,13 +1,15 @@
|
||||
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, 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;
|
||||
SetCriticalPriority = setCriticalPriority;
|
||||
ExposedPorts = exposedPorts;
|
||||
InternalPorts = internalPorts;
|
||||
EnvVars = envVars;
|
||||
@@ -32,6 +34,8 @@
|
||||
public int Number { get; }
|
||||
public string? NameOverride { get; }
|
||||
public ContainerResources Resources { get; }
|
||||
public SchedulingAffinity SchedulingAffinity { get; }
|
||||
public bool SetCriticalPriority { get; }
|
||||
public string Image { get; }
|
||||
public Port[] ExposedPorts { get; }
|
||||
public Port[] InternalPorts { get; }
|
||||
@@ -53,6 +57,7 @@
|
||||
$"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}'"))}";
|
||||
}
|
||||
}
|
||||
@@ -64,6 +69,11 @@
|
||||
Number = number;
|
||||
Tag = tag;
|
||||
Protocol = protocol;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Tag))
|
||||
{
|
||||
throw new Exception("A unique port tag is required");
|
||||
}
|
||||
}
|
||||
|
||||
public int Number { get; }
|
||||
+23
-7
@@ -1,6 +1,6 @@
|
||||
using Utils;
|
||||
|
||||
namespace KubernetesWorkflow
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public abstract class ContainerRecipeFactory
|
||||
{
|
||||
@@ -13,6 +13,8 @@ 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 bool setCriticalPriority;
|
||||
|
||||
public ContainerRecipe CreateRecipe(int index, int containerNumber, RecipeComponentFactory factory, StartupConfig config)
|
||||
{
|
||||
@@ -22,7 +24,7 @@ namespace KubernetesWorkflow
|
||||
|
||||
Initialize(config);
|
||||
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources,
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, setCriticalPriority,
|
||||
exposedPorts.ToArray(),
|
||||
internalPorts.ToArray(),
|
||||
envVars.ToArray(),
|
||||
@@ -40,6 +42,8 @@ namespace KubernetesWorkflow
|
||||
additionals.Clear();
|
||||
this.factory = null!;
|
||||
resources = new ContainerResources();
|
||||
schedulingAffinity = new SchedulingAffinity();
|
||||
setCriticalPriority = false;
|
||||
|
||||
return recipe;
|
||||
}
|
||||
@@ -52,17 +56,17 @@ namespace KubernetesWorkflow
|
||||
|
||||
protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
return AddExposedPort(factory.CreatePort(tag, protocol));
|
||||
return AddExposedPort(factory.CreateExternalPort(tag, protocol));
|
||||
}
|
||||
|
||||
protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
return AddExposedPort(factory.CreatePort(number, tag, protocol));
|
||||
return AddExposedPort(factory.CreateExternalPort(number, tag, protocol));
|
||||
}
|
||||
|
||||
protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
|
||||
{
|
||||
var p = factory.CreatePort(tag, protocol);
|
||||
var p = factory.CreateInternalPort(tag, protocol);
|
||||
internalPorts.Add(p);
|
||||
return p;
|
||||
}
|
||||
@@ -121,11 +125,23 @@ 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 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;
|
||||
+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, PortProtocol protocol)
|
||||
{
|
||||
return new Port(number, tag, protocol);
|
||||
}
|
||||
|
||||
public Port CreatePort(string tag, PortProtocol protocol)
|
||||
{
|
||||
return new Port(portNumberSource.GetNextNumber(), 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +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;
|
||||
knownLocation = PingForLocation(container);
|
||||
return knownLocation.Value;
|
||||
DetermineRunnerLocation();
|
||||
if (location == RunnerLocation.Unknown) throw new Exception("Runner location is unknown.");
|
||||
return location;
|
||||
}
|
||||
|
||||
private static RunnerLocation PingForLocation(RunningContainer container)
|
||||
private static void DetermineRunnerLocation()
|
||||
{
|
||||
if (PingHost(container.Pod.PodInfo.Ip))
|
||||
if (location != RunnerLocation.Unknown) return;
|
||||
|
||||
var port = Environment.GetEnvironmentVariable("KUBERNETES_PORT");
|
||||
var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
|
||||
|
||||
if (string.IsNullOrEmpty(port) || string.IsNullOrEmpty(host))
|
||||
{
|
||||
return RunnerLocation.InternalToCluster;
|
||||
location = RunnerLocation.ExternalToCluster;
|
||||
}
|
||||
|
||||
foreach (var port in container.ContainerPorts)
|
||||
else
|
||||
{
|
||||
if (port.ExternalAddress.IsValid() && PingHost(Format(port.ExternalAddress)))
|
||||
{
|
||||
return RunnerLocation.ExternalToCluster;
|
||||
}
|
||||
location = RunnerLocation.InternalToCluster;
|
||||
}
|
||||
|
||||
throw new Exception("Unable to determine location relative to kubernetes cluster.");
|
||||
}
|
||||
|
||||
private static string Format(Address host)
|
||||
{
|
||||
return host.Host
|
||||
.Replace("http://", "")
|
||||
.Replace("https://", "");
|
||||
}
|
||||
|
||||
private static bool PingHost(string host)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var pinger = new Ping();
|
||||
PingReply reply = pinger.Send(host);
|
||||
return reply.Status == IPStatus.Success;
|
||||
}
|
||||
catch (PingException)
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
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, ContainerPort[] containerPorts)
|
||||
{
|
||||
Pod = pod;
|
||||
Recipe = recipe;
|
||||
ServicePorts = servicePorts;
|
||||
Name = name;
|
||||
ContainerPorts = containerPorts;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public RunningPod Pod { get; }
|
||||
public ContainerRecipe Recipe { get; }
|
||||
public Port[] ServicePorts { get; }
|
||||
public ContainerPort[] ContainerPorts { get; }
|
||||
|
||||
public ContainerPort GetContainerPort(string portTag)
|
||||
{
|
||||
return ContainerPorts.Single(c => c.Port.Tag == portTag);
|
||||
}
|
||||
|
||||
public Address GetAddress(string portTag)
|
||||
{
|
||||
var containerPort = GetContainerPort(portTag);
|
||||
if (RunnerLocationUtils.DetermineRunnerLocation(this) == RunnerLocation.InternalToCluster)
|
||||
{
|
||||
return containerPort.InternalAddress;
|
||||
}
|
||||
if (!containerPort.ExternalAddress.IsValid()) throw new Exception($"Getting address by tag {portTag} resulted in an invalid address.");
|
||||
return containerPort.ExternalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public class ContainerPort
|
||||
{
|
||||
public ContainerPort(Port port, Address externalAddress, Address internalAddress)
|
||||
{
|
||||
Port = port;
|
||||
ExternalAddress = externalAddress;
|
||||
InternalAddress = internalAddress;
|
||||
}
|
||||
|
||||
public Port Port { get; }
|
||||
public Address ExternalAddress { get; }
|
||||
public Address InternalAddress { get; }
|
||||
}
|
||||
|
||||
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,54 +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)
|
||||
{
|
||||
return PortMapEntries
|
||||
.Where(p => p.ContainerNumber == containerRecipe.Number)
|
||||
.SelectMany(p => p.Ports)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
if (InternalService != null)
|
||||
{
|
||||
var p = InternalService.GetServicePortForRecipeAndTag(recipe, tag);
|
||||
if (p != null) return p;
|
||||
}
|
||||
|
||||
public class ContainerRecipePortMapEntry
|
||||
{
|
||||
public ContainerRecipePortMapEntry(int containerNumber, Port[] ports)
|
||||
{
|
||||
ContainerNumber = containerNumber;
|
||||
Ports = ports;
|
||||
throw new Exception($"Unable to find internal port by tag '{tag}' for recipe '{recipe.Name}'.");
|
||||
}
|
||||
|
||||
public int ContainerNumber { get; }
|
||||
public Port[] Ports { get; }
|
||||
}
|
||||
|
||||
public class PodInfo
|
||||
{
|
||||
public PodInfo(string name, string ip, string k8sNodeName)
|
||||
public Port GetExternalServicePorts(ContainerRecipe recipe, string tag)
|
||||
{
|
||||
Name = name;
|
||||
Ip = ip;
|
||||
K8SNodeName = k8sNodeName;
|
||||
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 string Name { get; }
|
||||
public string Ip { get; }
|
||||
public string K8SNodeName { get; }
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Logging;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
@@ -9,8 +11,10 @@ namespace KubernetesWorkflow
|
||||
IKnownLocations GetAvailableLocations();
|
||||
RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
|
||||
PodInfo GetPodInfo(RunningContainer container);
|
||||
PodInfo GetPodInfo(RunningContainers containers);
|
||||
CrashWatcher CreateCrashWatcher(RunningContainer container);
|
||||
void Stop(RunningContainers runningContainers);
|
||||
void Stop(RunningContainers containers);
|
||||
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
|
||||
string ExecuteCommand(RunningContainer container, string command, params string[] args);
|
||||
void DeleteNamespace();
|
||||
@@ -50,16 +54,33 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
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 RunningContainers(startupConfig, startResult, containers);
|
||||
cluster.Configuration.Hooks.OnContainersStarted(rc);
|
||||
|
||||
if (startResult.ExternalService != null)
|
||||
{
|
||||
componentFactory.Update(controller);
|
||||
}
|
||||
return rc;
|
||||
});
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningContainer container)
|
||||
{
|
||||
return K8s(c => c.GetPodInfo(container.RunningContainers.StartResult.Deployment));
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo(RunningContainers containers)
|
||||
{
|
||||
return K8s(c => c.GetPodInfo(containers.StartResult.Deployment));
|
||||
}
|
||||
|
||||
public CrashWatcher CreateCrashWatcher(RunningContainer container)
|
||||
{
|
||||
return K8s(c => c.CreateCrashWatcher(container));
|
||||
@@ -69,7 +90,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.Stop(runningContainers.RunningPod);
|
||||
controller.Stop(runningContainers.StartResult);
|
||||
cluster.Configuration.Hooks.OnContainersStopped(runningContainers);
|
||||
});
|
||||
}
|
||||
@@ -78,7 +99,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.DownloadPodLog(container.Pod, container.Recipe, logHandler, tailLines);
|
||||
controller.DownloadPodLog(container, logHandler, tailLines);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,7 +107,7 @@ namespace KubernetesWorkflow
|
||||
{
|
||||
return K8s(controller =>
|
||||
{
|
||||
return controller.ExecuteCommand(container.Pod, container.Recipe.Name, command, args);
|
||||
return controller.ExecuteCommand(container, command, args);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,18 +127,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,
|
||||
CreateContainerPorts(runningPod, r, servicePorts));
|
||||
return new RunningContainer(name, r, addresses);
|
||||
|
||||
}).ToArray();
|
||||
}
|
||||
@@ -135,43 +154,39 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
}
|
||||
|
||||
private ContainerPort[] CreateContainerPorts(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts)
|
||||
private ContainerAddress[] CreateContainerAddresses(StartResult startResult, ContainerRecipe recipe)
|
||||
{
|
||||
var result = new List<ContainerPort>();
|
||||
var result = new List<ContainerAddress>();
|
||||
foreach (var exposedPort in recipe.ExposedPorts)
|
||||
{
|
||||
result.Add(new ContainerPort(
|
||||
exposedPort,
|
||||
GetContainerExternalAddress(pod, servicePorts, exposedPort),
|
||||
GetContainerInternalAddress(pod, exposedPort)));
|
||||
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)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(internalPort.Tag))
|
||||
{
|
||||
result.Add(new ContainerPort(
|
||||
internalPort,
|
||||
new Address(string.Empty, 0),
|
||||
GetContainerInternalAddress(pod, internalPort)));
|
||||
}
|
||||
result.Add(new ContainerAddress(internalPort.Tag, GetContainerInternalAddress(startResult, recipe, internalPort.Tag), true));
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private static Address GetContainerExternalAddress(RunningPod pod, Port[] servicePorts, Port exposedPort)
|
||||
private static Address GetContainerExternalAddress(StartResult startResult, ContainerRecipe recipe, string tag)
|
||||
{
|
||||
var servicePort = servicePorts.Single(p => p.Tag == exposedPort.Tag);
|
||||
var port = startResult.GetExternalServicePorts(recipe, tag);
|
||||
|
||||
return new Address(
|
||||
pod.Cluster.HostAddress,
|
||||
servicePort.Number);
|
||||
startResult.Cluster.HostAddress,
|
||||
port.Number);
|
||||
}
|
||||
|
||||
private Address GetContainerInternalAddress(RunningPod pod, Port port)
|
||||
private Address GetContainerInternalAddress(StartResult startResult, ContainerRecipe recipe, string tag)
|
||||
{
|
||||
var namespaceName = startResult.Cluster.Configuration.KubernetesNamespace;
|
||||
var serviceName = startResult.InternalService!.Name;
|
||||
var port = startResult.GetInternalServicePorts(recipe, tag);
|
||||
|
||||
return new Address(
|
||||
$"http://{pod.PodInfo.Ip}",
|
||||
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
|
||||
port.Number);
|
||||
}
|
||||
|
||||
@@ -182,6 +197,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);
|
||||
@@ -190,6 +207,18 @@ 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)
|
||||
{
|
||||
try
|
||||
@@ -200,7 +229,7 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
catch (k8s.Autorest.HttpOperationException ex)
|
||||
{
|
||||
log.Error(JsonConvert.SerializeObject(ex));
|
||||
log.Error(JsonConvert.SerializeObject(ex.Response));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -216,30 +245,9 @@ namespace KubernetesWorkflow
|
||||
}
|
||||
catch (k8s.Autorest.HttpOperationException ex)
|
||||
{
|
||||
log.Error(JsonConvert.SerializeObject(ex));
|
||||
log.Error(JsonConvert.SerializeObject(ex.Response));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,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,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 RunningContainers RunningContainers { 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,44 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace KubernetesWorkflow.Types
|
||||
{
|
||||
public class RunningContainers
|
||||
{
|
||||
public RunningContainers(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
|
||||
{
|
||||
StartupConfig = startupConfig;
|
||||
StartResult = startResult;
|
||||
Containers = containers;
|
||||
|
||||
foreach (var c in containers) c.RunningContainers = 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 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -41,7 +46,7 @@ namespace Logging
|
||||
|
||||
public void Debug(string message = "", int skipFrames = 0)
|
||||
{
|
||||
if (debug)
|
||||
if (IsDebug)
|
||||
{
|
||||
var callerName = DebugStack.GetCallerName(skipFrames);
|
||||
Log($"(debug)({callerName}) {message}");
|
||||
@@ -50,7 +55,9 @@ namespace Logging
|
||||
|
||||
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)
|
||||
@@ -72,7 +79,7 @@ namespace Logging
|
||||
|
||||
private string ApplyReplacements(string str)
|
||||
{
|
||||
if (debug) return 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
{
|
||||
public class NullLog : BaseLog
|
||||
{
|
||||
public NullLog() : base(false)
|
||||
{
|
||||
}
|
||||
|
||||
public string FullFilename { get; set; } = "NULL";
|
||||
|
||||
protected override string GetFullName()
|
||||
@@ -15,11 +11,13 @@
|
||||
|
||||
public override void Log(string message)
|
||||
{
|
||||
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,22 @@
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public partial class BlockTimeFinder
|
||||
{
|
||||
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,280 @@
|
||||
using Logging;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public partial class BlockTimeFinder
|
||||
{
|
||||
private const ulong FetchRange = 6;
|
||||
private const int MaxEntries = 1024;
|
||||
private static readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
|
||||
private readonly Web3 web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public BlockTimeFinder(Web3 web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public ulong GetHighestBlockNumberBefore(DateTime moment)
|
||||
{
|
||||
log.Log("Looking for highest block before " + moment.ToString("o"));
|
||||
AssertMomentIsInPast(moment);
|
||||
Initialize();
|
||||
|
||||
return GetHighestBlockBefore(moment);
|
||||
}
|
||||
|
||||
public ulong GetLowestBlockNumberAfter(DateTime moment)
|
||||
{
|
||||
log.Log("Looking for lowest block after " + moment.ToString("o"));
|
||||
AssertMomentIsInPast(moment);
|
||||
Initialize();
|
||||
|
||||
return GetLowestBlockAfter(moment);
|
||||
}
|
||||
|
||||
private ulong GetHighestBlockBefore(DateTime moment)
|
||||
{
|
||||
var closestBefore = FindClosestBeforeEntry(moment);
|
||||
var closestAfter = FindClosestAfterEntry(moment);
|
||||
|
||||
if (closestBefore != null &&
|
||||
closestAfter != null &&
|
||||
closestBefore.Utc < moment &&
|
||||
closestAfter.Utc > moment &&
|
||||
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
|
||||
{
|
||||
log.Log("Found highest-Before: " + closestBefore);
|
||||
return closestBefore.BlockNumber;
|
||||
}
|
||||
|
||||
FetchBlocksAround(moment);
|
||||
return GetHighestBlockBefore(moment);
|
||||
}
|
||||
|
||||
private ulong GetLowestBlockAfter(DateTime moment)
|
||||
{
|
||||
var closestBefore = FindClosestBeforeEntry(moment);
|
||||
var closestAfter = FindClosestAfterEntry(moment);
|
||||
|
||||
if (closestBefore != null &&
|
||||
closestAfter != null &&
|
||||
closestBefore.Utc < moment &&
|
||||
closestAfter.Utc > moment &&
|
||||
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
|
||||
{
|
||||
log.Log("Found lowest-after: " + closestAfter);
|
||||
return closestAfter.BlockNumber;
|
||||
}
|
||||
|
||||
FetchBlocksAround(moment);
|
||||
return GetLowestBlockAfter(moment);
|
||||
}
|
||||
|
||||
private void FetchBlocksAround(DateTime moment)
|
||||
{
|
||||
var timePerBlock = EstimateTimePerBlock();
|
||||
log.Debug("Fetching blocks around " + moment.ToString("o") + " timePerBlock: " + timePerBlock.TotalSeconds);
|
||||
|
||||
EnsureRecentBlockIfNecessary(moment, timePerBlock);
|
||||
|
||||
var max = entries.Keys.Max();
|
||||
var blockDifference = CalculateBlockDifference(moment, timePerBlock, max);
|
||||
|
||||
FetchUp(max, blockDifference);
|
||||
FetchDown(max, blockDifference);
|
||||
}
|
||||
|
||||
private void FetchDown(ulong max, ulong blockDifference)
|
||||
{
|
||||
var target = max - blockDifference - 1;
|
||||
var fetchDown = FetchRange;
|
||||
while (fetchDown > 0)
|
||||
{
|
||||
if (!entries.ContainsKey(target))
|
||||
{
|
||||
var newBlock = AddBlockNumber(target);
|
||||
if (newBlock == null) return;
|
||||
fetchDown--;
|
||||
}
|
||||
target--;
|
||||
if (target <= 0) return;
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchUp(ulong max, ulong blockDifference)
|
||||
{
|
||||
var target = max - blockDifference;
|
||||
var fetchUp = FetchRange;
|
||||
while (fetchUp > 0)
|
||||
{
|
||||
if (!entries.ContainsKey(target))
|
||||
{
|
||||
var newBlock = AddBlockNumber(target);
|
||||
if (newBlock == null) return;
|
||||
fetchUp--;
|
||||
}
|
||||
target++;
|
||||
if (target >= max) return;
|
||||
}
|
||||
}
|
||||
|
||||
private ulong CalculateBlockDifference(DateTime moment, TimeSpan timePerBlock, ulong max)
|
||||
{
|
||||
var latest = entries[max];
|
||||
var timeDifference = latest.Utc - moment;
|
||||
double secondsDifference = Math.Abs(timeDifference.TotalSeconds);
|
||||
double secondsPerBlock = timePerBlock.TotalSeconds;
|
||||
|
||||
double numberOfBlocksDifference = secondsDifference / secondsPerBlock;
|
||||
var blockDifference = Convert.ToUInt64(numberOfBlocksDifference);
|
||||
if (blockDifference < 1) blockDifference = 1;
|
||||
return blockDifference;
|
||||
}
|
||||
|
||||
private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock)
|
||||
{
|
||||
var max = entries.Keys.Max();
|
||||
var latest = entries[max];
|
||||
var maxRetry = 10;
|
||||
while (moment > latest.Utc)
|
||||
{
|
||||
var newBlock = AddCurrentBlock();
|
||||
if (newBlock == null || newBlock.BlockNumber == latest.BlockNumber)
|
||||
{
|
||||
maxRetry--;
|
||||
if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries.");
|
||||
Thread.Sleep(timePerBlock);
|
||||
}
|
||||
max = entries.Keys.Max();
|
||||
latest = entries[max];
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddBlockNumber(decimal blockNumber)
|
||||
{
|
||||
return AddBlockNumber(Convert.ToUInt64(blockNumber));
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddBlockNumber(ulong blockNumber)
|
||||
{
|
||||
if (entries.ContainsKey(blockNumber))
|
||||
{
|
||||
return entries[blockNumber];
|
||||
}
|
||||
|
||||
if (entries.Count > MaxEntries)
|
||||
{
|
||||
log.Debug("Entries cleared!");
|
||||
entries.Clear();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
var time = GetTimestampFromBlock(blockNumber);
|
||||
if (time == null)
|
||||
{
|
||||
log.Log("Failed to get block for number: " + blockNumber);
|
||||
return null;
|
||||
}
|
||||
var entry = new BlockTimeEntry(blockNumber, time.Value);
|
||||
log.Debug("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o"));
|
||||
entries.Add(blockNumber, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private TimeSpan EstimateTimePerBlock()
|
||||
{
|
||||
var min = entries.Keys.Min();
|
||||
var max = entries.Keys.Max();
|
||||
var clippedMin = Math.Max(max - 100, min);
|
||||
var minTime = entries[min].Utc;
|
||||
var clippedMinBlock = AddBlockNumber(clippedMin);
|
||||
if (clippedMinBlock != null) minTime = clippedMinBlock.Utc;
|
||||
|
||||
var maxTime = entries[max].Utc;
|
||||
var elapsedTime = maxTime - minTime;
|
||||
|
||||
double elapsedSeconds = elapsedTime.TotalSeconds;
|
||||
double numberOfBlocks = max - min;
|
||||
double secondsPerBlock = elapsedSeconds / numberOfBlocks;
|
||||
|
||||
var result = TimeSpan.FromSeconds(secondsPerBlock);
|
||||
if (result.TotalSeconds < 1.0) result = TimeSpan.FromSeconds(1.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (!entries.Any())
|
||||
{
|
||||
AddCurrentBlock();
|
||||
AddBlockNumber(entries.Single().Key - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertMomentIsInPast(DateTime moment)
|
||||
{
|
||||
if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past.");
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddCurrentBlock()
|
||||
{
|
||||
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
|
||||
var blockNumber = number.ToDecimal();
|
||||
return AddBlockNumber(blockNumber);
|
||||
}
|
||||
|
||||
private DateTime? GetTimestampFromBlock(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)
|
||||
{
|
||||
int i = 0;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment)
|
||||
{
|
||||
BlockTimeEntry? result = null;
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
if (entry.Utc < moment) result = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.Utc > result.Utc && entry.Utc < moment) result = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private BlockTimeEntry? FindClosestAfterEntry(DateTime moment)
|
||||
{
|
||||
BlockTimeEntry? result = null;
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
if (entry.Utc > moment) result = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.Utc < result.Utc && entry.Utc > moment) result = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
@@ -17,11 +19,12 @@ 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()
|
||||
@@ -44,12 +47,19 @@ namespace NethereumWorkflow
|
||||
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()
|
||||
@@ -75,5 +85,24 @@ namespace NethereumWorkflow
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
var blockTimeFinder = new BlockTimeFinder(web3, log);
|
||||
|
||||
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
|
||||
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
|
||||
|
||||
return GetEvents<TEvent>(address, fromBlock, toBlock);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,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,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);
|
||||
}
|
||||
}
|
||||
|
||||
+36
-2
@@ -13,6 +13,11 @@
|
||||
return task.Result;
|
||||
}
|
||||
|
||||
public static void Wait(Task task)
|
||||
{
|
||||
task.Wait();
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan d)
|
||||
{
|
||||
var result = "";
|
||||
@@ -23,6 +28,35 @@
|
||||
return result;
|
||||
}
|
||||
|
||||
public static TimeSpan ParseTimespan(string span)
|
||||
{
|
||||
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)
|
||||
{
|
||||
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1));
|
||||
@@ -56,12 +90,12 @@
|
||||
|
||||
public static void Retry(Action action, int maxRetries, string description)
|
||||
{
|
||||
Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
|
||||
Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
|
||||
}
|
||||
|
||||
public static T Retry<T>(Func<T> action, int maxRetries, string description)
|
||||
{
|
||||
return Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
|
||||
return Retry(action, maxRetries, TimeSpan.FromSeconds(5), description);
|
||||
}
|
||||
|
||||
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
|
||||
|
||||
@@ -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,11 @@
|
||||
using GethPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.ABI;
|
||||
using Nethereum.Hex.HexTypes;
|
||||
using Nethereum.Util;
|
||||
using NethereumWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
@@ -7,10 +13,28 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
CodexContractsDeployment Deployment { get; }
|
||||
|
||||
void MintTestTokens(IHasEthAddress owner, TestToken testTokens);
|
||||
void MintTestTokens(EthAddress ethAddress, TestToken testTokens);
|
||||
bool IsDeployed();
|
||||
string MintTestTokens(IHasEthAddress owner, TestToken testTokens);
|
||||
string MintTestTokens(EthAddress ethAddress, TestToken testTokens);
|
||||
TestToken GetTestTokenBalance(IHasEthAddress owner);
|
||||
TestToken GetTestTokenBalance(EthAddress ethAddress);
|
||||
|
||||
Request[] GetStorageRequests(TimeRange timeRange);
|
||||
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
|
||||
RequestState GetRequestState(Request request);
|
||||
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange);
|
||||
RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange);
|
||||
SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange);
|
||||
SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange);
|
||||
}
|
||||
|
||||
public enum RequestState
|
||||
{
|
||||
New,
|
||||
Started,
|
||||
Cancelled,
|
||||
Finished,
|
||||
Failed
|
||||
}
|
||||
|
||||
public class CodexContractsAccess : ICodexContracts
|
||||
@@ -27,15 +51,19 @@ namespace CodexContractsPlugin
|
||||
|
||||
public CodexContractsDeployment Deployment { get; }
|
||||
|
||||
public void MintTestTokens(IHasEthAddress owner, TestToken testTokens)
|
||||
public bool IsDeployed()
|
||||
{
|
||||
MintTestTokens(owner.EthAddress, testTokens);
|
||||
return !string.IsNullOrEmpty(StartInteraction().GetTokenName(Deployment.TokenAddress));
|
||||
}
|
||||
|
||||
public void MintTestTokens(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 string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
|
||||
{
|
||||
return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
|
||||
}
|
||||
|
||||
public TestToken GetTestTokenBalance(IHasEthAddress owner)
|
||||
@@ -45,9 +73,108 @@ namespace CodexContractsPlugin
|
||||
|
||||
public TestToken GetTestTokenBalance(EthAddress ethAddress)
|
||||
{
|
||||
var interaction = new ContractInteractions(log, gethNode);
|
||||
var balance = interaction.GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
return balance.TestTokens();
|
||||
}
|
||||
|
||||
public Request[] GetStorageRequests(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
var i = StartInteraction();
|
||||
return events
|
||||
.Select(e =>
|
||||
{
|
||||
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
|
||||
var result = requestEvent.ReturnValue1;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
result.RequestId = e.Event.RequestId;
|
||||
return result;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = 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 EthAddress GetEthAddressFromTransaction(string transactionHash)
|
||||
{
|
||||
var transaction = gethNode.GetTransaction(transactionHash);
|
||||
return new EthAddress(transaction.From);
|
||||
}
|
||||
|
||||
private ContractInteractions StartInteraction()
|
||||
{
|
||||
return new ContractInteractions(log, gethNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using Logging;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
public class CodexContractsContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-1854dfb-dist-tests";
|
||||
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-b5f3399-dist-tests";
|
||||
|
||||
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
|
||||
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
|
||||
@@ -17,8 +19,9 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
var config = startupConfig.Get<CodexContractsContainerConfig>();
|
||||
|
||||
var containerPort = config.GethNode.StartResult.Container.GetContainerPort(GethContainerRecipe.HttpPortTag);
|
||||
var address = containerPort.InternalAddress;
|
||||
var address = config.GethNode.StartResult.Container.GetAddress(new NullLog(), GethContainerRecipe.HttpPortTag);
|
||||
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
|
||||
AddEnvVar("DISTTEST_NETWORK_URL", address.ToString());
|
||||
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
@@ -84,7 +85,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
{
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2));
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
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,10 +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)
|
||||
{
|
||||
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})");
|
||||
MintTokens(address.Address, amount, tokenAddress);
|
||||
return MintTokens(address.Address, amount, tokenAddress);
|
||||
}
|
||||
|
||||
public decimal GetBalance(string tokenAddress, string account)
|
||||
@@ -43,6 +61,17 @@ 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();
|
||||
@@ -56,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");
|
||||
@@ -67,7 +96,7 @@ namespace CodexContractsPlugin
|
||||
Amount = amount.ToBig()
|
||||
};
|
||||
|
||||
gethNode.SendTransaction(tokenAddress, function);
|
||||
return gethNode.SendTransaction(tokenAddress, function);
|
||||
}
|
||||
|
||||
private bool IsBlockNumberOK()
|
||||
@@ -87,6 +116,11 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
}
|
||||
|
||||
[Function("name", "string")]
|
||||
public class GetTokenNameFunction : FunctionMessage
|
||||
{
|
||||
}
|
||||
|
||||
[Function("mint")]
|
||||
public class MintTokensFunction : FunctionMessage
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
using GethPlugin;
|
||||
|
||||
namespace CodexContractsPlugin.Marketplace
|
||||
{
|
||||
public partial class Request : RequestBase
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
public byte[] RequestId { get; set; }
|
||||
|
||||
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
|
||||
}
|
||||
|
||||
public partial class RequestFulfilledEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestCancelledEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
public EthAddress Host { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFreedEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
@@ -0,0 +1,517 @@
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using System.Numerics;
|
||||
|
||||
// Generated code, do not modify.
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
namespace CodexContractsPlugin.Marketplace
|
||||
{
|
||||
public partial class ConfigFunction : ConfigFunctionBase { }
|
||||
|
||||
[Function("config", typeof(ConfigOutputDTO))]
|
||||
public class ConfigFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class FillSlotFunction : FillSlotFunctionBase { }
|
||||
|
||||
[Function("fillSlot")]
|
||||
public class FillSlotFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
[Parameter("bytes", "proof", 3)]
|
||||
public virtual byte[] Proof { get; set; }
|
||||
}
|
||||
|
||||
public partial class FreeSlotFunction : FreeSlotFunctionBase { }
|
||||
|
||||
[Function("freeSlot")]
|
||||
public class FreeSlotFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetActiveSlotFunction : GetActiveSlotFunctionBase { }
|
||||
|
||||
[Function("getActiveSlot", typeof(GetActiveSlotOutputDTO))]
|
||||
public class GetActiveSlotFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetChallengeFunction : GetChallengeFunctionBase { }
|
||||
|
||||
[Function("getChallenge", "bytes32")]
|
||||
public class GetChallengeFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetHostFunction : GetHostFunctionBase { }
|
||||
|
||||
[Function("getHost", "address")]
|
||||
public class GetHostFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetPointerFunction : GetPointerFunctionBase { }
|
||||
|
||||
[Function("getPointer", "uint8")]
|
||||
public class GetPointerFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetRequestFunction : GetRequestFunctionBase { }
|
||||
|
||||
[Function("getRequest", typeof(GetRequestOutputDTO))]
|
||||
public class GetRequestFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class IsProofRequiredFunction : IsProofRequiredFunctionBase { }
|
||||
|
||||
[Function("isProofRequired", "bool")]
|
||||
public class IsProofRequiredFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class MarkProofAsMissingFunction : MarkProofAsMissingFunctionBase { }
|
||||
|
||||
[Function("markProofAsMissing")]
|
||||
public class MarkProofAsMissingFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
[Parameter("uint256", "period", 2)]
|
||||
public virtual BigInteger Period { get; set; }
|
||||
}
|
||||
|
||||
public partial class MissingProofsFunction : MissingProofsFunctionBase { }
|
||||
|
||||
[Function("missingProofs", "uint256")]
|
||||
public class MissingProofsFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class MyRequestsFunction : MyRequestsFunctionBase { }
|
||||
|
||||
[Function("myRequests", "bytes32[]")]
|
||||
public class MyRequestsFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class MySlotsFunction : MySlotsFunctionBase { }
|
||||
|
||||
[Function("mySlots", "bytes32[]")]
|
||||
public class MySlotsFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class RequestEndFunction : RequestEndFunctionBase { }
|
||||
|
||||
[Function("requestEnd", "uint256")]
|
||||
public class RequestEndFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestStateFunction : RequestStateFunctionBase { }
|
||||
|
||||
[Function("requestState", "uint8")]
|
||||
public class RequestStateFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestStorageFunction : RequestStorageFunctionBase { }
|
||||
|
||||
[Function("requestStorage")]
|
||||
public class RequestStorageFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("tuple", "request", 1)]
|
||||
public virtual Request Request { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotStateFunction : SlotStateFunctionBase { }
|
||||
|
||||
[Function("slotState", "uint8")]
|
||||
public class SlotStateFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class SubmitProofFunction : SubmitProofFunctionBase { }
|
||||
|
||||
[Function("submitProof")]
|
||||
public class SubmitProofFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
[Parameter("bytes", "proof", 2)]
|
||||
public virtual byte[] Proof { get; set; }
|
||||
}
|
||||
|
||||
public partial class TokenFunction : TokenFunctionBase { }
|
||||
|
||||
[Function("token", "address")]
|
||||
public class TokenFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class WillProofBeRequiredFunction : WillProofBeRequiredFunctionBase { }
|
||||
|
||||
[Function("willProofBeRequired", "bool")]
|
||||
public class WillProofBeRequiredFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class WithdrawFundsFunction : WithdrawFundsFunctionBase { }
|
||||
|
||||
[Function("withdrawFunds")]
|
||||
public class WithdrawFundsFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class ProofSubmittedEventDTO : ProofSubmittedEventDTOBase { }
|
||||
|
||||
[Event("ProofSubmitted")]
|
||||
public class ProofSubmittedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "id", 1, false)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
[Parameter("bytes", "proof", 2, false)]
|
||||
public virtual byte[] Proof { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestCancelledEventDTO : RequestCancelledEventDTOBase { }
|
||||
|
||||
[Event("RequestCancelled")]
|
||||
public class RequestCancelledEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestFailedEventDTO : RequestFailedEventDTOBase { }
|
||||
|
||||
[Event("RequestFailed")]
|
||||
public class RequestFailedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestFulfilledEventDTO : RequestFulfilledEventDTOBase { }
|
||||
|
||||
[Event("RequestFulfilled")]
|
||||
public class RequestFulfilledEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO : SlotFilledEventDTOBase { }
|
||||
|
||||
[Event("SlotFilled")]
|
||||
public class SlotFilledEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2, false)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFreedEventDTO : SlotFreedEventDTOBase { }
|
||||
|
||||
[Event("SlotFreed")]
|
||||
public class SlotFreedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2, false)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
}
|
||||
|
||||
public partial class StorageRequestedEventDTO : StorageRequestedEventDTOBase { }
|
||||
|
||||
[Event("StorageRequested")]
|
||||
public class StorageRequestedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, false)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("tuple", "ask", 2, false)]
|
||||
public virtual Ask Ask { get; set; }
|
||||
[Parameter("uint256", "expiry", 3, false)]
|
||||
public virtual BigInteger Expiry { get; set; }
|
||||
}
|
||||
|
||||
public partial class ConfigOutputDTO : ConfigOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class ConfigOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("tuple", "collateral", 1)]
|
||||
public virtual CollateralConfig Collateral { get; set; }
|
||||
[Parameter("tuple", "proofs", 2)]
|
||||
public virtual ProofConfig Proofs { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public partial class GetActiveSlotOutputDTO : GetActiveSlotOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetActiveSlotOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("tuple", "", 1)]
|
||||
public virtual ActiveSlot ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetChallengeOutputDTO : GetChallengeOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetChallengeOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bytes32", "", 1)]
|
||||
public virtual byte[] ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetHostOutputDTO : GetHostOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetHostOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("address", "", 1)]
|
||||
public virtual string ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetPointerOutputDTO : GetPointerOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetPointerOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint8", "", 1)]
|
||||
public virtual byte ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetRequestOutputDTO : GetRequestOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetRequestOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("tuple", "", 1)]
|
||||
public virtual Request ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class IsProofRequiredOutputDTO : IsProofRequiredOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class IsProofRequiredOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bool", "", 1)]
|
||||
public virtual bool ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class MissingProofsOutputDTO : MissingProofsOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class MissingProofsOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint256", "", 1)]
|
||||
public virtual BigInteger ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class MyRequestsOutputDTO : MyRequestsOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class MyRequestsOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bytes32[]", "", 1)]
|
||||
public virtual List<byte[]> ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class MySlotsOutputDTO : MySlotsOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class MySlotsOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bytes32[]", "", 1)]
|
||||
public virtual List<byte[]> ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestEndOutputDTO : RequestEndOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class RequestEndOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint256", "", 1)]
|
||||
public virtual BigInteger ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestStateOutputDTO : RequestStateOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class RequestStateOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint8", "", 1)]
|
||||
public virtual byte ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class SlotStateOutputDTO : SlotStateOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class SlotStateOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint8", "", 1)]
|
||||
public virtual byte ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class TokenOutputDTO : TokenOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class TokenOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("address", "", 1)]
|
||||
public virtual string ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class WillProofBeRequiredOutputDTO : WillProofBeRequiredOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class WillProofBeRequiredOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bool", "", 1)]
|
||||
public virtual bool ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class CollateralConfig : CollateralConfigBase { }
|
||||
|
||||
public class CollateralConfigBase
|
||||
{
|
||||
[Parameter("uint8", "repairRewardPercentage", 1)]
|
||||
public virtual byte RepairRewardPercentage { get; set; }
|
||||
[Parameter("uint8", "maxNumberOfSlashes", 2)]
|
||||
public virtual byte MaxNumberOfSlashes { get; set; }
|
||||
[Parameter("uint16", "slashCriterion", 3)]
|
||||
public virtual ushort SlashCriterion { get; set; }
|
||||
[Parameter("uint8", "slashPercentage", 4)]
|
||||
public virtual byte SlashPercentage { get; set; }
|
||||
}
|
||||
|
||||
public partial class ProofConfig : ProofConfigBase { }
|
||||
|
||||
public class ProofConfigBase
|
||||
{
|
||||
[Parameter("uint256", "period", 1)]
|
||||
public virtual BigInteger Period { get; set; }
|
||||
[Parameter("uint256", "timeout", 2)]
|
||||
public virtual BigInteger Timeout { get; set; }
|
||||
[Parameter("uint8", "downtime", 3)]
|
||||
public virtual byte Downtime { get; set; }
|
||||
}
|
||||
|
||||
public partial class MarketplaceConfig : MarketplaceConfigBase { }
|
||||
|
||||
public class MarketplaceConfigBase
|
||||
{
|
||||
[Parameter("tuple", "collateral", 1)]
|
||||
public virtual CollateralConfig Collateral { get; set; }
|
||||
[Parameter("tuple", "proofs", 2)]
|
||||
public virtual ProofConfig Proofs { get; set; }
|
||||
}
|
||||
|
||||
public partial class Ask : AskBase { }
|
||||
|
||||
public class AskBase
|
||||
{
|
||||
[Parameter("uint64", "slots", 1)]
|
||||
public virtual ulong Slots { get; set; }
|
||||
[Parameter("uint256", "slotSize", 2)]
|
||||
public virtual BigInteger SlotSize { get; set; }
|
||||
[Parameter("uint256", "duration", 3)]
|
||||
public virtual BigInteger Duration { get; set; }
|
||||
[Parameter("uint256", "proofProbability", 4)]
|
||||
public virtual BigInteger ProofProbability { get; set; }
|
||||
[Parameter("uint256", "reward", 5)]
|
||||
public virtual BigInteger Reward { get; set; }
|
||||
[Parameter("uint256", "collateral", 6)]
|
||||
public virtual BigInteger Collateral { get; set; }
|
||||
[Parameter("uint64", "maxSlotLoss", 7)]
|
||||
public virtual ulong MaxSlotLoss { get; set; }
|
||||
}
|
||||
|
||||
public partial class Content : ContentBase { }
|
||||
|
||||
public class ContentBase
|
||||
{
|
||||
[Parameter("string", "cid", 1)]
|
||||
public virtual string Cid { get; set; }
|
||||
[Parameter("bytes32", "merkleRoot", 2)]
|
||||
public virtual byte[] MerkleRoot { get; set; }
|
||||
}
|
||||
|
||||
public partial class Request : RequestBase { }
|
||||
|
||||
public class RequestBase
|
||||
{
|
||||
[Parameter("address", "client", 1)]
|
||||
public virtual string Client { get; set; }
|
||||
[Parameter("tuple", "ask", 2)]
|
||||
public virtual Ask Ask { get; set; }
|
||||
[Parameter("tuple", "content", 3)]
|
||||
public virtual Content Content { get; set; }
|
||||
[Parameter("uint256", "expiry", 4)]
|
||||
public virtual BigInteger Expiry { get; set; }
|
||||
[Parameter("bytes32", "nonce", 5)]
|
||||
public virtual byte[] Nonce { get; set; }
|
||||
}
|
||||
|
||||
public partial class ActiveSlot : ActiveSlotBase { }
|
||||
|
||||
public class ActiveSlotBase
|
||||
{
|
||||
[Parameter("tuple", "request", 1)]
|
||||
public virtual Request Request { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
@@ -0,0 +1 @@
|
||||
This code was generated using the Nethereum code generator, here: http://playground.nethereum.com
|
||||
@@ -1,5 +1,6 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
@@ -28,19 +29,18 @@ namespace CodexDiscordBotPlugin
|
||||
{
|
||||
}
|
||||
|
||||
public RunningContainer Deploy(DiscordBotStartupConfig config)
|
||||
public RunningContainers Deploy(DiscordBotStartupConfig config)
|
||||
{
|
||||
var workflow = tools.CreateWorkflow();
|
||||
return StartContainer(workflow, config);
|
||||
}
|
||||
|
||||
private RunningContainer StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
|
||||
private RunningContainers StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
startupConfig.NameOverride = config.Name;
|
||||
startupConfig.Add(config);
|
||||
var rc = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig);
|
||||
return rc.Containers.Single();
|
||||
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
{
|
||||
public static class CoreInterfaceExtensions
|
||||
{
|
||||
public static RunningContainer DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
|
||||
public static RunningContainers DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
|
||||
{
|
||||
return Plugin(ci).Deploy(config);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using Utils;
|
||||
|
||||
namespace CodexDiscordBotPlugin
|
||||
@@ -12,16 +13,30 @@ namespace CodexDiscordBotPlugin
|
||||
{
|
||||
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("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);
|
||||
|
||||
if (!string.IsNullOrEmpty(config.DataPath))
|
||||
{
|
||||
AddEnvVar("DATAPATH", config.DataPath);
|
||||
AddVolume(config.DataPath, 1.GB());
|
||||
}
|
||||
|
||||
AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
{
|
||||
public class DiscordBotStartupConfig
|
||||
{
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName)
|
||||
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo)
|
||||
{
|
||||
Name = name;
|
||||
Token = token;
|
||||
ServerName = serverName;
|
||||
AdminRoleName = adminRoleName;
|
||||
AdminChannelName = adminChannelName;
|
||||
KubeNamespace = kubeNamespace;
|
||||
GethInfo = gethInfo;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
@@ -16,6 +18,28 @@
|
||||
public string ServerName { get; }
|
||||
public string AdminRoleName { get; }
|
||||
public string AdminChannelName { get; }
|
||||
public string KubeNamespace { 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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin
|
||||
@@ -62,14 +63,17 @@ namespace CodexPlugin
|
||||
|
||||
public string UploadFile(FileStream fileStream)
|
||||
{
|
||||
// private const string UploadFailedMessage = "Unable to store block";
|
||||
|
||||
return Http().HttpPostStream("upload", fileStream);
|
||||
return Http().HttpPostStream("data", fileStream);
|
||||
}
|
||||
|
||||
public Stream DownloadFile(string contentId)
|
||||
{
|
||||
return Http().HttpGetStream("download/" + contentId);
|
||||
return Http().HttpGetStream("data/" + contentId + "/network");
|
||||
}
|
||||
|
||||
public CodexLocalDataResponse[] LocalFiles()
|
||||
{
|
||||
return Http().HttpGetJson<CodexLocalDataResponse[]>("data");
|
||||
}
|
||||
|
||||
public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request)
|
||||
@@ -97,6 +101,12 @@ namespace CodexPlugin
|
||||
return Container.Name;
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo()
|
||||
{
|
||||
var workflow = tools.CreateWorkflow();
|
||||
return workflow.GetPodInfo(Container);
|
||||
}
|
||||
|
||||
private IHttp Http()
|
||||
{
|
||||
return tools.CreateHttp(GetAddress(), baseUrl: "/api/codex/v1", CheckContainerCrashed, Container.Name);
|
||||
@@ -109,7 +119,7 @@ namespace CodexPlugin
|
||||
|
||||
private Address GetAddress()
|
||||
{
|
||||
return Container.GetAddress(CodexContainerRecipe.ApiPortTag);
|
||||
return Container.GetAddress(tools.GetLog(), CodexContainerRecipe.ApiPortTag);
|
||||
}
|
||||
|
||||
private void CheckContainerCrashed(HttpClient client)
|
||||
|
||||
@@ -5,9 +5,10 @@ namespace CodexPlugin
|
||||
public class CodexDebugResponse
|
||||
{
|
||||
public string id { get; set; } = string.Empty;
|
||||
public string[] addrs { get; set; } = new string[0];
|
||||
public string[] addrs { get; set; } = Array.Empty<string>();
|
||||
public string repo { get; set; } = string.Empty;
|
||||
public string spr { get; set; } = string.Empty;
|
||||
public string[] announceAddresses { get; set; } = Array.Empty<string>();
|
||||
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
|
||||
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
|
||||
public CodexDebugVersionResponse codex { get; set; } = new();
|
||||
@@ -76,12 +77,7 @@ namespace CodexPlugin
|
||||
|
||||
public string peerId { get; set; } = string.Empty;
|
||||
public long seqNo { get; set; }
|
||||
public CodexDebugPeerAddressResponse[] addresses { get; set; } = Array.Empty<CodexDebugPeerAddressResponse>();
|
||||
}
|
||||
|
||||
public class CodexDebugPeerAddressResponse
|
||||
{
|
||||
public string address { get; set; } = string.Empty;
|
||||
public string[] addresses { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public class CodexDebugThresholdBreaches
|
||||
@@ -170,4 +166,30 @@ namespace CodexPlugin
|
||||
{
|
||||
public string cid { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CodexLocalData
|
||||
{
|
||||
public CodexLocalData(ContentId cid, CodexLocalDataManifestResponse manifest)
|
||||
{
|
||||
Cid = cid;
|
||||
Manifest = manifest;
|
||||
}
|
||||
|
||||
public ContentId Cid { get; }
|
||||
public CodexLocalDataManifestResponse Manifest { get; }
|
||||
}
|
||||
|
||||
public class CodexLocalDataResponse
|
||||
{
|
||||
public string cid { get; set; } = string.Empty;
|
||||
public CodexLocalDataManifestResponse manifest { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CodexLocalDataManifestResponse
|
||||
{
|
||||
public string rootHash { get; set; } = string.Empty;
|
||||
public int originalBytes { get; set; }
|
||||
public int blockSize { get; set; }
|
||||
public bool @protected { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using KubernetesWorkflow;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin
|
||||
@@ -25,7 +27,10 @@ namespace CodexPlugin
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
SetResourcesRequest(milliCPUs: 100, memory: 100.MB());
|
||||
SetResourceLimits(milliCPUs: 4000, memory: 12.GB());
|
||||
//SetResourceLimits(milliCPUs: 4000, memory: 12.GB());
|
||||
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
SetSystemCriticalPriority();
|
||||
|
||||
var config = startupConfig.Get<CodexStartupConfig>();
|
||||
|
||||
@@ -43,8 +48,9 @@ namespace CodexPlugin
|
||||
|
||||
if (config.PublicTestNet != null)
|
||||
{
|
||||
AddEnvVar("CODEX_NAT", config.PublicTestNet.PublicNatIP);
|
||||
// This makes the node announce itself to its public IP address.
|
||||
AddEnvVar("NAT_IP_AUTO", "false");
|
||||
AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -94,11 +100,10 @@ namespace CodexPlugin
|
||||
{
|
||||
var mconfig = config.MarketplaceConfig;
|
||||
var gethStart = mconfig.GethNode.StartResult;
|
||||
var ip = gethStart.Container.Pod.PodInfo.Ip;
|
||||
var port = gethStart.WsPort.Number;
|
||||
var wsAddress = gethStart.Container.GetInternalAddress(GethContainerRecipe.WsPortTag);
|
||||
var marketplaceAddress = mconfig.CodexContracts.Deployment.MarketplaceAddress;
|
||||
|
||||
AddEnvVar("CODEX_ETH_PROVIDER", $"ws://{ip}:{port}");
|
||||
AddEnvVar("CODEX_ETH_PROVIDER", $"{wsAddress.Host.Replace("http://", "ws://")}:{wsAddress.Port}");
|
||||
AddEnvVar("CODEX_MARKETPLACE_ADDRESS", marketplaceAddress);
|
||||
AddEnvVar("CODEX_PERSISTENCE", "true");
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
using CodexContractsPlugin;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public class CodexDeployment
|
||||
{
|
||||
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment, CodexContractsDeployment codexContractsDeployment, RunningContainer? prometheusContainer, RunningContainer? discordBotContainer, DeploymentMetadata metadata)
|
||||
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment,
|
||||
CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer,
|
||||
RunningContainers? discordBotContainer, DeploymentMetadata metadata,
|
||||
String id)
|
||||
{
|
||||
Id = id;
|
||||
CodexInstances = codexInstances;
|
||||
GethDeployment = gethDeployment;
|
||||
CodexContractsDeployment = codexContractsDeployment;
|
||||
@@ -16,29 +20,33 @@ namespace CodexPlugin
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public String Id { get; }
|
||||
public CodexInstance[] CodexInstances { get; }
|
||||
public GethDeployment GethDeployment { get; }
|
||||
public CodexContractsDeployment CodexContractsDeployment { get; }
|
||||
public RunningContainer? PrometheusContainer { get; }
|
||||
public RunningContainer? DiscordBotContainer { get; }
|
||||
public RunningContainers? PrometheusContainer { get; }
|
||||
public RunningContainers? DiscordBotContainer { get; }
|
||||
public DeploymentMetadata Metadata { get; }
|
||||
}
|
||||
|
||||
public class CodexInstance
|
||||
{
|
||||
public CodexInstance(RunningContainer container, CodexDebugResponse info)
|
||||
public CodexInstance(RunningContainers containers, CodexDebugResponse info)
|
||||
{
|
||||
Container = container;
|
||||
Containers = containers;
|
||||
Info = info;
|
||||
}
|
||||
|
||||
public RunningContainer Container { get; }
|
||||
public RunningContainers Containers { get; }
|
||||
public CodexDebugResponse Info { get; }
|
||||
}
|
||||
|
||||
public class DeploymentMetadata
|
||||
{
|
||||
public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
|
||||
public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace,
|
||||
int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel,
|
||||
int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI,
|
||||
int blockMN)
|
||||
{
|
||||
Name = name;
|
||||
StartUtc = startUtc;
|
||||
@@ -73,4 +81,4 @@ namespace CodexPlugin
|
||||
public int BlockMI { get; }
|
||||
public int BlockMN { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using FileUtils;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using MetricsPlugin;
|
||||
using Utils;
|
||||
@@ -18,10 +19,13 @@ namespace CodexPlugin
|
||||
//CodexDebugRepoStoreResponse[] GetDebugRepoStore();
|
||||
ContentId UploadFile(TrackedFile file);
|
||||
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
|
||||
CodexLocalData[] LocalFiles();
|
||||
void ConnectToPeer(ICodexNode node);
|
||||
CodexDebugVersionResponse Version { get; }
|
||||
IMarketplaceAccess Marketplace { get; }
|
||||
CrashWatcher CrashWatcher { get; }
|
||||
PodInfo GetPodInfo();
|
||||
ITransferSpeeds TransferSpeeds { get; }
|
||||
void Stop();
|
||||
}
|
||||
|
||||
@@ -31,6 +35,7 @@ namespace CodexPlugin
|
||||
private const string UploadFailedMessage = "Unable to store block";
|
||||
private readonly IPluginTools tools;
|
||||
private readonly EthAddress? ethAddress;
|
||||
private readonly TransferSpeeds transferSpeeds;
|
||||
|
||||
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress)
|
||||
{
|
||||
@@ -40,6 +45,7 @@ namespace CodexPlugin
|
||||
Group = group;
|
||||
Marketplace = marketplaceAccess;
|
||||
Version = new CodexDebugVersionResponse();
|
||||
transferSpeeds = new TransferSpeeds();
|
||||
}
|
||||
|
||||
public RunningContainer Container { get { return CodexAccess.Container; } }
|
||||
@@ -48,13 +54,12 @@ namespace CodexPlugin
|
||||
public CodexNodeGroup Group { get; }
|
||||
public IMarketplaceAccess Marketplace { get; }
|
||||
public CodexDebugVersionResponse Version { get; private set; }
|
||||
public ITransferSpeeds TransferSpeeds { get => transferSpeeds; }
|
||||
public IMetricsScrapeTarget MetricsScrapeTarget
|
||||
{
|
||||
get
|
||||
{
|
||||
var port = CodexAccess.Container.Recipe.GetPortByTag(CodexContainerRecipe.MetricsPortTag);
|
||||
if (port == null) throw new Exception("Metrics is not available for this Codex node. Please start it with the option '.EnableMetrics()' to enable it.");
|
||||
return new MetricsScrapeTarget(CodexAccess.Container, port);
|
||||
return new MetricsScrapeTarget(CodexAccess.Container, CodexContainerRecipe.MetricsPortTag);
|
||||
}
|
||||
}
|
||||
public EthAddress EthAddress
|
||||
@@ -100,11 +105,14 @@ namespace CodexPlugin
|
||||
|
||||
var logMessage = $"Uploading file {file.Describe()}...";
|
||||
Log(logMessage);
|
||||
var response = Stopwatch.Measure(tools.GetLog(), logMessage, () =>
|
||||
var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () =>
|
||||
{
|
||||
return CodexAccess.UploadFile(fileStream);
|
||||
});
|
||||
|
||||
var response = measurement.Value;
|
||||
transferSpeeds.AddUploadSample(file.GetFilesize(), measurement.Duration);
|
||||
|
||||
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
|
||||
if (response.StartsWith(UploadFailedMessage)) FrameworkAssert.Fail("Node failed to store block.");
|
||||
|
||||
@@ -117,11 +125,17 @@ namespace CodexPlugin
|
||||
var logMessage = $"Downloading for contentId: '{contentId.Id}'...";
|
||||
Log(logMessage);
|
||||
var file = tools.GetFileManager().CreateEmptyFile(fileLabel);
|
||||
Stopwatch.Measure(tools.GetLog(), logMessage, () => DownloadToFile(contentId.Id, file));
|
||||
var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () => DownloadToFile(contentId.Id, file));
|
||||
transferSpeeds.AddDownloadSample(file.GetFilesize(), measurement);
|
||||
Log($"Downloaded file {file.Describe()} to '{file.Filename}'.");
|
||||
return file;
|
||||
}
|
||||
|
||||
public CodexLocalData[] LocalFiles()
|
||||
{
|
||||
return CodexAccess.LocalFiles().Select(l => new CodexLocalData(new ContentId(l.cid), l.manifest)).ToArray();
|
||||
}
|
||||
|
||||
public void ConnectToPeer(ICodexNode node)
|
||||
{
|
||||
var peer = (CodexNode)node;
|
||||
@@ -134,6 +148,11 @@ namespace CodexPlugin
|
||||
Log($"Successfully connected to peer {peer.GetName()}.");
|
||||
}
|
||||
|
||||
public PodInfo GetPodInfo()
|
||||
{
|
||||
return CodexAccess.GetPodInfo();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (Group.Count() > 1) throw new InvalidOperationException("Codex-nodes that are part of a group cannot be " +
|
||||
@@ -154,8 +173,9 @@ namespace CodexPlugin
|
||||
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.codex}");
|
||||
}
|
||||
|
||||
//lifecycle.Log.AddStringReplace(nodePeerId, nodeName);
|
||||
//lifecycle.Log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
|
||||
var log = tools.GetLog();
|
||||
log.AddStringReplace(nodePeerId, nodeName);
|
||||
log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
|
||||
Version = debugInfo.codex;
|
||||
}
|
||||
|
||||
@@ -166,7 +186,10 @@ namespace CodexPlugin
|
||||
|
||||
// The peer we want to connect is in a different pod.
|
||||
// We must replace the default IP with the pod IP in the multiAddress.
|
||||
return multiAddress.Replace("0.0.0.0", peer.CodexAccess.Container.Pod.PodInfo.Ip);
|
||||
var workflow = tools.CreateWorkflow();
|
||||
var podInfo = workflow.GetPodInfo(peer.Container);
|
||||
|
||||
return multiAddress.Replace("0.0.0.0", podInfo.Ip);
|
||||
}
|
||||
|
||||
private void DownloadToFile(string contentId, TrackedFile file)
|
||||
@@ -198,5 +221,15 @@ namespace CodexPlugin
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ContentId id && Id == id.Id;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Core;
|
||||
using GethPlugin;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using MetricsPlugin;
|
||||
using System.Collections;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
@@ -19,12 +19,13 @@ namespace CodexPlugin
|
||||
|
||||
public void Announce()
|
||||
{
|
||||
tools.GetLog().Log($"Loaded with Codex ID: '{codexStarter.GetCodexId()}'");
|
||||
tools.GetLog().Log($"Loaded with Codex ID: '{codexStarter.GetCodexId()}' - Revision: {codexStarter.GetCodexRevision()}");
|
||||
}
|
||||
|
||||
public void AddMetadata(IAddMetadata metadata)
|
||||
{
|
||||
metadata.Add("codexid", codexStarter.GetCodexId());
|
||||
metadata.Add("codexrevision", codexStarter.GetCodexRevision());
|
||||
}
|
||||
|
||||
public void Decommission()
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace CodexPlugin
|
||||
|
||||
public class CodexLogCustomTopics
|
||||
{
|
||||
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p, CodexLogLevel blockExchange)
|
||||
{
|
||||
DiscV5 = discV5;
|
||||
Libp2p = libp2p;
|
||||
BlockExchange = blockExchange;
|
||||
}
|
||||
|
||||
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p)
|
||||
{
|
||||
DiscV5 = discV5;
|
||||
@@ -35,6 +42,7 @@ namespace CodexPlugin
|
||||
|
||||
public CodexLogLevel DiscV5 { get; set; }
|
||||
public CodexLogLevel Libp2p { get; set; }
|
||||
public CodexLogLevel? BlockExchange { get; }
|
||||
}
|
||||
|
||||
public class CodexSetup : CodexStartupConfig, ICodexSetup
|
||||
@@ -133,7 +141,7 @@ namespace CodexPlugin
|
||||
|
||||
private IEnumerable<string> DescribeArgs()
|
||||
{
|
||||
if (PublicTestNet != null) yield return $"<!>Public TestNet at {PublicTestNet.PublicNatIP}:{PublicTestNet.PublicListenPort}<!>";
|
||||
if (PublicTestNet != null) yield return $"<!>Public TestNet with listenPort: {PublicTestNet.PublicListenPort}<!>";
|
||||
yield return $"LogLevel={LogLevelWithTopics()}";
|
||||
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
|
||||
if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
|
||||
namespace CodexPlugin
|
||||
@@ -24,8 +25,12 @@ namespace CodexPlugin
|
||||
|
||||
var containers = StartCodexContainers(startupConfig, codexSetup.NumberOfNodes, codexSetup.Location);
|
||||
|
||||
var podInfos = string.Join(", ", containers.Containers().Select(c => $"Container: '{c.Name}' runs at '{c.Pod.PodInfo.K8SNodeName}'={c.Pod.PodInfo.Ip}"));
|
||||
Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers().First().Recipe.Image}'. ({podInfos})");
|
||||
foreach (var rc in containers)
|
||||
{
|
||||
var podInfo = GetPodInfo(rc);
|
||||
var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}"));
|
||||
Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers().First().Recipe.Image}'. ({podInfos})");
|
||||
}
|
||||
LogSeparator();
|
||||
|
||||
return containers;
|
||||
@@ -61,6 +66,12 @@ namespace CodexPlugin
|
||||
return recipe.Image;
|
||||
}
|
||||
|
||||
public string GetCodexRevision()
|
||||
{
|
||||
if (versionResponse != null) return versionResponse.revision;
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private StartupConfig CreateStartupConfig(CodexSetup codexSetup)
|
||||
{
|
||||
var startupConfig = new StartupConfig();
|
||||
@@ -80,6 +91,12 @@ namespace CodexPlugin
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private PodInfo GetPodInfo(RunningContainers rc)
|
||||
{
|
||||
var workflow = pluginTools.CreateWorkflow();
|
||||
return workflow.GetPodInfo(rc);
|
||||
}
|
||||
|
||||
private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningContainers[] runningContainers, CodexNodeFactory codexNodeFactory)
|
||||
{
|
||||
var group = new CodexNodeGroup(this, pluginTools, runningContainers, codexNodeFactory);
|
||||
|
||||
@@ -52,12 +52,31 @@ namespace CodexPlugin
|
||||
"connection",
|
||||
"connmanager",
|
||||
"websock",
|
||||
"ws-session"
|
||||
"ws-session",
|
||||
"dialer",
|
||||
"muxedupgrade",
|
||||
"upgrade",
|
||||
"identify"
|
||||
};
|
||||
var blockExchangeTopics = new[]
|
||||
{
|
||||
"codex",
|
||||
"pendingblocks",
|
||||
"peerctxstore",
|
||||
"discoveryengine",
|
||||
"blockexcengine",
|
||||
"blockexcnetwork",
|
||||
"blockexcnetworkpeer"
|
||||
};
|
||||
|
||||
level = $"{level};" +
|
||||
$"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" +
|
||||
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)}";
|
||||
|
||||
if (CustomTopics.BlockExchange != null)
|
||||
{
|
||||
level += $";{CustomTopics.BlockExchange.ToString()!.ToLowerInvariant()}:{string.Join(",", blockExchangeTopics)}";
|
||||
}
|
||||
}
|
||||
return level;
|
||||
}
|
||||
@@ -65,7 +84,6 @@ namespace CodexPlugin
|
||||
|
||||
public class CodexTestNetConfig
|
||||
{
|
||||
public string PublicNatIP { get; set; } = string.Empty;
|
||||
public int PublicDiscoveryPort { get; set; }
|
||||
public int PublicListenPort { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
@@ -10,13 +10,6 @@ namespace CodexPlugin
|
||||
return Plugin(ci).DeployCodexNodes(number, setup);
|
||||
}
|
||||
|
||||
public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningContainer[] containers)
|
||||
{
|
||||
// ew, clean this up.
|
||||
var rcs = new RunningContainers(null!, containers.First().Pod, containers);
|
||||
return WrapCodexContainers(ci, new[] { rcs });
|
||||
}
|
||||
|
||||
public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningContainers[] containers)
|
||||
{
|
||||
return Plugin(ci).WrapCodexContainers(ci, containers);
|
||||
|
||||
@@ -10,6 +10,7 @@ namespace CodexPlugin
|
||||
{
|
||||
string MakeStorageAvailable(ByteSize size, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration);
|
||||
StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
|
||||
StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry);
|
||||
}
|
||||
|
||||
public class MarketplaceAccess : IMarketplaceAccess
|
||||
@@ -25,13 +26,20 @@ namespace CodexPlugin
|
||||
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
{
|
||||
return RequestStorage(contentId, pricePerSlotPerSecond, requiredCollateral, minRequiredNumberOfNodes, proofProbability, duration, duration / 2);
|
||||
}
|
||||
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry)
|
||||
{
|
||||
var expireUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + expiry.TotalSeconds;
|
||||
|
||||
var request = new CodexSalesRequestStorageRequest
|
||||
{
|
||||
duration = ToDecInt(duration.TotalSeconds),
|
||||
proofProbability = ToDecInt(proofProbability),
|
||||
reward = ToDecInt(pricePerSlotPerSecond),
|
||||
collateral = ToDecInt(requiredCollateral),
|
||||
expiry = null,
|
||||
expiry = ToDecInt(expireUtc),
|
||||
nodes = minRequiredNumberOfNodes,
|
||||
tolerance = null,
|
||||
};
|
||||
@@ -41,11 +49,15 @@ namespace CodexPlugin
|
||||
$"requiredCollateral: {requiredCollateral}, " +
|
||||
$"minRequiredNumberOfNodes: {minRequiredNumberOfNodes}, " +
|
||||
$"proofProbability: {proofProbability}, " +
|
||||
$"expiry: {Time.FormatDuration(expiry)}, " +
|
||||
$"duration: {Time.FormatDuration(duration)})");
|
||||
|
||||
var response = codexAccess.RequestStorage(request, contentId.Id);
|
||||
|
||||
if (response == "Purchasing not available")
|
||||
if (response == "Purchasing not available" ||
|
||||
response == "Expiry required" ||
|
||||
response == "Expiry needs to be in future" ||
|
||||
response == "Expiry has to be before the request's end (now + duration)")
|
||||
{
|
||||
throw new InvalidOperationException(response);
|
||||
}
|
||||
@@ -104,6 +116,12 @@ namespace CodexPlugin
|
||||
return null!;
|
||||
}
|
||||
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry)
|
||||
{
|
||||
Unavailable();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration)
|
||||
{
|
||||
Unavailable();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public interface ITransferSpeeds
|
||||
{
|
||||
BytesPerSecond? GetUploadSpeed();
|
||||
BytesPerSecond? GetDownloadSpeed();
|
||||
}
|
||||
|
||||
public class TransferSpeeds : ITransferSpeeds
|
||||
{
|
||||
private readonly List<BytesPerSecond> uploads = new List<BytesPerSecond>();
|
||||
private readonly List<BytesPerSecond> downloads = new List<BytesPerSecond>();
|
||||
|
||||
public void AddUploadSample(ByteSize bytes, TimeSpan duration)
|
||||
{
|
||||
uploads.Add(Convert(bytes, duration));
|
||||
}
|
||||
|
||||
public void AddDownloadSample(ByteSize bytes, TimeSpan duration)
|
||||
{
|
||||
downloads.Add(Convert(bytes, duration));
|
||||
}
|
||||
|
||||
public BytesPerSecond? GetUploadSpeed()
|
||||
{
|
||||
if (!uploads.Any()) return null;
|
||||
return uploads.Average();
|
||||
}
|
||||
|
||||
public BytesPerSecond? GetDownloadSpeed()
|
||||
{
|
||||
if (!downloads.Any()) return null;
|
||||
return downloads.Average();
|
||||
}
|
||||
|
||||
private static BytesPerSecond Convert(ByteSize size, TimeSpan duration)
|
||||
{
|
||||
double bytes = size.SizeInBytes;
|
||||
double seconds = duration.TotalSeconds;
|
||||
|
||||
return new BytesPerSecond(System.Convert.ToInt64(Math.Round(bytes / seconds)));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ListExtensions
|
||||
{
|
||||
public static BytesPerSecond Average(this List<BytesPerSecond> list)
|
||||
{
|
||||
double sum = list.Sum(i => i.SizeInBytes);
|
||||
double num = list.Count;
|
||||
|
||||
return new BytesPerSecond(Convert.ToInt64(Math.Round(sum / num)));
|
||||
}
|
||||
|
||||
public static BytesPerSecond? OptionalAverage(this List<BytesPerSecond?>? list)
|
||||
{
|
||||
if (list == null || !list.Any() || !list.Any(i => i != null)) return null;
|
||||
var values = list.Where(i => i != null).Cast<BytesPerSecond>().ToArray();
|
||||
double sum = values.Sum(i => i.SizeInBytes);
|
||||
double num = values.Length;
|
||||
|
||||
return new BytesPerSecond(Convert.ToInt64(Math.Round(sum / num)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace DeployAndRunPlugin
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
|
||||
namespace DeployAndRunPlugin
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
|
||||
namespace DeployAndRunPlugin
|
||||
{
|
||||
|
||||
@@ -9,11 +9,22 @@
|
||||
{
|
||||
public EthAddress(string address)
|
||||
{
|
||||
Address = address;
|
||||
Address = address.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string Address { get; }
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is EthAddress address &&
|
||||
Address == address.Address;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Address);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Address;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
|
||||
namespace GethPlugin
|
||||
{
|
||||
@@ -23,13 +24,19 @@ namespace GethPlugin
|
||||
|
||||
var args = CreateArgs(config);
|
||||
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
SetSystemCriticalPriority();
|
||||
|
||||
AddEnvVar("GETH_ARGS", args);
|
||||
}
|
||||
|
||||
private string CreateArgs(GethStartupConfig config)
|
||||
{
|
||||
if (config.IsMiner) AddEnvVar("ENABLE_MINER", "1");
|
||||
UnlockAccounts(0, 1);
|
||||
if (config.IsMiner)
|
||||
{
|
||||
AddEnvVar("ENABLE_MINER", "1");
|
||||
UnlockAccounts(0, 1);
|
||||
}
|
||||
|
||||
var httpPort = CreateApiPort(config, tag: HttpPortTag);
|
||||
var discovery = CreateDiscoveryPort(config);
|
||||
@@ -37,16 +44,20 @@ namespace GethPlugin
|
||||
var authRpc = CreateP2pPort(config, tag: AuthRpcPortTag);
|
||||
var wsPort = CreateP2pPort(config, tag: WsPortTag);
|
||||
|
||||
var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {GetTestNetArgs(config)} {defaultArgs}";
|
||||
var args = $"--http.addr 0.0.0.0 --http.port {httpPort.Number} --port {listen.Number} --discovery.port {discovery.Number} {defaultArgs}";
|
||||
|
||||
if (config.BootstrapNode != null)
|
||||
{
|
||||
var bootPubKey = config.BootstrapNode.PublicKey;
|
||||
var bootIp = config.BootstrapNode.IpAddress;
|
||||
var bootPort = config.BootstrapNode.Port;
|
||||
var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}";
|
||||
var bootstrapArg = $" --bootnodes enode://{bootPubKey}@{bootIp}:{bootPort}";
|
||||
args += bootstrapArg;
|
||||
}
|
||||
if (config.IsPublicTestNet != null)
|
||||
{
|
||||
AddEnvVar("NAT_PUBLIC_IP_AUTO", PublicIpService.Address);
|
||||
}
|
||||
|
||||
return args + $" --authrpc.port {authRpc.Number} --ws --ws.addr 0.0.0.0 --ws.port {wsPort.Number}";
|
||||
}
|
||||
@@ -54,20 +65,13 @@ namespace GethPlugin
|
||||
private void UnlockAccounts(int startIndex, int numberOfAccounts)
|
||||
{
|
||||
if (startIndex < 0) throw new ArgumentException();
|
||||
if (numberOfAccounts < 1) throw new ArgumentException();
|
||||
if (numberOfAccounts < 0) throw new ArgumentException();
|
||||
if (startIndex + numberOfAccounts > 1000) throw new ArgumentException("Out of accounts!");
|
||||
|
||||
AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString());
|
||||
AddEnvVar("UNLOCK_NUMBER", numberOfAccounts.ToString());
|
||||
}
|
||||
|
||||
private string GetTestNetArgs(GethStartupConfig config)
|
||||
{
|
||||
if (config.IsPublicTestNet == null) return string.Empty;
|
||||
|
||||
return $"--nat=extip:{config.IsPublicTestNet.PublicIp}";
|
||||
}
|
||||
|
||||
private Port CreateDiscoveryPort(GethStartupConfig config)
|
||||
{
|
||||
if (config.IsPublicTestNet == null) return AddInternalPort(DiscoveryPortTag);
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Recipe;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace GethPlugin
|
||||
{
|
||||
public class GethDeployment : IHasContainer
|
||||
{
|
||||
public GethDeployment(RunningContainer container, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey)
|
||||
public GethDeployment(RunningContainers containers, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey)
|
||||
{
|
||||
Container = container;
|
||||
Containers = containers;
|
||||
DiscoveryPort = discoveryPort;
|
||||
HttpPort = httpPort;
|
||||
WsPort = wsPort;
|
||||
@@ -15,7 +17,9 @@ namespace GethPlugin
|
||||
PubKey = pubKey;
|
||||
}
|
||||
|
||||
public RunningContainer Container { get; }
|
||||
public RunningContainers Containers { get; }
|
||||
[JsonIgnore]
|
||||
public RunningContainer Container { get { return Containers.Containers.Single(); } }
|
||||
public Port DiscoveryPort { get; }
|
||||
public Port HttpPort { get; }
|
||||
public Port WsPort { get; }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using NethereumWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace GethPlugin
|
||||
{
|
||||
@@ -13,19 +16,23 @@ namespace GethPlugin
|
||||
Ether GetEthBalance();
|
||||
Ether GetEthBalance(IHasEthAddress address);
|
||||
Ether GetEthBalance(EthAddress address);
|
||||
void SendEth(IHasEthAddress account, Ether eth);
|
||||
void SendEth(EthAddress account, Ether eth);
|
||||
string SendEth(IHasEthAddress account, Ether eth);
|
||||
string SendEth(EthAddress account, Ether eth);
|
||||
TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
|
||||
void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
|
||||
string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
|
||||
Transaction GetTransaction(string transactionHash);
|
||||
decimal? GetSyncedBlockNumber();
|
||||
bool IsContractAvailable(string abi, string contractAddress);
|
||||
GethBootstrapNode GetBootstrapRecord();
|
||||
List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new();
|
||||
List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new();
|
||||
}
|
||||
|
||||
public class GethNode : IGethNode
|
||||
public class DeploymentGethNode : BaseGethNode, IGethNode
|
||||
{
|
||||
private readonly ILog log;
|
||||
|
||||
public GethNode(ILog log, GethDeployment startResult)
|
||||
public DeploymentGethNode(ILog log, GethDeployment startResult)
|
||||
{
|
||||
this.log = log;
|
||||
StartResult = startResult;
|
||||
@@ -34,6 +41,59 @@ namespace GethPlugin
|
||||
public GethDeployment StartResult { get; }
|
||||
public RunningContainer Container => StartResult.Container;
|
||||
|
||||
public GethBootstrapNode GetBootstrapRecord()
|
||||
{
|
||||
var address = StartResult.Container.GetInternalAddress(GethContainerRecipe.ListenPortTag);
|
||||
|
||||
return new GethBootstrapNode(
|
||||
publicKey: StartResult.PubKey,
|
||||
ipAddress: address.Host.Replace("http://", ""),
|
||||
port: address.Port
|
||||
);
|
||||
}
|
||||
|
||||
protected override NethereumInteraction StartInteraction()
|
||||
{
|
||||
var address = StartResult.Container.GetAddress(log, GethContainerRecipe.HttpPortTag);
|
||||
var account = StartResult.Account;
|
||||
|
||||
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
}
|
||||
|
||||
public class CustomGethNode : BaseGethNode, IGethNode
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly string gethHost;
|
||||
private readonly int gethPort;
|
||||
private readonly string privateKey;
|
||||
|
||||
public GethDeployment StartResult => throw new NotImplementedException();
|
||||
public RunningContainer Container => throw new NotImplementedException();
|
||||
|
||||
public CustomGethNode(ILog log, string gethHost, int gethPort, string privateKey)
|
||||
{
|
||||
this.log = log;
|
||||
this.gethHost = gethHost;
|
||||
this.gethPort = gethPort;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public GethBootstrapNode GetBootstrapRecord()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected override NethereumInteraction StartInteraction()
|
||||
{
|
||||
var creator = new NethereumInteractionCreator(log, gethHost, gethPort, privateKey);
|
||||
return creator.CreateWorkflow();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseGethNode
|
||||
{
|
||||
public Ether GetEthBalance()
|
||||
{
|
||||
return StartInteraction().GetEthBalance().Eth();
|
||||
@@ -49,14 +109,14 @@ namespace GethPlugin
|
||||
return StartInteraction().GetEthBalance(address.Address).Eth();
|
||||
}
|
||||
|
||||
public void SendEth(IHasEthAddress owner, Ether eth)
|
||||
public string SendEth(IHasEthAddress owner, Ether eth)
|
||||
{
|
||||
SendEth(owner.EthAddress, eth);
|
||||
return SendEth(owner.EthAddress, eth);
|
||||
}
|
||||
|
||||
public void SendEth(EthAddress account, Ether eth)
|
||||
public string SendEth(EthAddress account, Ether eth)
|
||||
{
|
||||
StartInteraction().SendEth(account.Address, eth.Eth);
|
||||
return StartInteraction().SendEth(account.Address, eth.Eth);
|
||||
}
|
||||
|
||||
public TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
|
||||
@@ -64,18 +124,14 @@ namespace GethPlugin
|
||||
return StartInteraction().Call<TFunction, 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()
|
||||
{
|
||||
StartInteraction().SendTransaction(contractAddress, function);
|
||||
return StartInteraction().SendTransaction(contractAddress, function);
|
||||
}
|
||||
|
||||
private NethereumInteraction StartInteraction()
|
||||
public Transaction GetTransaction(string transactionHash)
|
||||
{
|
||||
var address = StartResult.Container.GetAddress(GethContainerRecipe.HttpPortTag);
|
||||
var account = StartResult.Account;
|
||||
|
||||
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
|
||||
return creator.CreateWorkflow();
|
||||
return StartInteraction().GetTransaction(transactionHash);
|
||||
}
|
||||
|
||||
public decimal? GetSyncedBlockNumber()
|
||||
@@ -87,5 +143,17 @@ namespace GethPlugin
|
||||
{
|
||||
return StartInteraction().IsContractAvailable(abi, contractAddress);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
|
||||
{
|
||||
return StartInteraction().GetEvents<TEvent>(address, fromBlockNumber, toBlockNumber);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
return StartInteraction().GetEvents<TEvent>(address, timeRange);
|
||||
}
|
||||
|
||||
protected abstract NethereumInteraction StartInteraction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ namespace GethPlugin
|
||||
|
||||
Log($"Geth node started.");
|
||||
|
||||
return new GethDeployment(container, discoveryPort, httpPort, wsPort, account, pubKey);
|
||||
return new GethDeployment(containers, discoveryPort, httpPort, wsPort, account, pubKey);
|
||||
}
|
||||
|
||||
public IGethNode WrapGethContainer(GethDeployment startResult)
|
||||
{
|
||||
startResult = SerializeGate.Gate(startResult);
|
||||
return new GethNode(tools.GetLog(), startResult);
|
||||
return new DeploymentGethNode(tools.GetLog(), startResult);
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
public interface IGethSetup
|
||||
{
|
||||
IGethSetup IsMiner();
|
||||
IGethSetup WithBootstrapNode(IGethNode node);
|
||||
IGethSetup WithBootstrapNode(GethBootstrapNode node);
|
||||
IGethSetup WithName(string name);
|
||||
IGethSetup AsPublicTestNet(GethTestNetConfig gethTestNetConfig);
|
||||
@@ -15,6 +16,11 @@
|
||||
public string? NameOverride { get; private set; }
|
||||
public GethTestNetConfig? IsPublicTestNet { get; private set; }
|
||||
|
||||
public IGethSetup WithBootstrapNode(IGethNode node)
|
||||
{
|
||||
return WithBootstrapNode(node.GetBootstrapRecord());
|
||||
}
|
||||
|
||||
public IGethSetup WithBootstrapNode(GethBootstrapNode node)
|
||||
{
|
||||
BootstrapNode = node;
|
||||
@@ -42,14 +48,12 @@
|
||||
|
||||
public class GethTestNetConfig
|
||||
{
|
||||
public GethTestNetConfig(string publicIp, int discoveryPort, int listenPort)
|
||||
public GethTestNetConfig(int discoveryPort, int listenPort)
|
||||
{
|
||||
PublicIp = publicIp;
|
||||
DiscoveryPort = discoveryPort;
|
||||
ListenPort = listenPort;
|
||||
}
|
||||
|
||||
public string PublicIp { get; }
|
||||
public int DiscoveryPort { get; }
|
||||
public int ListenPort { get; }
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user