Compare commits

..
Author SHA1 Message Date
benbierens 17564957cb POC: codex commit blamer 2024-02-26 12:50:51 +01:00
benbierens 64ecf531bd Adds missing error message check to marketplaceAccess 2024-02-26 09:02:46 +01:00
Giuliano Mega ec0f7a6790 Remove run ID and introduce deploy ID (#93)
This PR removes the notion of a run id and replaces it with a deploy id in continuous tests. Deploy ids can be set at deploy time only (duh), and will be picked up by the test runner from the deploy file on subsequent runs of the continuous test runner. As a consequence, the deploy id becomes a deployer parameter, and can no longer be overridden at the runner. For non-continuous tests, the deploy ID is created on-the-fly.
2024-02-22 10:41:07 -03:00
Slava 930c44f037 ci: use temporary branch for setup-kubectl action (#96) 2024-02-19 20:50:42 +02:00
Slava dc33f0655c ci: upldate actions to the latest versions (#95) 2024-02-19 13:05:51 +02:00
benbierens 8a4d9fecea Merge branch 'feature/bot-upgrade' 2024-01-31 11:31:52 -05:00
benbierens cb259a9086 Setting up docker for rewarder bot 2024-01-31 11:30:29 -05:00
benbierens 9d8ec6f794 loads up reward-repo with test server roles 2024-01-31 11:18:22 -05:00
benbierens acc9526cd5 Extracts reward config from bot and rewarder 2024-01-29 11:02:47 -05:00
benbierens 43a160a9cc Wires everything up 2024-01-27 09:12:20 -05:00
benbierens 4de2626a33 wip 2024-01-26 18:17:56 -05:00
benbierens 255e08a301 Implements check classes 2024-01-26 17:29:57 -05:00
benbierens 71409b06ae Increases timeout for codex contracts deployment 2024-01-26 11:37:18 -05:00
benbierens f25bca727d Fixes crash in blocktime finder. Increases smart contract deploy timeout. 2024-01-26 11:27:37 -05:00
benbierens fb73312e80 Merge branch 'master' into feature/bot-upgrade 2024-01-26 08:47:23 -05:00
Slava 9a626b7260 Updates contracts image to latest working one (#92) 2024-01-25 19:10:49 +02:00
Slava ede80410ea Disable Report workflows (#91) 2024-01-23 10:47:36 +02:00
benbierens 00c720137a Adds method for getting request state and receiving cancelled events 2024-01-22 16:27:32 +01:00
benbierens 890cff93d5 Adds method for getting request-fulfilled events 2024-01-22 16:05:04 +01:00
benbierens 678b719cef wip 2024-01-22 11:47:28 +01:00
benbierens 1b7c11b849 Setting up rewards 2024-01-22 10:27:07 +01:00
benbierens c6a7489f11 wip 2024-01-20 13:07:56 +01:00
benbierens f7fcef56c7 upgrades log filtering 2024-01-18 10:24:59 +01:00
benbierens 29fa554146 Adds log topics for block exchange 2024-01-18 09:55:07 +01:00
benbierens 2f10b30283 Sets up rewards api and handling. 2023-12-20 15:56:03 +01:00
benbierens 391a2653d9 Sets up getting of slot filled and freed events 2023-12-20 13:21:53 +01:00
benbierens 55be07d711 Implements getting slot host address 2023-12-20 11:34:23 +01:00
benbierens 75757e37fb Can fetch storage requests from chain 2023-12-20 10:55:29 +01:00
benbierens 2be31a4d3b Cleanup 2023-12-20 10:33:44 +01:00
benbierens 4b74a9d5fe working block time finder 2023-12-20 09:48:22 +01:00
benbierens 4f2539c59f Adds marketplace generated code 2023-12-19 15:43:26 +01:00
benbierens 7df1f3da7b block time finder 2023-12-19 15:30:46 +01:00
benbierens fb57998aa8 Merge branch 'master' into feature/bot-upgrade 2023-12-19 14:02:31 +01:00
Slava c4eceacafb Trigger Docker builds on folders content changes (#90) 2023-12-18 16:42:23 +02:00
Slava 974fc460c4 Remove preview from MintCommand explorer link (#89) 2023-12-18 16:38:16 +02:00
benbierens 87e84ae90d Adds method for checking of codex contracts are deployed 2023-12-18 13:32:58 +01:00
benbierens 2c026f99ca Merge branch 'feature/bot-upgrade' 2023-12-18 11:28:22 +01:00
benbierens 6298744918 Better logging 2023-12-18 11:27:28 +01:00
Slava 5342261db0 Update Report workflows (#88) 2023-12-18 11:30:15 +02:00
Slava fd540a53b1 Add Discordbot docker builds (#87)
* Add BiblioTech Docker builds

https://github.com/codex-storage/infra-codex/issues/113

* Organize workflows

https://github.com/codex-storage/infra-codex/issues/113
2023-12-15 14:07:52 +02:00
benbierens 03283414cb Adds transaction ID to record of user activity 2023-12-15 11:09:15 +01:00
benbierens beaa67c280 Adds transaction links to mint output 2023-12-15 11:02:06 +01:00
benbierens d67ddab290 Checking for all unwanted log entries 2023-12-15 09:32:03 +01:00
benbierens b143136590 Merge branch 'feature/public-testnet-deploying' 2023-12-14 15:59:22 +01:00
benbierens 8fc8a3f7cf Adds check for block verification failures 2023-12-12 14:18:26 +01:00
benbierens 2b21722cf3 forgot to hook up getSPR command 2023-12-11 11:19:05 +01:00
benbierens c3fb7384e3 Sets up deploying discordbot with geth env vars 2023-12-11 11:09:29 +01:00
benbierens fde19383df Removes deployment.json from discord bot. 2023-12-11 11:01:12 +01:00
benbierens 777e414f0a Prevents duplicate deployments. 2023-12-11 09:59:09 +01:00
benbierens 6d7309cc9c Update bot for deployment without codex nodes. 2023-12-11 08:51:46 +01:00
benbierens ec03be6936 Fixes initialization of geth node 2023-12-11 08:38:31 +01:00
benbierens 5dc918287c Merge branch 'master' into feature/public-testnet-deploying 2023-12-11 08:30:25 +01:00
benbierens 55811b20da Fixes validation for not deploying codex nodes 2023-12-11 08:30:05 +01:00
benbierens 09554da362 Adds transfer speeds to status logs. 2023-12-06 10:50:02 +01:00
benbierens 074f5ebfae Sets up transferSpeed class 2023-12-06 09:59:45 +01:00
benbierens 260bc93414 Updates contracts image to latest 2023-12-06 09:34:22 +01:00
benbierens ad05dc07f0 wip report for november 2023-12-05 08:31:28 +01:00
Slava 12ea002660 Update Runners Jobs nodeSelector (#86)
https://github.com/codex-storage/infra-codex/issues/96
2023-11-27 19:22:28 +02:00
Slava 4c46a708ab Change affinity label (#85)
https://github.com/codex-storage/infra-codex/issues/100
2023-11-27 19:06:04 +02:00
Slava b57a727195 Adjust C-Tests for autoruns (#84) 2023-11-26 21:20:04 +02:00
Slava 00207de04d Auto cleanup old Continuous tests Jobs (#83) 2023-11-24 20:29:50 +02:00
benbierens 3761e236a3 Sets node critical priority for codex and geth nodes 2023-11-23 14:50:54 +01:00
benbierens ff52e8e841 Defines public IP fetching service 2023-11-23 14:15:37 +01:00
benbierens b61f5d835c Fixes request-storage endpoint 2023-11-23 14:10:59 +01:00
benbierens 1a86d3459d Fixes local files endpoint 2023-11-23 13:43:19 +01:00
benbierens 6bd7098cfe Fixes download endpoint. 2023-11-23 13:32:58 +01:00
Slava 06ff7c9760 Add CODEXDOCKERIMAGE input to C-Tests workflow (#82) 2023-11-22 21:04:59 +02:00
Slava 703f410161 Fix reports workflows scheduler (#81) 2023-11-22 12:38:19 +02:00
benbierens 46ab3b31ca Checking that we can bootstrap geth nodes together. 2023-11-21 10:33:11 +01:00
benbierens a84d6a3c22 Replaces debug/info fetching with list of known SPRs, settable via admin commands. 2023-11-21 09:38:58 +01:00
benbierens 54471d41d5 Allows for deployment without codex nodes 2023-11-21 09:03:52 +01:00
benbierens 8b224f6922 Updates public ip to point to service 2023-11-20 11:19:10 +01:00
benbierens 005c9b4259 Merge branch 'master' into feature/public-testnet-deploying 2023-11-20 11:17:17 +01:00
Slava 6f53e78e6a Run C-Tests reports by Scheduler (#80) 2023-11-19 21:13:40 +02:00
benbierens f4532f3801 Target duration now supports timespan string format. 2023-11-17 14:51:32 +01:00
benbierens b90c47a994 Switches to automatic public IP address for codex and geth containers. 2023-11-15 14:53:25 +01:00
benbierens db55792f42 Catching exceptions in task-factory. 2023-11-14 13:28:50 +01:00
benbierens 5996c0fa63 Adds involvedpods, error, and duration in seconds to status logs for continuous tests. 2023-11-14 12:56:47 +01:00
benbierens 485e3cf02e Merge branch 'master' into feature/public-testnet-deploying 2023-11-14 10:50:41 +01:00
benbierens ee319a6d0f Merge branch 'feature/scheduling-affinity' 2023-11-14 10:50:32 +01:00
benbierens b47b596062 Fetches used external ports in order to guarantee no collisions. 2023-11-14 10:49:14 +01:00
benbierens 7de0e5a1c4 Sets up tests-runners as avoided scheduling affinity. 2023-11-14 10:16:00 +01:00
Slava 7114fd1c00 Use dedicated node pool for tests runners (#79)
https://github.com/codex-storage/infra-codex/issues/88
2023-11-13 22:37:00 +02:00
benbierens 4192952a37 Prevents reuse of external port numbers 2023-11-13 16:05:41 +01:00
benbierens b82c74865e Adds numberSource for correct range for external ports 2023-11-13 15:27:52 +01:00
benbierens ca350604e3 Debugging nodeport 2023-11-13 15:11:49 +01:00
benbierens 5a7608460b retry by setting nodeport explicitly 2023-11-13 13:58:34 +01:00
benbierens 2565ced7fe Adds test report for october 2023-11-13 13:29:28 +01:00
benbierens a81f0b9145 Merge branch 'master' into feature/public-testnet-deploying 2023-11-13 13:15:23 +01:00
benbierens 1f841ca629 Removes modified/clean indicator 2023-11-13 13:05:44 +01:00
benbierens 537c37351a Merge branch 'master' of https://github.com/status-im/cs-codex-dist-tests 2023-11-13 11:57:48 +01:00
benbierens 0f9e01e393 Adds git revision information to logs for testing framework status 2023-11-13 11:56:02 +01:00
Slava 90118b3481 Use separate log folder per Continuous Tests run (#78) 2023-11-13 12:50:32 +02:00
benbierens 412a4d3c7a Sets exit code for continuous tests runner 2023-11-13 11:17:26 +01:00
benbierens 960bef455a Adds codex revision to metadata. 2023-11-13 09:37:45 +01:00
benbierens 344d3cd44f Merge branch 'master' into feature/continuous-test-status-logs
# Conflicts:
#	Tests/DistTestCore/Logs/StatusLog.cs
2023-11-13 09:33:24 +01:00
benbierens 6636ef3ac1 Moves routing table test into peerDiscovery tests. 2023-11-13 09:32:10 +01:00
Slava 496f8fa370 Add Run ID to the Tests Identification (#77) 2023-11-12 18:35:11 +02:00
Slava 25148db971 Show Tests Identification in runners workflows (#76) 2023-11-12 17:07:53 +02:00
benbierens 71ad471958 Cleanup routing table tests. Adds retry. 2023-11-12 11:24:58 +01:00
benbierens 96ff3c38bb Adds test for routing table checks. 2023-11-12 10:36:48 +01:00
benbierens ed56d9edcc Cleanup of kubernetesWorkflow assembly. 2023-11-12 10:07:23 +01:00
benbierens b78f527c39 Adds test-type to STATUS log jsons. 2023-11-10 15:28:53 +01:00
benbierens 096282ae1a Fixes debug/peer serialization. Adds retry for pod-finding. 2023-11-10 15:23:16 +01:00
benbierens 0be6df46bc Merge branch 'deployment-json-rework' into feature/continuous-test-status-logs 2023-11-10 09:49:35 +01:00
benbierens c5cc15daa0 Updates endpoint to match latest Codex 2023-11-10 08:20:08 +01:00
benbierens 0301c3b076 Adds status log entry for each test run in continuous tests. 2023-11-09 11:35:45 +01:00
benbierens 84382b4ab4 Removes server-admin requirement for admin commands 2023-11-09 11:14:17 +01:00
benbierens 26ec841db1 locks out default permissions for admin commands 2023-11-09 10:45:28 +01:00
benbierens 1ed93bbdde Adds required admin permission for admin commands. 2023-11-09 10:37:57 +01:00
benbierens ef83441b2f Correct chunking of long messages 2023-11-09 10:13:34 +01:00
benbierens 180bf3fd35 Fixes debugPeer response formatting 2023-11-08 13:33:48 +01:00
benbierens d1403c04c4 updates bot secret name 2023-11-08 11:59:31 +01:00
benbierens 8d12bc45a1 Loops in kubeconfig and namespace for discord bot. 2023-11-08 11:49:21 +01:00
benbierens 9f0f7c374a port update 2023-11-08 10:19:06 +01:00
benbierens b8ce4c49d6 Merge branch 'master' into deployment-json-rework
# Conflicts:
#	Tests/CodexContinuousTests/run.sh
#	Tools/CodexNetDeployer/deploy-continuous-testnet.sh
2023-11-08 09:29:22 +01:00
benbierens 90070de028 Enable debug logging from single environment variable or static field. 2023-11-08 09:24:39 +01:00
benbierens 073f0d86a1 Cleans up runnerLocationUtils. 2023-11-07 16:02:09 +01:00
benbierens ec6c987ef9 determine runner location with environment variables 2023-11-07 15:51:28 +01:00
benbierens 3f8699a59b Removes dependency on pod information for elastic search download 2023-11-07 14:33:45 +01:00
benbierens 2b463d060d Restores debug setting 2023-11-07 14:00:08 +01:00
benbierens 9d464708b5 Updates local address format 2023-11-07 13:49:28 +01:00
benbierens 3e49ce6ae9 Adds kube namespace to internal addresses 2023-11-07 13:13:06 +01:00
benbierens 6e60a8614c Removes runnerlocation field from StartResult. 2023-11-07 13:06:49 +01:00
benbierens eecdcf308d Rewrites runner location set 2023-11-07 12:48:18 +01:00
benbierens 3e500e8346 Unknown runner location option. 2023-11-07 12:42:01 +01:00
benbierens ea66b5b408 debugging address selection in cluster 2023-11-07 12:02:17 +01:00
benbierens 655f4895d0 Fixes serialization issue for running containers for discord bot and metrics container 2023-11-07 11:17:20 +01:00
Ben Bierens dae4aa1f64 Merge pull request #74 from codex-storage/feature/automate-continuous-tests
Automate Continuous Tests using GitHub Actions
2023-11-07 10:48:45 +01:00
benbierens 20f92633f7 updates metrics name in twoclient test 2023-11-07 10:42:31 +01:00
benbierens d0bed64c68 Disables resource limits 2023-11-07 09:25:51 +01:00
benbierens 1412dd84fe Get ports explicitly for internal or external endpoints 2023-11-06 16:52:55 +01:00
benbierens ad88560061 Creates internal service ports for external ports from container recipe. 2023-11-06 16:38:32 +01:00
benbierens 8d0b3feff7 logs runner location 2023-11-06 16:12:18 +01:00
benbierens 6672427565 Restores marketplace test 2023-11-06 16:10:19 +01:00
benbierens 4f9c0e0ce7 Restores metrics 2023-11-06 15:27:23 +01:00
benbierens dc9f3ab090 removes dependency on static pod name and address info 2023-11-06 14:33:47 +01:00
Veaceslav Doina ead1ff7ec6 Automate Continuous Tests using GitHub Actions (#69) 2023-11-06 15:09:51 +02:00
Veaceslav Doina 807e8b7b5f Fix TwoClientTest metric name
https://github.com/codex-storage/nim-codex/pull/603
2023-11-06 15:06:47 +02:00
benbierens db0a21bc60 Merge branch 'automated-teststarter' into deployment-json-rework 2023-11-03 14:57:32 +01:00
benbierens 19d466d5d6 better service names 2023-11-03 14:53:27 +01:00
194 changed files with 5234 additions and 1412 deletions
-81
View File
@@ -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 -
+28
View File
@@ -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
+14 -13
View File
@@ -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 }}
+26
View File
@@ -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
+20
View File
@@ -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
+170
View File
@@ -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
+83
View File
@@ -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
View File
@@ -1,4 +1,5 @@
.vs
obj
bin
.vscode
.vscode
Tools/CommitBlamer/CodexTestLogs
+1
View File
@@ -1,4 +1,5 @@
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
namespace Core
{
+21
View File
@@ -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>();
}
}
+18
View File
@@ -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; }
}
}
+53
View File
@@ -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),
})
};
}
}
+5
View File
@@ -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.");
+39
View File
@@ -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>
+52
View File
@@ -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()
+15 -14
View File
@@ -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; }
}
}
+219 -83
View File
@@ -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)
+4 -1
View File
@@ -1,4 +1,7 @@
namespace KubernetesWorkflow
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
namespace KubernetesWorkflow
{
public interface IK8sHooks
{
+12 -1
View File
@@ -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;
}
+3 -1
View File
@@ -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";
}
}
@@ -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>();
}
@@ -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; }
@@ -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,6 +1,6 @@
using Utils;
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class ContainerResources
{
@@ -1,4 +1,4 @@
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class PodAnnotations
{
@@ -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()));
}
}
}
+52 -37
View File
@@ -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>();
}
}
}
+65 -57
View File
@@ -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 -6
View File
@@ -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);
-4
View File
@@ -2,10 +2,6 @@
{
public class ConsoleLog : BaseLog
{
public ConsoleLog() : base(false)
{
}
protected override string GetFullName()
{
return "CONSOLE";
+17
View File
@@ -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;
}
}
}
+1 -3
View File
@@ -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 -4
View File
@@ -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)
+17 -5
View File
@@ -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));
}
}
}
+12
View File
@@ -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
View File
@@ -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)
+24
View File
@@ -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; }
}
}
+15 -5
View File
@@ -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)
+29 -7
View File
@@ -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");
+17 -9
View File
@@ -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; }
}
}
}
+41 -8
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using MetricsPlugin;
using System.Collections;
+3 -2
View File
@@ -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()
+9 -1
View File
@@ -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}";
+19 -2
View File
@@ -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
{
+12 -1
View File
@@ -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);
+8 -4
View File
@@ -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; }
+86 -18
View File
@@ -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();
}
}
+2 -2
View File
@@ -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