Compare commits

...
Author SHA1 Message Date
benbierens f9408ab3b5 Merge branch 'feature/public-testnet-deploying' into automated-teststarter 2023-11-03 14:46:17 +01:00
benbierens 438c4271e1 Updates response message instead of deleting it. 2023-11-02 15:04:53 +01:00
benbierens fe11a97458 Fixes initialization issue for deployments monitor 2023-11-02 14:25:17 +01:00
benbierens b8774e7273 Adds SPR-fetching command. 2023-11-02 12:30:48 +01:00
benbierens bc51fc2e30 Fixes faulty persistent volume claim creation 2023-11-02 11:32:24 +01:00
benbierens 5241144e99 Locks up deploy-and-run script after tests are started. 2023-11-01 10:27:31 +01:00
benbierens 49300273e0 Cleanup continuous tests nicely even if no tests were selected for running. 2023-10-31 15:33:00 +01:00
benbierens fcb5a527a9 Fixes pod-readback labeling issue. 2023-10-31 15:04:59 +01:00
benbierens a6f7bc2393 Removes knownPods class. 2023-10-31 14:48:16 +01:00
benbierens ac07327d77 Upgrades volume support for use with deploy-and-run container 2023-10-31 14:20:50 +01:00
benbierens b5e5570145 Creates volume mount for kubeconfig file 2023-10-31 11:38:54 +01:00
benbierens c348ca9849 sets up test starter tool 2023-10-31 11:22:59 +01:00
benbierens bfdbebb36e Adds deploy-and-run plugin 2023-10-31 11:15:08 +01:00
benbierens e87f255f48 Creates dockerimage that will deploy-and-run from environment args 2023-10-31 11:01:10 +01:00
benbierens 85ad0b414f Cleans up some gethnode arguments. 2023-10-30 13:30:14 +01:00
benbierens 901259d0fa Adds debug-peer command 2023-10-29 10:40:01 +01:00
benbierens 62cc9e0587 Formats the json response 2023-10-29 10:02:51 +01:00
benbierens d7827a4531 Forgot to connect command handler. 2023-10-29 09:47:34 +01:00
benbierens f17d123af8 Adds netinfo command 2023-10-29 09:25:50 +01:00
benbierens d5b87253ae Explicitly turn off auto-nat. 2023-10-27 11:21:43 +02:00
benbierens 529f923595 Sleep before fetching debug info. 2023-10-27 10:56:30 +02:00
benbierens a362eb1e96 Separate port for geth listening 2023-10-27 08:33:07 +02:00
benbierens 9b1ab3185f Idiot developer forgets to use variable. 2023-10-25 14:42:53 +02:00
benbierens 939eed544d Fixes error where empty string is passed as datapath 2023-10-25 14:33:47 +02:00
benbierens 3914d58a6a Sets up support for UDP ports and applies them to discovery ports. 2023-10-25 14:23:07 +02:00
benbierens 4102ce0a04 Moves discord bot datafiles to volume 2023-10-25 11:53:33 +02:00
benbierens 69296577f8 adds Whois command 2023-10-25 11:25:27 +02:00
benbierens bd9fc3a3cf Ephemeral messages. 2023-10-25 10:54:26 +02:00
benbierens f148598a21 Restrict admin commands to admin channel 2023-10-25 10:38:21 +02:00
benbierens 6e82d6b1e6 Disallow duplicate eth addresses 2023-10-25 10:25:00 +02:00
benbierens 6d44a0ccfc Important user feedback message update 2023-10-25 09:54:08 +02:00
benbierens fb70daa2e9 Adds discord bot to deploy script 2023-10-25 09:40:47 +02:00
benbierens 14e8222dfe Adds codex debug-info to deployment json. Needed for bootstrapping later on. 2023-10-25 09:14:35 +02:00
benbierens dc0edce251 Moves continuous-tests default log path to persistent volume 2023-10-25 08:56:48 +02:00
benbierens b8b9e6b997 Successful deployment add and remove. 2023-10-24 15:45:40 +02:00
benbierens 29672ece68 remove command 2023-10-24 15:43:07 +02:00
benbierens a0461a446e cleanup of admin commands 2023-10-24 15:25:45 +02:00
benbierens 5aff8c6f6d setting up subcommands. 2023-10-24 14:07:15 +02:00
benbierens dfe477d192 Removes codexdeployment from input of discord bot. 2023-10-24 13:48:24 +02:00
benbierens ade08a27fe Wires codex bot into deployer. 2023-10-24 10:29:07 +02:00
benbierens cc8a860f41 First attempt to send deployment json into discord bot container. 2023-10-24 10:17:57 +02:00
benbierens d4522f0d9c Sets up bot container recipe 2023-10-24 10:06:07 +02:00
benbierens b74349cc68 Setting up codex discord bot plugin 2023-10-24 09:51:29 +02:00
benbierens 49f6c7e37e Merge branch 'master' into feature/public-testnet-deploying 2023-10-24 09:42:37 +02:00
benbierens e073f7a881 Fixes locking and retrying in http class 2023-10-24 09:41:37 +02:00
benbierens 3326c42f7a Switching to ip + port internal addresses 2023-10-24 09:03:56 +02:00
benbierens eb70fe612b trying with podIP for internal addresses. 2023-10-23 16:12:45 +02:00
benbierens b3ba39b2e5 Debug logging for nethereum interactions 2023-10-23 16:04:50 +02:00
benbierens abb9560b6d Better logging of k8s exceptions 2023-10-23 15:49:14 +02:00
benbierens 2fae9505d6 attempt to fix contract deployment in testnet mode 2023-10-23 15:28:20 +02:00
benbierens b2b338d0a5 how did this not get commited? 2023-10-23 14:25:13 +02:00
benbierens 0e087c6fee Logs continuous test config at startup. 2023-10-23 14:17:52 +02:00
benbierens 58816430e3 Adds internal ports to container-port array as long as they have tags. 2023-10-23 14:05:08 +02:00
benbierens 5a021a4bfe Capture only first Geth account. 2023-10-23 13:36:20 +02:00
benbierens a68e849768 Wires up geth public IP and discovery port. 2023-10-23 13:19:57 +02:00
benbierens 020865f5c0 Wires up deployer config 2023-10-23 13:08:49 +02:00
benbierens 4280f910ae Makes making storage available optional. 2023-10-23 12:52:47 +02:00
benbierens ec8a041257 Setting up deployer config for public testnet deploying. 2023-10-23 12:43:50 +02:00
benbierens bcb690d143 Adds option to deploy codex in a public test net. 2023-10-23 12:33:48 +02:00
benbierens 840e794761 wip: updating codex container recipe for public deployment 2023-10-23 11:30:54 +02:00
benbierens 854325f10c Adds option to deploy geth for public testnet. 2023-10-23 11:23:44 +02:00
benbierens 7a6d7d787b Splits up metrics-scraper from metrics-endpoints configuration in deployer. 2023-10-23 10:57:59 +02:00
benbierens cc2e8d5992 Merge branch 'app/discord-bot' 2023-10-23 10:32:23 +02:00
benbierens 4adce837ec Logs total run duration in overview log. 2023-10-23 10:32:11 +02:00
benbierens e11a7d1600 Gives deployments a name. 2023-10-23 10:19:52 +02:00
benbierens ad70394333 Merge branch 'master' into app/discord-bot 2023-10-23 10:13:23 +02:00
benbierens 50fbf0ad52 Replaces retry-time with maxNumberOfRetries in timesets. 2023-10-23 10:11:02 +02:00
benbierens 45fbd699a9 Disables calls to custom API endpoints. 2023-10-23 09:36:31 +02:00
benbierens bf18fa03a2 adds sleep to the starting of the test screens 2023-10-22 11:29:16 +02:00
benbierens 116f62e73e Adds eth address validation. 2023-10-22 11:26:00 +02:00
benbierens 8ef2e6023e All works 2023-10-22 11:10:45 +02:00
benbierens e16b1ce079 Sets up guild role checking 2023-10-22 10:38:46 +02:00
benbierens 4aa4731480 setting up all the commands 2023-10-22 10:10:52 +02:00
benbierens 8ad2dee67c Adds user repo. 2023-10-22 09:32:03 +02:00
benbierens 869aeb9253 Deals with timeout for operations that may take a while. 2023-10-20 11:20:38 +02:00
benbierens 8910c7ff27 Stores contracts deployment as part of CodexDeployment json. 2023-10-20 10:15:38 +02:00
benbierens 2b10f2ec58 Adds mint command 2023-10-20 10:14:56 +02:00
benbierens 991927b95f Setting up balance-getting command 2023-10-20 09:49:23 +02:00
benbierens b1bd1de027 Merge branch 'feature/multiple-container-addresses' 2023-10-20 08:31:45 +02:00
benbierens 3b258c9e2e Pins contract image to one compatible with current main codex 2023-10-20 08:31:23 +02:00
benbierens 0fd6a6f06e Fixes port tag mismatch 2023-10-19 15:48:49 +02:00
benbierens 2fea475237 multiple service ports 2023-10-19 14:03:36 +02:00
benbierens 45050c34e4 Implements GetAddress method for runningContainers. 2023-10-19 11:18:59 +02:00
benbierens 43fa57dc97 Mandatory port tags for exposed ports 2023-10-19 11:12:08 +02:00
benbierens 3a8bb760ef Adding support for multiple exposed container ports 2023-10-19 11:08:30 +02:00
benbierens 766e2f5c20 Very basic endpoint pinging that might not even work. 2023-10-18 14:59:39 +02:00
benbierens 888b19d8e5 working example of slash commands with arguments 2023-10-18 13:55:56 +02:00
benbierens f33866efc1 setting up slash commands 2023-10-18 13:48:15 +02:00
benbierens 8c7229504e do not print token 2023-10-18 11:21:06 +02:00
benbierens bcb05cd0c9 Dockerizes discord bot 2023-10-18 11:01:24 +02:00
benbierens 7179c70463 Sets up an echo command 2023-10-18 09:10:04 +02:00
benbierens b3da42522f Sets up project 2023-10-18 08:57:59 +02:00
benbierens 6b1102efa7 Adds name argument to deploy-and-run script 2023-10-17 14:32:05 +02:00
benbierens 8c82b4c527 defaults the codex log topics to warn 2023-10-17 13:52:04 +02:00
benbierens d0cafb83a1 Fixes compile error in single test runner 2023-10-16 13:10:45 +02:00
benbierens 8e4d43b73b Adds start and finished times to deployment json. 2023-10-16 11:19:57 +02:00
benbierens 8f37b4cf38 Sets long timeouts for debug/repostore call. 2023-10-10 18:08:21 +02:00
benbierens 1a277ef1b5 Logging repostore content on twoclient test failure 2023-10-10 17:54:19 +02:00
benbierens b81d574a4b adds block exchange tests. Updates namespaces 2023-10-09 16:59:52 +02:00
benbierens 7aae48d489 Merge branch 'experiment/deploy-replication' 2023-10-09 16:37:09 +02:00
benbierens b4a187f0b6 undo custom image 2023-10-09 16:35:09 +02:00
benbierens b3ab369dde adds peer id 2023-10-09 12:59:05 +02:00
benbierens f08986f6c0 Setup call to debug blockexchange endpoint. 2023-10-09 12:33:35 +02:00
benbierens b0610393af rigged for twoclient test with full container log download 2023-10-08 19:11:31 +02:00
benbierens e4b7e461e9 Adds pod name to downloaded logs 2023-10-08 18:58:15 +02:00
benbierens 0dcf3b0b16 Adds pod and deployment name 2023-10-08 18:56:00 +02:00
benbierens f3555f2703 Adds node id to downloaded container logs 2023-10-08 08:31:48 +02:00
benbierens 24ba0b4f09 Adds missing libp2p topics 2023-10-08 08:04:04 +02:00
benbierens e2a6a2f792 fixes possible null-ref 2023-10-08 07:46:48 +02:00
benbierens 4f764ac4c7 Wires up the codex log topics config options. 2023-10-08 07:29:55 +02:00
benbierens ab9257ce56 moving the deployment json 2023-10-07 10:29:09 +02:00
benbierens 1a48c40ad7 Example of setting codex log topics for libp2p and discv5 2023-10-07 09:48:12 +02:00
benbierens 092128d77b with working loops this time 2023-10-07 08:04:11 +02:00
benbierens c653afa554 trying with script 2023-10-07 07:53:10 +02:00
benbierens fab855c4bb adds log and data paths 2023-10-07 07:49:55 +02:00
benbierens 75e263be31 Try with script 2023-10-07 07:47:54 +02:00
benbierens 58016378c4 Increase deployer kubernetes timeout 2023-10-07 07:40:20 +02:00
benbierens b365d4c0ba bumps timeout because cluster might need to start more nodes 2023-10-05 08:58:05 +02:00
benbierens 8fb63213bc Trying deploy replication 2023-10-05 08:55:13 +02:00
benbierens ffe762f860 makes output deployment json configurable. 2023-10-05 08:46:59 +02:00
benbierens 9cb8c57615 Adds test report for September 2023-10-05 08:31:44 +02:00
benbierens 1ca943b189 Fixes partial rename in k8sController. 2023-10-04 10:37:18 +02:00
benbierens 5db8be3252 Merge branch 'feature/log-mon'
# Conflicts:
#	Tests/CodexContinuousTests/ContinuousTestRunner.cs
2023-10-04 09:48:21 +02:00
benbierens ccc4732752 Cleanup 2023-10-04 09:36:59 +02:00
benbierens 562f886e30 Bumps k8s operation timeout for continuous test runner. 2023-10-04 09:26:11 +02:00
benbierens 3f2c789dd5 Fixes issue where boolean arguments can't be set to false. 2023-10-04 09:19:45 +02:00
benbierens 917d715497 Fixes startup of log reconstruction 2023-10-03 16:11:14 +02:00
benbierens 696fb10386 debugging the downloader in the cluster 2023-10-03 15:36:44 +02:00
benbierens a69bea47af Debugging downloader in cluster 2023-10-03 15:18:58 +02:00
benbierens adbd481c7c Sets elasticsearch downloader for in-cluster running. 2023-10-03 14:32:31 +02:00
benbierens 1bca2bb928 Updates parser for new counter format. 2023-10-03 13:34:39 +02:00
benbierens 6fe9d38eb3 Adds filter option for selecting tests. Adds cleanup option for deleting deployment namespace. 2023-10-03 09:53:02 +02:00
benbierens 6a96bd7639 Implements elastic search log downloader. Requires log-counter codex update! 2023-10-02 14:42:36 +02:00
benbierens c3ec64f25e very very WIP for how to download complete log from elastic search 2023-10-02 12:01:49 +02:00
benbierens 3dcbb78204 Prevents multiple continuous tests from running interleaved. 2023-10-02 11:18:27 +02:00
benbierens 73c49b42c6 Fixes incorrect log message when test run gets cancelled. 2023-10-02 09:24:01 +02:00
benbierens da855b8d0d Proper log monitoring shutdown 2023-10-01 10:52:05 +02:00
benbierens b23c66c86e Working timerange limited log download 2023-10-01 09:57:32 +02:00
benbierens cec6d787de Adds methods for streaming container log 2023-10-01 08:56:21 +02:00
Ben Bierens b96751cd2c Merge pull request #68 from codex-storage/feature/deploy-local-codex
Feature/deploy local codex
2023-09-29 10:45:15 +02:00
benbierens ce995b3df7 Adds log prefixer to local builder 2023-09-29 10:43:21 +02:00
benbierens ac7fa4905d Adds dockertag description 2023-09-29 10:40:48 +02:00
benbierens dcdcd110a1 Updates READMEs 2023-09-29 10:19:59 +02:00
benbierens 8d2ce8ae7a Adds Readme to codex net deployer 2023-09-28 15:29:29 +02:00
benbierens 3e1b361b7b Fixes cleanup of online codexnodes in Codex test fixture 2023-09-28 15:04:21 +02:00
benbierens 993f2ff813 Fixes issue where autobootstrap node doesn't clear old bootstrap node handles. 2023-09-28 14:23:48 +02:00
benbierens 260294fb35 Fixes marketplace interference check tests. 2023-09-28 14:00:13 +02:00
benbierens b0f72cf453 Automated local docker build is working. 2023-09-28 12:22:35 +02:00
benbierens 0f12bcd086 Sets up local builder in codex plugin. 2023-09-28 11:31:09 +02:00
benbierens 453a1a35a2 Adds status log to continuous tests 2023-09-28 09:39:15 +02:00
benbierens dabf836838 Makes default run id static. 2023-09-28 08:53:01 +02:00
benbierens 8caa7ab4fa Adds option to limit continuous test run to set duration. 2023-09-27 11:33:54 +02:00
benbierens 6a2bd11dd5 Adds codex environment variables to startup log of continuous tests. 2023-09-27 09:09:42 +02:00
benbierens d830ccb48e Sets timestamp as default runID when environment variable is not set. 2023-09-27 08:49:36 +02:00
benbierens 42b0ab67a0 Logging bytes per millisecond for twoclient upload and download 2023-09-26 15:17:35 +02:00
benbierens 0a2bca9f52 Restores pod labels in deployer. Adds metrics assert for twoclient continuous test. 2023-09-26 14:32:28 +02:00
Slava 52345adb49 Update Continuous-Tests documentation (#67)
Some minor changes to reflect updated framework architecture
2023-09-25 20:04:06 +03:00
benbierens 9dbd33a906 Fixes serialization issue for running-pod type. 2023-09-25 09:57:27 +02:00
benbierens 3a24a77803 Adds option for partial matches of kube node names. 2023-09-25 09:19:29 +02:00
benbierens 10697f1047 Updates container location support 2023-09-25 08:47:19 +02:00
benbierens e78659690b Pod labels hook for containers deployed with CodexNetDeployer. 2023-09-22 11:10:22 +02:00
benbierens b8b59508aa Nicens up the timeouts for the net deployer. 2023-09-22 10:02:16 +02:00
benbierens ea09605827 Reverts combining http and ws ports. Fixes runner location utils. 2023-09-22 09:52:30 +02:00
benbierens e0a734591f Sets geth http and ws port to same number. 2023-09-22 09:17:56 +02:00
benbierens 100a9f15b8 Downloads container log when codex contract deployment fails. 2023-09-22 08:42:18 +02:00
benbierens d2854bb905 Cleanup 2023-09-22 07:43:46 +02:00
benbierens 192079f9c8 Adds name to geth container 2023-09-21 14:48:41 +02:00
benbierens e1a529175a Merge branch 'plugin-refactor' 2023-09-21 14:42:57 +02:00
benbierens 385fab498d updates ci command 2023-09-21 14:42:45 +02:00
benbierens 418daf1e3f Updates the docs 2023-09-21 14:39:41 +02:00
benbierens 01b2ff2181 Merge branch 'master' into plugin-refactor
# Conflicts:
#	DistTestCore/CodexStarter.cs
#	DistTestCore/GethStarter.cs
#	ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs
#	ProjectPlugins/CodexPlugin/CodexSetup.cs
#	ProjectPlugins/CodexPlugin/CodexStartupConfig.cs
2023-09-21 11:07:27 +02:00
Ben Bierens 002d3a6ade Merge pull request #58 from codex-storage/feature/marketplace-validator-sim-failures
[feature] Enable marketplace simulation of proof failures
2023-09-21 11:00:27 +02:00
Ben Bierens c569a795ce Merge pull request #59 from codex-storage/fix/wait-until
Fix re-evaluation of condition in `Time.WaitUntil`
2023-09-21 10:59:01 +02:00
Ben Bierens 7c45f213d9 Merge pull request #60 from codex-storage/feature/wait-for-contract-finished
Add `WaitForStorageContractFinished` for marketplace tests
2023-09-21 10:58:41 +02:00
benbierens e4716b5471 Adds IHasContainer to geth objects. 2023-09-21 10:56:48 +02:00
Ben Bierens 3e24bcaf50 Merge pull request #64 from codex-storage/decrease-codex-pods-resources-request
Decrease Codex Pods resources request
2023-09-21 10:53:49 +02:00
benbierens 2f81927bef Merge branch 'master' into plugin-refactor 2023-09-21 10:49:39 +02:00
benbierens dbf0ed714c Restores continuous test runner. 2023-09-21 10:33:09 +02:00
benbierens b5e0c9bfe0 Console output alignment is important. 2023-09-21 08:49:09 +02:00
Slava fb1ee57373 Cosmetic changes (#66) 2023-09-20 20:08:11 +03:00
benbierens 5b2557b3f4 Set up loading of plugins. 2023-09-20 13:56:01 +02:00
benbierens 8cde69a483 Restores continuous test runner 2023-09-20 13:33:58 +02:00
benbierens 48da92c737 Removes codex net downloader 2023-09-20 12:56:02 +02:00
benbierens 3b6fdaa3bc Restores CodexNetDeployer 2023-09-20 12:55:09 +02:00
benbierens 75369d68f7 Restores CodexNetDeployer 2023-09-20 12:02:32 +02:00
benbierens bf4c8d8026 Restores project references. 2023-09-20 10:59:52 +02:00
benbierens 09670e00e9 Moves projects into folders 2023-09-20 10:51:47 +02:00
benbierens df8ab6bcf5 Serialization gate requirement passed. 2023-09-20 10:13:29 +02:00
benbierens 5fa4e0ff9f Sets serialization gate between each deploy and wrap to ensure application lifecycle invariance. 2023-09-20 09:16:57 +02:00
benbierens cedaf84740 Wires up marketplace access 2023-09-20 08:45:55 +02:00
Slava 2e13954152 Pass a command during automated tests run (#65) 2023-09-19 17:53:15 +03:00
ThatBen 6cbf363bb1 restores balance asserts. 2023-09-19 16:22:07 +02:00
benbierens 4bc225f1d9 Cleanup 2023-09-19 13:58:45 +02:00
benbierens 6cf86af3b5 Successful testtoken mint and balance 2023-09-19 13:39:24 +02:00
benbierens 58b1c1e03c Can send eth 2023-09-19 11:51:59 +02:00
benbierens a20fc6864b working out marketplace details 2023-09-19 10:28:02 +02:00
Slava 50a1281a92 Decrease Codex Pods resources request 2023-09-18 17:24:11 +03:00
Slava c2a84c7f0e Add basic documentation how to run Continuous Tests (#63) 2023-09-18 17:06:35 +03:00
ThatBen 825200b386 Allows container resources to be conditional in container recipes. 2023-09-18 15:45:21 +02:00
ThatBen 3d4370b154 deploys codex contract 2023-09-15 16:27:08 +02:00
ThatBen 4cc93eba73 Can start Geth 2023-09-15 15:52:02 +02:00
ThatBen f2a8c123a5 Setting up geth plugin 2023-09-15 12:36:35 +02:00
ThatBen f7c69d6f24 Nicer deployment names. 2023-09-15 12:25:10 +02:00
ThatBen fb7488769d Clean up core accessibility 2023-09-14 15:40:15 +02:00
ThatBen ae7ab3d84b Container simplify for metrics plugin 2023-09-14 15:30:09 +02:00
ThatBen 7ef3f615e1 Moves log downloading to core. 2023-09-14 15:26:46 +02:00
Eric f2ad9e1198 Add WaitForStorageContractFinished for marketplace tests 2023-09-14 15:24:12 +10:00
Eric 0d9aa230da Fix re-evaluation of condition in Time.WaitUntil
For example, seller.Marketplace.GetBalance() is captured once on call, and then is compared to a continually updated sellerWithFailures.Marketplace.GetBalance.

Update RetryAssert such that `var c = constraint.Resolve();` is executed inside of the `Time.WaitUntil` instead of outside, so it is continually reevaluated.
2023-09-14 15:22:46 +10:00
Eric d412665f97 revert change to codex image 2023-09-14 15:19:06 +10:00
Eric 5dbaaaee0f replace tab chars with spaces 2023-09-14 15:17:30 +10:00
Eric fd012196f8 cleanup 2023-09-14 15:11:04 +10:00
Eric 52b907e52e Add LogLevelWithTopics, Get simulate proof failures test running 2023-09-14 15:04:37 +10:00
Eric f765933efa update WithLogLevel to accept params string[] 2023-09-14 15:03:09 +10:00
Eric 5f9371e95e Add validator and simulation of proof failures 2023-09-14 15:02:53 +10:00
benbierens 0b67dc56bb wiring up metadata 2023-09-13 16:06:05 +02:00
benbierens a2e07fbd2e Automatic downloading of container logs on test failure 2023-09-13 15:10:19 +02:00
benbierens ca5981e852 Cleanup codex log level and namespace override 2023-09-13 14:37:53 +02:00
benbierens ec5aebb47b Restores full-connectivity tests. 2023-09-13 14:24:43 +02:00
benbierens 84dd514517 Restores connectivity test helpers 2023-09-13 12:24:46 +02:00
benbierens d1895bab02 Implements bringOffline for codex nodes 2023-09-13 12:09:44 +02:00
benbierens 53bb9968ff Metrics test passed 2023-09-13 11:59:21 +02:00
benbierens 1ca3ddc67e Implements metrics plugin 2023-09-13 11:25:08 +02:00
benbierens a05a82c030 cleanup of tools creation, adds automatic log prefixing. 2023-09-13 10:23:05 +02:00
benbierens 3c724f6206 initialize plugins with constructor 2023-09-13 10:03:11 +02:00
benbierens d900416d7c setting up for metrics 2023-09-13 09:12:18 +02:00
benbierens 7e7414d491 Removes core-interface associate methods. 2023-09-13 08:55:04 +02:00
benbierens dd6b99c670 Brings back all the tests. 2023-09-12 15:43:30 +02:00
benbierens 140dd37c6e fixes issue with unassociated coreinterfaces. 2023-09-12 15:18:36 +02:00
benbierens 8849c4dfa7 Extracts plugin mapping to core assembly. 2023-09-12 14:50:18 +02:00
benbierens dc1bed6861 Extracts core from disttest core. 2023-09-12 13:34:58 +02:00
benbierens 32ad778a91 twoclient test passed. 2023-09-12 11:43:46 +02:00
benbierens 4cbd9ac828 workflow creator lifecycle 2023-09-12 11:37:20 +02:00
benbierens c995732f35 Fun with assembly loading 2023-09-12 11:25:04 +02:00
benbierens 0f86642524 simple test lined up 2023-09-12 10:31:55 +02:00
ThatBen 48dda1735c Good progress 2023-09-11 16:57:57 +02:00
Slava ef2c18c599 Fix Dist-Tests workflow input TESTID (#57) 2023-09-11 15:36:36 +03:00
benbierens 83d184177a Moving all codex details to CodexPlugin 2023-09-11 11:59:33 +02:00
benbierens 6915e90861 Extracts file manager utils to separate assembly 2023-09-11 10:43:27 +02:00
benbierens 07396644a4 Merge branch 'feature/automatic-volumes' 2023-09-11 10:00:28 +02:00
benbierens 0bcca1e845 unused usings 2023-09-11 09:58:23 +02:00
benbierens 1b3b45a6a4 Bumps codex resources 2023-09-08 18:13:54 +02:00
benbierens a5749824fb Update to full connectivity helper 2023-09-08 15:43:10 +02:00
benbierens d074f64c42 a 2023-09-08 14:03:47 +02:00
benbierens 75b16bd2ca Include request resource sets. 2023-09-08 13:47:49 +02:00
benbierens 9d2dbbbcab Fixes missing usings 2023-09-08 10:21:40 +02:00
benbierens 13dcaff5d1 Reduces resources for codex instances 2023-09-08 10:16:12 +02:00
benbierens 195527b650 Wires up resource limits 2023-09-08 10:14:52 +02:00
benbierens df6da29a69 Moves ByteSize to Utils assembly. 2023-09-08 09:39:56 +02:00
benbierens 075d9e6ae2 switched to ReadWriteOnce access mode 2023-09-07 10:55:35 +02:00
benbierens 999ed6cfc8 Creates persistent-volume-claims for codex nodes 2023-09-07 10:37:52 +02:00
benbierens aa3e500a58 Adds volume support to container recipe factory 2023-09-07 08:19:19 +02:00
Ben Bierens a03d9cd92f Merge pull request #53 from codex-storage/feature/make-grafana-dashboard-more-portable
Make Grafana dashboard more portable
2023-09-07 08:11:07 +02:00
Slava fdc98665e2 Fix Dist-Tests workflow inputs (#54) 2023-09-06 19:29:37 +03:00
Slava d409f6c01e Make Grafana dashboard more portable 2023-09-06 15:25:43 +03:00
benbierens 6115366656 August test report 2023-09-05 10:17:12 +02:00
Slava 983e1b84cc Allow access from Prometheus namespace (#51) (#52) 2023-09-04 15:58:43 +03:00
benbierens 937c3db160 Setup values for two-client test run. 2023-09-04 09:19:04 +02:00
benbierens a528c0111d Merge branch 'feature/continuous-peer-test' 2023-09-04 09:15:30 +02:00
benbierens c673aff0fe Merge branch 'master' into feature/continuous-peer-test 2023-09-04 09:13:15 +02:00
Slavaandbenbierens a4b6b561d7 Feature/add annotations for prometheus (#49)
* Add Prometheus annotations for Codex Pods (#48)

* Remove unnecessary condition (#48)

* Update Annotations to be handled via ContainerRecipe (#48)

* Wires up metrics port to codex pod annotation

* Cleans up handling of pod labels

* Fix annotations names (#48)

---------

Co-authored-by: benbierens <thatbenbierens@gmail.com>
2023-09-04 10:08:34 +03:00
benbierens 23f5318dfc More uploads 2023-09-01 14:40:48 +02:00
benbierens 3df4a97bba debugging local 2023-09-01 10:39:41 +02:00
benbierens a26d38b749 Moves container logs into log folder. 2023-09-01 09:28:04 +02:00
benbierens a29d646027 Removes future-numbers. 2023-09-01 08:45:23 +02:00
benbierens cf9d2ffe19 Fix for marketplace request deserialization 2023-08-31 11:19:53 +02:00
benbierens 647022a27e Updates overview log for all-nodes tests. 2023-08-31 09:50:34 +02:00
benbierens 7d042d0a1c Log replacements for node ids in continuous test fixture logs. 2023-08-30 10:57:20 +02:00
benbierens 8d7a7b4d1b Better exception logging for tests with multiple time-moments. 2023-08-30 10:13:48 +02:00
benbierens a1ec187919 Configured runner to stop after 10 failures 2023-08-30 09:23:13 +02:00
benbierens a5773bf710 More logging. Set container names for codex deployment. 2023-08-30 08:53:09 +02:00
benbierens 0a8928b5aa Adds logging and connectivity check. 2023-08-30 08:33:38 +02:00
benbierens 8f61c46ff0 Adds simple test to ensure routing table is correctly filled. 2023-08-29 16:08:40 +02:00
Ben Bierens 4cc09c268a Merge pull request #47 from codex-storage/feature/handle-non-200-response-code
Add better error handling for non-200 codes
2023-08-29 15:59:00 +02:00
benbierens 6bb7b994ed Merge branch 'feature/peer-connection-check-on-deploy' 2023-08-29 14:48:14 +02:00
benbierens facfd10316 Removes log asserts, they are unreliable in the cluster. 2023-08-29 14:48:03 +02:00
benbierens 3aea8f41b8 wires up timing measurement for peer download helper. 2023-08-29 10:09:48 +02:00
benbierens 553a368714 Adds a container-restart check at the end of the deployment. 2023-08-28 11:53:59 +02:00
benbierens 249b13d503 Adds tests to ensure marketplace and metrics don't interfere with peer discovery 2023-08-28 11:16:58 +02:00
benbierens c22ba346af Adds peer info to dashboard 2023-08-25 08:32:08 +02:00
benbierens b276592daf Fixes download stream error in peerDownloadHelper 2023-08-24 15:56:56 +02:00
benbierens 7da90fc53b cleanup 2023-08-24 15:45:21 +02:00
benbierens eec70a9b8c Adds peer connectivity check after deployment 2023-08-24 11:32:32 +02:00
benbierens 7eda26d177 Cleanup of full-connectivity helper. 2023-08-24 10:59:11 +02:00
benbierens 3c94727101 Slows down the two continuous tests 2023-08-24 10:44:06 +02:00
benbierens 561024738a Fixes issue with log downloading in holdmybeer test 2023-08-24 09:33:46 +02:00
benbierens 9a117cca70 rigs continuous testnet for holdmybeer test. 2023-08-23 08:29:16 +02:00
benbierens e340b3cd41 Merge branch 'spike/local-continuous-debug' 2023-08-22 15:56:05 +02:00
benbierens 2803b8b484 Merge branch 'master' into spike/local-continuous-debug
# Conflicts:
#	DistTestCore/Codex/CodexContainerRecipe.cs
2023-08-22 15:53:07 +02:00
benbierens 47aedc2ac8 cleanup 2023-08-22 15:51:39 +02:00
benbierens 7e5121a642 Fixes test for cloud environment 2023-08-22 09:06:44 +02:00
benbierens ce3850b0b0 I accidentally filled up my harddrive with random numbers last night. 2023-08-21 07:58:58 +02:00
benbierens 1682dfc08f Adds holdmybeer test to continuous test runner 2023-08-18 10:31:20 +02:00
Eric 70c56eadec Add better error handling for non-200 codes
Also add better json deserialization error handling
2023-08-18 15:40:40 +10:00
Slava c716fde17b Add documentation how to run tests in remote Kubernetes (#41)
https://github.com/codex-storage/cs-codex-dist-tests/issues/40
2023-08-17 14:43:13 +03:00
benbierens aaca9e1ec5 Bumps to new images. 2023-08-17 11:06:45 +02:00
benbierens c8cb04d859 Allows downloading only log-tails. 2023-08-16 16:13:29 +02:00
Slava 543f2b97dd Add reusable workflow for Docker (#42) (#45) 2023-08-16 14:46:17 +03:00
benbierens 30364abf21 Correct checking of upload and download log statements 2023-08-16 11:39:24 +02:00
benbierens 6486dba289 Asserting correct filesize and block numbers in the codex logs 2023-08-16 10:52:44 +02:00
benbierens 57c46004b1 cleanup 2023-08-15 15:00:47 +02:00
benbierens 3abb1770c2 Fixes crash log downloads 2023-08-15 12:55:30 +02:00
benbierens 08ee09af1e wires crashWatcher into CodexStarter 2023-08-15 11:01:18 +02:00
benbierens 72fa368357 working example of crash log recovery 2023-08-15 10:03:01 +02:00
benbierens 86bae93e98 Wires up block-maintenance number of blocks param 2023-08-14 16:37:31 +02:00
benbierens c0b7d3a747 Hooks up block maintenance interval config param. 2023-08-14 15:51:03 +02:00
benbierens 6e5e30afd4 rigged up a pod crash detector maybe 2023-08-14 15:10:36 +02:00
benbierens 9f0600d6c1 Automatic quota line. Holding test. 2023-08-14 10:35:42 +02:00
benbierens f2346d5d09 Merge branch 'feature/grafana-dashboard' into spike/local-continuous-debug
# Conflicts:
#	ContinuousTests/ContinuousTestRunner.cs
2023-08-14 08:19:09 +02:00
benbierens b4302ab6f7 Decent dashboard setup 2023-08-13 11:19:35 +02:00
benbierens 75cdb94e27 use cluster-external addresses for dashboard URLs 2023-08-13 10:10:47 +02:00
benbierens bce8a48cad logs grafana image id 2023-08-13 10:07:47 +02:00
benbierens de66236383 Cleans up grafana starter. 2023-08-13 10:01:06 +02:00
benbierens f1131abf79 Wires dashboard into deployer 2023-08-13 09:07:23 +02:00
benbierens fc3f424208 Splits up grafana starter 2023-08-13 08:40:32 +02:00
benbierens 75228c815d fixes dashboard permissions 2023-08-13 08:27:30 +02:00
benbierens 485af03367 very wip: automatic grafana dashboard 2023-08-11 12:38:26 +02:00
benbierens 2acb669c31 automatically adds prometheus as datasource 2023-08-11 11:06:40 +02:00
benbierens 79908b8a54 disables grafana auth 2023-08-11 10:16:19 +02:00
benbierens 9037ddab6e initial setup of a grafana container 2023-08-11 09:37:30 +02:00
benbierens 5d92b99926 Determine runner location by pinging pod IP. 2023-08-11 09:09:03 +02:00
benbierens fff02656b7 automatically determine runner location 2023-08-11 08:39:51 +02:00
benbierens 5f1b9bf8d4 lines up test type names 2023-08-10 14:31:08 +02:00
benbierens 7280fcc3cf Merge branch 'master' into spike/local-continuous-debug
# Conflicts:
#	ContinuousTests/ContinuousTestRunner.cs
2023-08-10 14:26:36 +02:00
benbierens ea767e23ae Merge branch 'feature/labeling-pods' 2023-08-10 14:25:43 +02:00
benbierens 3de8e29198 turns on metrics by default for deployment 2023-08-10 14:25:32 +02:00
benbierens c7e5ddb0cb additional application ids, fixes deployment namespace issue 2023-08-10 13:58:50 +02:00
benbierens 683cc882be dash is disallowed 2023-08-10 11:59:57 +02:00
benbierens 9c1b469f0d same for fixture name 2023-08-10 11:56:29 +02:00
benbierens 4995665fe5 deals with adhoc test context categories 2023-08-10 11:53:49 +02:00
benbierens 5a861df968 local tests passed 2023-08-10 11:47:34 +02:00
benbierens 01ec6f7d8b sets up additional pod labels 2023-08-10 11:25:22 +02:00
benbierens 17673e8fa5 cleanup some workflow/lifecycle madness 2023-08-10 10:58:18 +02:00
benbierens 9a63a82a49 Merge branch 'master' into feature/labeling-pods
# Conflicts:
#	DistTestCore/Marketplace/CodexContractsContainerRecipe.cs
2023-08-10 09:50:15 +02:00
benbierens 061694f83a Adds option to set block TTL from codex-setup 2023-08-10 09:44:23 +02:00
benbierens ed5b2d5b80 bumps to latest contracts image 2023-08-10 08:39:32 +02:00
benbierens 68d19353aa moves to 80MB and long timeouts 2023-08-08 14:42:59 +02:00
benbierens eaf5db5e91 Run test against increasing file sizes 2023-08-08 10:45:16 +02:00
benbierens 598dc766fa use extra log codex image 2023-08-08 09:44:38 +02:00
benbierens ab42fa8004 removes just-wait tests 2023-08-07 18:31:13 +02:00
benbierens ee1c11542e Merge branch 'master' into spike/local-continuous-debug 2023-08-07 18:28:18 +02:00
benbierens 0259c30bb5 Wires up pod labels 2023-08-07 15:51:44 +02:00
benbierens 1e7470d476 Lays out the plan 2023-08-07 15:07:20 +02:00
benbierens 9840c50e9f Rolls back node version in contracts container. 2023-08-07 14:33:04 +02:00
benbierens 2bafab8b22 Adds check against failure to deploy codex contracts 2023-08-07 11:21:26 +02:00
benbierens 5ad2aa4c6a Merge branch 'master' into spike/local-continuous-debug 2023-08-07 10:58:27 +02:00
benbierens 85b8b6122f Merge branch 'feature/fix-pod-multiples' 2023-08-07 10:51:27 +02:00
benbierens 8a6c5e95ee Makes sure codex nodes are always started in their own pods. 2023-08-07 10:44:48 +02:00
benbierens 771a952f02 Download container logs when EnsureOnline fails 2023-08-04 08:19:03 +02:00
benbierens 9fab7757b0 Pins codex image to fixed hash 2023-08-04 07:55:46 +02:00
benbierens 3884a6a7f7 a 2023-08-02 15:11:27 +02:00
benbierens 4234719e53 Adds continuous test net report for July 2023 2023-08-02 14:44:15 +02:00
benbierens 032506535f Merge branch 'feature/codex-version-info' 2023-08-02 14:43:39 +02:00
benbierens 5675c1d77a use codex version info in logging 2023-07-31 11:51:29 +02:00
benbierens 29137263fb Merge branch 'feature/docker-image-testruns' 2023-07-31 09:56:57 +02:00
benbierens 8d229f1720 cleans up waiting for storage contract state helper methods. 2023-07-31 09:56:37 +02:00
benbierens 9a7946c863 Basic tests pass 2023-07-31 09:16:52 +02:00
benbierens 10ffd29257 Cleanup 2023-07-21 09:44:46 +02:00
benbierens 933932b091 Fixes fetching of docker image for status log 2023-07-21 09:42:38 +02:00
benbierens dbbd05ea97 Merge branch 'master' into feature/docker-image-testruns
# Conflicts:
#	DistTestCore/Codex/CodexContainerRecipe.cs
2023-07-21 09:34:37 +02:00
benbierens dc4749aca6 Merge branch 'feature/log-continuous-downloader' 2023-07-21 09:31:28 +02:00
benbierens ce7ad1ba70 restores downloader default config 2023-07-21 09:29:14 +02:00
benbierens 71f45ca5ea Merge branch 'master' into feature/log-continuous-downloader 2023-07-21 09:25:41 +02:00
benbierens 754e74f6d8 Merge branch 'feature/logs-update-for-kibana' 2023-07-21 09:23:31 +02:00
benbierens 344c004299 Adds test duration to status log 2023-07-21 09:20:28 +02:00
Slava 07b85bda5e Add new variables to the job/pod (#35) 2023-07-21 10:03:10 +03:00
benbierens 8b3589a489 Adds one post-test log download 2023-07-21 09:00:07 +02:00
benbierens c762e30331 autoconfirms deploy 2023-07-21 07:58:10 +02:00
benbierens 0807b16aa4 Merge branch 'master' into feature/log-continuous-downloader 2023-07-21 07:45:05 +02:00
benbierens 0f9e6b6b58 Bumps to latest codex image 2023-07-21 07:44:13 +02:00
benbierens 9de7e1d320 Moves log downloader into continous test runner 2023-07-20 13:37:11 +02:00
benbierens 2dd5775296 Very simple log appender 2023-07-20 10:52:19 +02:00
benbierens f2517b2ade Bump to latest codex master image 2023-07-20 09:02:17 +02:00
benbierens c744fd12c3 Rigs sh scripts for debugging of continuous tests. 2023-07-19 08:49:43 +02:00
benbierens bbf66bb0f0 Cleanup of full connectivity helpers 2023-07-18 16:07:52 +02:00
benbierens bfefba9633 Fixes large discovery tests 2023-07-18 14:26:21 +02:00
benbierens d6d32bf8e6 Adds category to status log. Updates timestamp format 2023-07-18 13:18:26 +02:00
benbierens 07776dc6fb Adds log tag to each log file. 2023-07-18 11:08:05 +02:00
benbierens 87d346ee20 Merge branch 'master' into feature/logs-update-for-kibana 2023-07-18 10:39:47 +02:00
benbierens fa1057ddfd Sets correct output filename for status log. 2023-07-18 10:39:27 +02:00
benbierens 009ce1e9f3 Wires up the status log. 2023-07-18 09:47:44 +02:00
benbierens f50b8dfc83 Sets up StatusLog 2023-07-18 09:02:41 +02:00
benbierens 78434048b7 Example docker-compose 2023-07-17 11:29:27 +02:00
benbierens fa7ba9149e Sets docker images in constructor of recipes 2023-07-17 11:12:14 +02:00
benbierens 6147c14c9b setting up 2023-07-17 09:26:54 +02:00
328 changed files with 14710 additions and 5758 deletions
+19 -11
View File
@@ -15,19 +15,23 @@ on:
workflow_dispatch:
inputs:
branch:
description: Branch
description: Branch (master)
required: false
type: string
source:
description: Repository with tests
description: Repository with tests (current)
required: false
type: string
nameprefix:
description: Runner job/pod name prefix
description: Runner prefix (codex-dist-tests)
required: false
type: string
namespace:
description: Kubernetes namespace for runner
description: Runner namespace (default)
required: false
type: string
command:
description: Runner command (dotnet test Tests)
required: false
type: string
@@ -35,8 +39,9 @@ on:
env:
BRANCH: ${{ github.ref_name }}
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
NAMEPREFIX: cs-codex-dist-tests
NAMESPACE: cs-codex-dist-tests
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
@@ -52,10 +57,14 @@ jobs:
- name: Variables
run: |
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.nameprefix }}" ]] && echo "NAMEPREFIX=${{ inputs.nameprefix }}" >>"$GITHUB_ENV" || echo "NAMEPREFIX=${{ env.NAMEPREFIX }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
[[ -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
@@ -69,5 +78,4 @@ jobs:
- name: Kubectl - Create Job
run: |
export RUNID=$(date +%Y%m%d-%H%M%S)
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
+177
View File
@@ -0,0 +1,177 @@
name: Docker - Reusable
on:
workflow_call:
inputs:
docker_file:
default: docker/Dockerfile
description: Dockerfile
required: false
type: string
docker_repo:
default: codexstorage/cs-codex-dist-tests
description: DockerHub repository
required: false
type: string
tag_latest:
default: true
description: Set latest tag for Docker images
required: false
type: boolean
tag_sha:
default: true
description: Set Git short commit as Docker tag
required: false
type: boolean
tag_suffix:
default: ''
description: Suffix for Docker images tag
required: false
type: string
env:
DOCKER_FILE: ${{ inputs.docker_file }}
DOCKER_REPO: ${{ inputs.docker_repo }}
TAG_LATEST: ${{ inputs.tag_latest }}
TAG_SHA: ${{ inputs.tag_sha }}
TAG_SUFFIX: ${{ inputs.tag_suffix }}
jobs:
# Build platform specific image
build:
strategy:
fail-fast: true
matrix:
target:
- os: linux
arch: amd64
- os: linux
arch: arm64
include:
- target:
os: linux
arch: amd64
builder: ubuntu-22.04
- target:
os: linux
arch: arm64
builder: buildjet-4vcpu-ubuntu-2204-arm
name: Build ${{ matrix.target.os }}/${{ matrix.target.arch }}
runs-on: ${{ matrix.builder }}
env:
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.DOCKER_REPO }}
- name: Docker - Set up Buildx
uses: docker/setup-buildx-action@v2
- name: Docker - Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Build and Push by digest
id: build
uses: docker/build-push-action@v4
with:
context: .
file: ${{ env.DOCKER_FILE }}
platforms: ${{ env.PLATFORM }}
push: true
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true
- name: Docker - Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Docker - Upload digest
uses: actions/upload-artifact@v3
with:
name: digests
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Publish multi-platform image
publish:
name: Publish multi-platform image
runs-on: ubuntu-latest
needs: build
steps:
- name: Docker - Variables
run: |
# Adjust custom suffix when set and
if [[ -n "${{ env.TAG_SUFFIX }}" ]]; then
echo "TAG_SUFFIX=-${{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
fi
# Disable SHA tags on tagged release
if [[ ${{ startsWith(github.ref, 'refs/tags/') }} == "true" ]]; then
echo "TAG_SHA=false" >>$GITHUB_ENV
fi
# Handle latest and latest-custom using raw
if [[ ${{ env.TAG_SHA }} == "false" ]]; then
echo "TAG_LATEST=false" >>$GITHUB_ENV
echo "TAG_RAW=true" >>$GITHUB_ENV
if [[ -z "${{ env.TAG_SUFFIX }}" ]]; then
echo "TAG_RAW_VALUE=latest" >>$GITHUB_ENV
else
echo "TAG_RAW_VALUE=latest-{{ env.TAG_SUFFIX }}" >>$GITHUB_ENV
fi
else
echo "TAG_RAW=false" >>$GITHUB_ENV
fi
- name: Docker - Download digests
uses: actions/download-artifact@v3
with:
name: digests
path: /tmp/digests
- name: Docker - Set up Buildx
uses: docker/setup-buildx-action@v2
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.DOCKER_REPO }}
flavor: |
latest=${{ env.TAG_LATEST }}
suffix=${{ env.TAG_SUFFIX }},onlatest=true
tags: |
type=semver,pattern={{version}}
type=raw,enable=${{ env.TAG_RAW }},value=latest
type=sha,enable=${{ env.TAG_SHA }}
- name: Docker - Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.DOCKER_REPO }}@sha256:%s ' *)
- name: Docker - Inspect image
run: |
docker buildx imagetools inspect ${{ env.DOCKER_REPO }}:${{ steps.meta.outputs.version }}
+5 -106
View File
@@ -11,113 +11,12 @@ on:
- docker/Dockerfile
- docker/docker-entrypoint.sh
- .github/workflows/docker.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
env:
DOCKER_FILE: docker/Dockerfile
DOCKER_REPO: codexstorage/cs-codex-dist-tests
jobs:
# Build platform specific image
build:
strategy:
fail-fast: true
matrix:
target:
- os: linux
arch: amd64
- os: linux
arch: arm64
include:
- target:
os: linux
arch: amd64
builder: ubuntu-22.04
- target:
os: linux
arch: arm64
builder: buildjet-4vcpu-ubuntu-2204-arm
name: Build ${{ matrix.target.os }}/${{ matrix.target.arch }}
runs-on: ${{ matrix.builder }}
outputs:
tags-linux-amd64: ${{ steps.tags.outputs.tags-linux-amd64 }}
tags-linux-arm64: ${{ steps.tags.outputs.tags-linux-arm64 }}
env:
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
SUFFIX: ${{ format('{0}-{1}', 'linux', matrix.target.arch) }}
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.DOCKER_REPO }}
flavor: |
latest=false
tags: |
type=semver,pattern={{version}},suffix=-${{ env.SUFFIX }}
type=sha,suffix=-${{ env.SUFFIX }},enable=${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Docker - Set tags output
id: tags
run: |
if [[ '${{ matrix.target.os }}' == 'linux' && '${{ matrix.target.arch }}' == 'amd64' ]]; then
echo "tags-linux-amd64=${{ steps.meta.outputs.tags }}" >> "$GITHUB_OUTPUT"
elif [[ '${{ matrix.target.os }}' == 'linux' && '${{ matrix.target.arch }}' == 'arm64' ]]; then
echo "tags-linux-arm64=${{ steps.meta.outputs.tags }}" >> "$GITHUB_OUTPUT"
fi
- name: Docker - Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Build and Push
uses: docker/build-push-action@v4
with:
context: .
file: ${{ env.DOCKER_FILE }}
platforms: ${{ env.PLATFORM }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Publish single image
publish:
name: Push single image
runs-on: ubuntu-latest
needs: build
steps:
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.DOCKER_REPO }}
tags: |
type=semver,pattern={{version}}
type=sha,enable=${{ !startsWith(github.ref, 'refs/tags/') }}
- name: Docker - Set tags
run: |
# Transform multi-line tags in to the comma-seperated
TAGS=$(echo "${{ steps.meta.outputs.tags }}" | tr '\n' ',' | awk '{gsub(/,$/,"");}1')
echo "TAGS=${TAGS}" >>$GITHUB_ENV
- name: Docker - Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Create and push manifest images
uses: Noelware/docker-manifest-action@master
with:
inputs: ${{ env.TAGS }}
images: ${{ needs.build.outputs.tags-linux-amd64 }},${{ needs.build.outputs.tags-linux-arm64 }}
push: true
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
secrets: inherit
+86
View File
@@ -0,0 +1,86 @@
# Distributed System Tests for Nim-Codex
## Contributing plugins
The testing framework was created for testing Codex. However, it's been designed such that other distributed/containerized projects can 'easily' be added. In order to add your project to the framework you must:
1. Create a library assembly in the project plugins folder.
1. It must contain a type that implements the `IProjectPlugin` interface from the `Core` assembly.
1. If your plugin wants to expose any specific methods or objects to the code using the framework (the tests and tools), it must implement extensions for the `CoreInterface` type.
## Constructors & Tools
Your implementation of `IProjectPlugin` must have a public constructor with a single argument of type `IPluginTools`, for example:
```C#
public class MyPlugin : IProjectPlugin
{
public MyPlugin(IPluginTools tools)
{
...
}
...
}
```
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients for containers. (Without having to figure out addresses manually.)
## Plugin Interfaces
The `IProjectPlugin` interface requires the implementation of two methods.
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version information at this time if applicable.
1. `Decommission` - Should your plugin have any active system resources, free them in this method.
There are a few optional interfaces your plugin may choose to implement. The framework will automatically use these interfaces.
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string with will be prepended to all log statements made by your plugin.
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by code that uses your plugin.
## Core Interface
Any functionality your plugin wants to expose to code which uses the framework will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
```C#
public static class CoreInterfaceExtensions
{
public static MyPluginReturnType DoSomethingCool(this CoreInterface ci, string someArgument)
{
return Plugin(ci).SomethingCool(someArgument);
}
private static MyPlugin Plugin(CoreInterface ci)
{
return ci.GetPlugin<MyPlugin>();
}
}
```
While technically you can build whatever you like on top of the `CoreInterface` and your own plugin types, I recommend that you follow the approach explained below.
## Deploying, Wrapping, and Starting
When building a plugin, it is important to make as few assumptions as possible about how it will be used by whoever is going to use the framework. For this reason, I recommend you expose three kinds of methods using your `CoreInterface` extensions:
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning containers as a result. If your project requires additional information, you can create a new class type to contain both it and the containers created.
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain.
1. Start - This kind of method does both, simply calling a Deploy method first, then a Wrap method, and returns the result.
Here's an example:
```C#
public static class CoreInterfaceExtensions
{
public static RunningContainers DeployMyProject(this CoreInterface ci, string someArgument)
{
// `RunningContainers` is a framework type. It contains all necessary information about a deployed container. It is serializable.
// Should you need to return any additional information, create a new type that contains it as well as the container information. Make sure it is serializable.
return Plugin(ci).DeployMyProjectContainer(someArgument); // <-- This method should use the `PluginTools.CreateWorkflow()` tool to deploy a container with a configuration that matches someArguments.
}
public static IMyProjectNode WrapMyProjectContainer(this CoreInterface ci, RunningContainers container)
{
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` tool to create an HTTP client for the container, then wrap it in an object that
// represents the API of your project.
}
public static IMyProjectNode StartMyProject(this CoreInterface ci, string someArgument)
{
// Start is now nothing more than a convenience method, combining the previous two.
var rc = ci.DeployMyProject(someArgument);
return WrapMyProjectContainer(ci, rc);
}
}
```
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
+35 -19
View File
@@ -1,24 +1,40 @@
# Distributed System Tests for Nim-Codex
## Contributing tests
Do you want to write some tests for Codex using this distributed test setup? Great! Here's what you do.
Do you want to write some tests using this distributed test setup? Great! Here's what you do.
1. Create a branch. Name it something descriptive, but start it with `tests/` please. [Example: `tests/data-redundancy`.]
1. Checkout your branch, and decide if your tests will be 'short' tests (minutes to hours), or 'long' tests (hours to days), or both! Create a folder for your tests in the matching folders (`/Tests`, `/LongTests`) and don't worry: You can always move your tests later if you like. [Example, short: `/Tests/DataRedundancy/`, long: `/LongTests/DataRedundancy/`]
1. Create one or more code files in your new folder, and write some tests! Here are some tips to help you get started. You can always take a look at the example tests found in [`/Tests/BasicTests/ExampleTests.cs`](/Tests/BasicTests/ExampleTests.cs)
1. Set up a standard NUnit test fixture.
1. Inherrit from `DistTest` or `AutoBootstrapDistTest`.
1. When using `DistTest`:
1. You must start your own Codex bootstrap node. You can use `SetupCodexBootstrapNode(...)` for this.
1. When you start other Codex nodes with `SetupCodexNodes(...)` you can pass the bootstrap node by adding the `.WithBootstrapNode(...)` option.
1. When using `AutoBootstrapDistTest`:
1. The test-infra creates the bootstrap node for you, and automatically passes it to each Codex node you create in your tests. Handy for keeping your tests clean and to-the-point.
1. When using the auto-bootstrap, you have no control over the bootstrap node from your tests. You can't (for example) shut it down during the course of the test. If you need this level of control for your scenario, use the `DistTest` instead.
1. You can generate files of random test data by calling `GenerateTestFile(...)`.
1. If your test needs a long time to run, add the `[UseLongTimeouts]` function attribute. This will greatly increase maximum time-out values for operations like for example uploading and downloading files.
1. You can enable access to the Codex node metrics by adding the option `.EnableMetrics()`. Enabling metrics will make the test-infra download and save all Codex metrics in case of a test failure. (The metrics are stored as CSV, in the same location as the test log file.)
1. You can enable access to the blockchain marketplace by adding the option `.EnableMarketplace(...)`.
1. Enabling metrics and/or enabling the marketplace takes extra resources from the test-infra and increases the time needed during Codex node setup. Please don't enable these features unless your tests need them.
1. Tip: Codex nodes can be named. Use the option `WithName(...)` and make reading your test logs a little nicer!
1. Tip: Commit often.
1. Once you're happy with your tests, please create a pull-request and ask (another) Codex core contributor to review your changes.
1. Checkout your branch.
1. Create a new assembly in the `/Tests` folder. This can be an NUnit test assembly or simply a console app.
1. Add Project references to `Core`, as well as any project plugin you'll be using.
1. Write tests! Use existing tests for inspiration.
## Tips for writing tests for Codex
### Transient tests
1. Add new code files to `Tests/CodexTests`
1. Inherrit from `CodexDistTest` or `AutoBootstrapDistTest`.
1. When using `CodexDistTest`:
1. You must start your own Codex bootstrap node. You can use `AddCodex(...)` for this.
1. When you start other Codex nodes with `AddCodex(...)` you can pass the bootstrap node by adding the `.WithBootstrapNode(...)` option.
1. When using `AutoBootstrapDistTest`:
1. The test-infra creates the bootstrap node for you, and automatically passes it to each Codex node you create in your tests. Handy for keeping your tests clean and to-the-point.
1. When using the auto-bootstrap, you have no control over the bootstrap node from your tests. You can't (for example) shut it down during the course of the test. If you need this level of control for your scenario, use the `CodexDistTest` instead.
1. If your test needs a long time to run, add the `[UseLongTimeouts]` function attribute. This will greatly increase maximum time-out values for operations like for example uploading and downloading files.
### Continuous tests
1. Add new code files to `Tests/CodexContinousTests/Tests`
1. Inherrit from `ContinuousTest`
1. Define one or more methods and decorate them with the `[TestMoment(...)]` attribute.
1. The TestMoment takes a number of seconds as argument. Each moment will be executed by the continuous test runner applying the given seconds as delay. (Non-cumulative. So two moments at T:10 will be executed one after another without delay, in this case the order of execution should not be depended upon.)
1. Continuous tests automatically receive access to the Codex nodes that the tests are being run against.
1. Additionally, Continuous tests can start their own transient Codex nodes and bootstrap them against the persistent nodes.
### Tips for either type of test
1. You can generate files of random test data by calling `GenerateTestFile(...)`.
1. You can enable access to the Codex node metrics by adding the option `.EnableMetrics()`. Enabling metrics will make the test-infra download and save all Codex metrics in case of a test failure. (The metrics are stored as CSV, in the same location as the test log file.)
1. You can enable access to the blockchain marketplace by adding the option `.EnableMarketplace(...)`.
1. Enabling metrics and/or enabling the marketplace takes extra resources from the test-infra and increases the time needed during Codex node setup. Please don't enable these features unless your tests need them.
1. Tip: Codex nodes can be named. Use the option `WithName(...)` and make reading your test logs a little nicer!
1. Tip: Commit often.
## Don't forget
1. Once you're happy with your tests, please create a pull-request and ask a Codex core contributor to review your changes.
-99
View File
@@ -1,99 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using DistTestCore.Marketplace;
using KubernetesWorkflow;
namespace CodexNetDeployer
{
public class CodexNodeStarter
{
private readonly Configuration config;
private readonly WorkflowCreator workflowCreator;
private readonly TestLifecycle lifecycle;
private readonly GethStartResult gethResult;
private string bootstrapSpr = "";
private int validatorsLeft;
public CodexNodeStarter(Configuration config, WorkflowCreator workflowCreator, TestLifecycle lifecycle, GethStartResult gethResult, int numberOfValidators)
{
this.config = config;
this.workflowCreator = workflowCreator;
this.lifecycle = lifecycle;
this.gethResult = gethResult;
validatorsLeft = numberOfValidators;
}
public RunningContainer? Start(int i)
{
Console.Write($" - {i} = ");
var workflow = workflowCreator.CreateWorkflow();
var workflowStartup = new StartupConfig();
workflowStartup.Add(gethResult);
workflowStartup.Add(CreateCodexStartupConfig(bootstrapSpr, i, validatorsLeft));
var containers = workflow.Start(1, Location.Unspecified, new CodexContainerRecipe(), workflowStartup);
var container = containers.Containers.First();
var codexAccess = new CodexAccess(lifecycle.Log, container, lifecycle.TimeSet, lifecycle.Configuration.GetAddress(container));
var account = gethResult.MarketplaceNetwork.Bootstrap.AllAccounts.Accounts[i];
var tokenAddress = gethResult.MarketplaceNetwork.Marketplace.TokenAddress;
var marketAccess = new MarketplaceAccess(lifecycle, gethResult.MarketplaceNetwork, account, codexAccess);
try
{
var debugInfo = codexAccess.GetDebugInfo();
if (!string.IsNullOrWhiteSpace(debugInfo.spr))
{
Console.Write("Online\t");
var interaction = gethResult.MarketplaceNetwork.Bootstrap.StartInteraction(lifecycle);
interaction.MintTestTokens(new[] { account.Account }, config.InitialTestTokens, tokenAddress);
Console.Write("Tokens minted\t");
var response = marketAccess.MakeStorageAvailable(
totalSpace: config.StorageSell!.Value.MB(),
minPriceForTotalSpace: config.MinPrice.TestTokens(),
maxCollateral: config.MaxCollateral.TestTokens(),
maxDuration: TimeSpan.FromSeconds(config.MaxDuration));
if (!string.IsNullOrEmpty(response))
{
Console.Write("Storage available\tOK" + Environment.NewLine);
if (string.IsNullOrEmpty(bootstrapSpr)) bootstrapSpr = debugInfo.spr;
validatorsLeft--;
return container;
}
}
}
catch (Exception ex)
{
Console.WriteLine("Exception:" + ex.ToString());
}
Console.Write("Unknown failure. Downloading container log." + Environment.NewLine);
lifecycle.DownloadLog(container);
return null;
}
private CodexStartupConfig CreateCodexStartupConfig(string bootstrapSpr, int i, int validatorsLeft)
{
var codexStart = new CodexStartupConfig(config.CodexLogLevel);
if (!string.IsNullOrEmpty(bootstrapSpr)) codexStart.BootstrapSpr = bootstrapSpr;
codexStart.StorageQuota = config.StorageQuota!.Value.MB();
var marketplaceConfig = new MarketplaceInitialConfig(100000.Eth(), 0.TestTokens(), validatorsLeft > 0);
marketplaceConfig.AccountIndexOverride = i;
codexStart.MarketplaceConfig = marketplaceConfig;
codexStart.MetricsEnabled = config.RecordMetrics;
if (config.BlockTTL != Configuration.SecondsIn1Day)
{
codexStart.BlockTTL = config.BlockTTL;
}
return codexStart;
}
}
}
-99
View File
@@ -1,99 +0,0 @@
using ArgsUniform;
using DistTestCore;
using DistTestCore.Codex;
namespace CodexNetDeployer
{
public class Configuration
{
public const int SecondsIn1Day = 24 * 60 * 60;
[Uniform("kube-config", "kc", "KUBECONFIG", false, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
public string KubeConfigFile { get; set; } = "null";
[Uniform("kube-namespace", "kn", "KUBENAMESPACE", true, "Kubernetes namespace to be used for deployment.")]
public string KubeNamespace { get; set; } = string.Empty;
[Uniform("nodes", "n", "NODES", true, "Number of Codex nodes to be created.")]
public int? NumberOfCodexNodes { get; set; }
[Uniform("validators", "v", "VALIDATORS", true, "Number of Codex nodes that will be validating.")]
public int? NumberOfValidators { get; set; }
[Uniform("storage-quota", "sq", "STORAGEQUOTA", true, "Storage quota in megabytes used by each Codex node.")]
public int? StorageQuota { get; set; }
[Uniform("storage-sell", "ss", "STORAGESELL", true, "Number of megabytes of storage quota to make available for selling.")]
public int? StorageSell { get; set; }
[Uniform("log-level", "l", "LOGLEVEL", true, "Log level used by each Codex node. [Trace, Debug*, Info, Warn, Error]")]
public CodexLogLevel CodexLogLevel { get; set; } = CodexLogLevel.Debug;
[Uniform("test-tokens", "tt", "TESTTOKENS", true, "Initial amount of test-tokens minted for each Codex node.")]
public int InitialTestTokens { get; set; } = int.MaxValue;
[Uniform("min-price", "mp", "MINPRICE", true, "Minimum price for the storage space for which contracts will be accepted.")]
public int MinPrice { get; set; }
[Uniform("max-collateral", "mc", "MAXCOLLATERAL", true, "Maximum collateral that will be placed for the total storage space.")]
public int MaxCollateral { get; set; }
[Uniform("max-duration", "md", "MAXDURATION", true, "Maximum duration in seconds for contracts which will be accepted.")]
public int MaxDuration { get; set; }
[Uniform("block-ttl", "bt", "BLOCKTTL", false, "Block timeout in seconds. Default is 24 hours.")]
public int BlockTTL { get; set; } = SecondsIn1Day;
[Uniform("record-metrics", "rm", "RECORDMETRICS", false, "If true, metrics will be collected for all Codex nodes.")]
public bool RecordMetrics { get; set; } = false;
public TestRunnerLocation RunnerLocation { get; set; } = TestRunnerLocation.InternalToCluster;
public List<string> Validate()
{
var errors = new List<string>();
ForEachProperty(
onString: (n, v) => StringIsSet(n, v, errors),
onInt: (n, v) => IntIsOverZero(n, v, errors));
if (NumberOfValidators > NumberOfCodexNodes)
{
errors.Add($"{nameof(NumberOfValidators)} ({NumberOfValidators}) may not be greater than {nameof(NumberOfCodexNodes)} ({NumberOfCodexNodes}).");
}
if (StorageSell.HasValue && StorageQuota.HasValue && StorageSell.Value >= StorageQuota.Value)
{
errors.Add("StorageSell cannot be greater than or equal to StorageQuota.");
}
return errors;
}
private void ForEachProperty(Action<string, string> onString, Action<string, int?> onInt)
{
var properties = GetType().GetProperties();
foreach (var p in properties)
{
if (p.PropertyType == typeof(string)) onString(p.Name, (string)p.GetValue(this)!);
if (p.PropertyType == typeof(int?)) onInt(p.Name, (int?)p.GetValue(this)!);
if (p.PropertyType == typeof(int)) onInt(p.Name, (int)p.GetValue(this)!);
}
}
private static void IntIsOverZero(string variable, int? value, List<string> errors)
{
if (value == null || value.Value < 1)
{
errors.Add($"{variable} must be set and must be greater than 0.");
}
}
private static void StringIsSet(string variable, string value, List<string> errors)
{
if (string.IsNullOrWhiteSpace(value))
{
errors.Add($"{variable} must be set.");
}
}
}
}
-120
View File
@@ -1,120 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using KubernetesWorkflow;
using Logging;
namespace CodexNetDeployer
{
public class Deployer
{
private readonly Configuration config;
private readonly NullLog log;
private readonly DefaultTimeSet timeset;
public Deployer(Configuration config)
{
this.config = config;
log = new NullLog();
timeset = new DefaultTimeSet();
}
public CodexDeployment Deploy()
{
Log("Initializing...");
var (workflowCreator, lifecycle) = CreateFacilities();
Log("Preparing configuration...");
// We trick the Geth companion node into unlocking all of its accounts, by saying we want to start 999 codex nodes.
var setup = new CodexSetup(999, config.CodexLogLevel);
setup.WithStorageQuota(config.StorageQuota!.Value.MB()).EnableMarketplace(0.TestTokens());
setup.MetricsEnabled = config.RecordMetrics;
Log("Creating Geth instance and deploying contracts...");
var gethStarter = new GethStarter(lifecycle, workflowCreator);
var gethResults = gethStarter.BringOnlineMarketplaceFor(setup);
Log("Geth started. Codex contracts deployed.");
Log("Warning: It can take up to 45 minutes for the Geth node to finish unlocking all if its 1000 preconfigured accounts.");
// It takes a second for the geth node to unlock a single account. Let's assume 3.
// We can't start the codex nodes until their accounts are definitely unlocked. So
// We wait:
Thread.Sleep(TimeSpan.FromSeconds(3.0 * config.NumberOfCodexNodes!.Value));
Log("Starting Codex nodes...");
// Each node must have its own IP, so it needs it own pod. Start them 1 at a time.
var codexStarter = new CodexNodeStarter(config, workflowCreator, lifecycle, gethResults, config.NumberOfValidators!.Value);
var codexContainers = new List<RunningContainer>();
for (var i = 0; i < config.NumberOfCodexNodes; i++)
{
var container = codexStarter.Start(i);
if (container != null) codexContainers.Add(container);
}
var prometheusContainer = StartMetricsService(lifecycle, setup, codexContainers);
return new CodexDeployment(gethResults, codexContainers.ToArray(), prometheusContainer, CreateMetadata());
}
private (WorkflowCreator, TestLifecycle) CreateFacilities()
{
var kubeConfig = GetKubeConfig(config.KubeConfigFile);
var lifecycleConfig = new DistTestCore.Configuration
(
kubeConfigFile: kubeConfig,
logPath: "null",
logDebug: false,
dataFilesPath: "notUsed",
codexLogLevel: config.CodexLogLevel,
runnerLocation: config.RunnerLocation
);
var kubeFlowConfig = new KubernetesWorkflow.Configuration(
k8sNamespacePrefix: config.KubeNamespace,
kubeConfigFile: kubeConfig,
operationTimeout: timeset.K8sOperationTimeout(),
retryDelay: timeset.WaitForK8sServiceDelay());
var workflowCreator = new WorkflowCreator(log, kubeFlowConfig, testNamespacePostfix: string.Empty);
var lifecycle = new TestLifecycle(log, lifecycleConfig, timeset, workflowCreator);
return (workflowCreator, lifecycle);
}
private RunningContainer? StartMetricsService(TestLifecycle lifecycle, CodexSetup setup, List<RunningContainer> codexContainers)
{
if (!setup.MetricsEnabled) return null;
Log("Starting metrics service...");
var runningContainers = new RunningContainers(null!, null!, codexContainers.ToArray());
return lifecycle.PrometheusStarter.CollectMetricsFor(runningContainers).Containers.Single();
}
private string? GetKubeConfig(string kubeConfigFile)
{
if (string.IsNullOrEmpty(kubeConfigFile) || kubeConfigFile.ToLowerInvariant() == "null") return null;
return kubeConfigFile;
}
private DeploymentMetadata CreateMetadata()
{
return new DeploymentMetadata(
kubeNamespace: config.KubeNamespace,
numberOfCodexNodes: config.NumberOfCodexNodes!.Value,
numberOfValidators: config.NumberOfValidators!.Value,
storageQuotaMB: config.StorageQuota!.Value,
codexLogLevel: config.CodexLogLevel,
initialTestTokens: config.InitialTestTokens,
minPrice: config.MinPrice,
maxCollateral: config.MaxCollateral,
maxDuration: config.MaxDuration);
}
private void Log(string msg)
{
Console.WriteLine(msg);
}
}
}
@@ -1,12 +0,0 @@
dotnet run \
--kube-config=/opt/kubeconfig.yaml \
--kube-namespace=codex-continuous-tests \
--nodes=5 \
--validators=3 \
--storage-quota=2048 \
--storage-sell=1024 \
--min-price=1024 \
--max-collateral=1024 \
--max-duration=3600000 \
--block-ttl=120
-22
View File
@@ -1,22 +0,0 @@
using ArgsUniform;
using DistTestCore;
using DistTestCore.Codex;
namespace CodexNetDownloader
{
public class Configuration
{
[Uniform("output-path", "o", "OUTPUT", true, "Path where files will be written.")]
public string OutputPath { get; set; } = "output";
[Uniform("codex-deployment", "c", "CODEXDEPLOYMENT", true, "Path to codex-deployment JSON file.")]
public string CodexDeploymentJson { get; set; } = string.Empty;
[Uniform("kube-config", "kc", "KUBECONFIG", true, "Path to Kubeconfig file. Use 'null' (default) to use local cluster.")]
public string KubeConfigFile { get; set; } = "null";
public CodexDeployment CodexDeployment { get; set; } = null!;
public TestRunnerLocation RunnerLocation { get; set; } = TestRunnerLocation.InternalToCluster;
}
}
-53
View File
@@ -1,53 +0,0 @@
using ArgsUniform;
using ContinuousTests;
using DistTestCore;
using DistTestCore.Codex;
using Logging;
using Newtonsoft.Json;
public class Program
{
public static void Main(string[] args)
{
var nl = Environment.NewLine;
Console.WriteLine("CodexNetDownloader" + nl);
var uniformArgs = new ArgsUniform<CodexNetDownloader.Configuration>(PrintHelp, args);
var config = uniformArgs.Parse(true);
if (args.Any(a => a == "--external"))
{
config.RunnerLocation = TestRunnerLocation.ExternalToCluster;
}
config.CodexDeployment = ParseCodexDeploymentJson(config.CodexDeploymentJson);
if (!Directory.Exists(config.OutputPath)) Directory.CreateDirectory(config.OutputPath);
var k8sFactory = new K8sFactory();
var (_, lifecycle) = k8sFactory.CreateFacilities(config.KubeConfigFile, config.OutputPath, "dataPath", config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), new NullLog(), config.RunnerLocation);
foreach (var container in config.CodexDeployment.CodexContainers)
{
lifecycle.DownloadLog(container);
}
Console.WriteLine("Done!");
}
private static CodexDeployment ParseCodexDeploymentJson(string filename)
{
var d = JsonConvert.DeserializeObject<CodexDeployment>(File.ReadAllText(filename))!;
if (d == null) throw new Exception("Unable to parse " + filename);
return d;
}
private static void PrintHelp()
{
var nl = Environment.NewLine;
Console.WriteLine("CodexNetDownloader lets you download all container logs given a codex-deployment.json file." + nl);
Console.WriteLine("CodexNetDownloader assumes you are running this tool from *inside* the Kubernetes cluster. " +
"If you are not running this from a container inside the cluster, add the argument '--external'." + nl);
}
}
-20
View File
@@ -1,20 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using KubernetesWorkflow;
using Logging;
namespace ContinuousTests
{
public class CodexAccessFactory
{
public CodexAccess[] Create(Configuration config, RunningContainer[] containers, BaseLog log, ITimeSet timeSet)
{
return containers.Select(container =>
{
var address = container.ClusterExternalAddress;
if (config.RunnerLocation == TestRunnerLocation.InternalToCluster) address = container.ClusterInternalAddress;
return new CodexAccess(log, container, timeSet, address);
}).ToArray();
}
}
}
-111
View File
@@ -1,111 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using Logging;
namespace ContinuousTests
{
public abstract class ContinuousTestLongTimeouts : ContinuousTest
{
public override ITimeSet TimeSet => new LongTimeSet();
}
public abstract class ContinuousTest
{
protected const int Zero = 0;
protected const int MinuteOne = 60;
protected const int MinuteFive = MinuteOne * 5;
protected const int HourOne = MinuteOne * 60;
protected const int HourThree = HourOne * 3;
protected const int DayOne = HourOne * 24;
protected const int DayThree = DayOne * 3;
private const string UploadFailedMessage = "Unable to store block";
public void Initialize(CodexAccess[] nodes, BaseLog log, FileManager fileManager, Configuration configuration, CancellationToken cancelToken)
{
Nodes = nodes;
Log = log;
FileManager = fileManager;
Configuration = configuration;
CancelToken = cancelToken;
if (nodes != null)
{
NodeRunner = new NodeRunner(Nodes, configuration, TimeSet, Log, CustomK8sNamespace, EthereumAccountIndex);
}
else
{
NodeRunner = null!;
}
}
public CodexAccess[] Nodes { get; private set; } = null!;
public BaseLog Log { get; private set; } = null!;
public IFileManager FileManager { get; private set; } = null!;
public Configuration Configuration { get; private set; } = null!;
public virtual ITimeSet TimeSet { get { return new DefaultTimeSet(); } }
public CancellationToken CancelToken { get; private set; } = new CancellationToken();
public NodeRunner NodeRunner { get; private set; } = null!;
public abstract int RequiredNumberOfNodes { get; }
public abstract TimeSpan RunTestEvery { get; }
public abstract TestFailMode TestFailMode { get; }
public virtual int EthereumAccountIndex { get { return -1; } }
public virtual string CustomK8sNamespace { get { return string.Empty; } }
public string Name
{
get
{
return GetType().Name;
}
}
public ContentId? UploadFile(CodexAccess node, TestFile file)
{
using var fileStream = File.OpenRead(file.Filename);
var logMessage = $"Uploading file {file.Describe()}...";
var response = Stopwatch.Measure(Log, logMessage, () =>
{
return node.UploadFile(fileStream);
});
if (string.IsNullOrEmpty(response)) return null;
if (response.StartsWith(UploadFailedMessage)) return null;
Log.Log($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
}
public TestFile DownloadFile(CodexAccess node, ContentId contentId, string fileLabel = "")
{
var logMessage = $"Downloading for contentId: '{contentId.Id}'...";
var file = FileManager.CreateEmptyTestFile(fileLabel);
Stopwatch.Measure(Log, logMessage, () => DownloadToFile(node, contentId.Id, file));
Log.Log($"Downloaded file {file.Describe()} to '{file.Filename}'.");
return file;
}
private void DownloadToFile(CodexAccess node, string contentId, TestFile file)
{
using var fileStream = File.OpenWrite(file.Filename);
try
{
using var downloadStream = node.DownloadFile(contentId);
downloadStream.CopyTo(fileStream);
}
catch
{
Log.Log($"Failed to download file '{contentId}'.");
throw;
}
}
}
public enum TestFailMode
{
StopAfterFirstFailure,
AlwaysRunAllMoments
}
}
-65
View File
@@ -1,65 +0,0 @@
using DistTestCore;
using Logging;
namespace ContinuousTests
{
public class ContinuousTestRunner
{
private readonly K8sFactory k8SFactory = new K8sFactory();
private readonly ConfigLoader configLoader = new ConfigLoader();
private readonly TestFactory testFactory = new TestFactory();
private readonly Configuration config;
private readonly StartupChecker startupChecker;
private readonly CancellationToken cancelToken;
public ContinuousTestRunner(string[] args, CancellationToken cancelToken)
{
config = configLoader.Load(args);
startupChecker = new StartupChecker(config, cancelToken);
this.cancelToken = cancelToken;
}
public void Run()
{
startupChecker.Check();
var taskFactory = new TaskFactory();
var overviewLog = new FixtureLog(new LogConfig(config.LogPath, false), "Overview");
overviewLog.Log("Continuous tests starting...");
var allTests = testFactory.CreateTests();
ClearAllCustomNamespaces(allTests, overviewLog);
var testLoops = allTests.Select(t => new TestLoop(taskFactory, config, overviewLog, t.GetType(), t.RunTestEvery, cancelToken)).ToArray();
foreach (var testLoop in testLoops)
{
if (cancelToken.IsCancellationRequested) break;
overviewLog.Log("Launching test-loop for " + testLoop.Name);
testLoop.Begin();
Thread.Sleep(TimeSpan.FromSeconds(5));
}
overviewLog.Log("Finished launching test-loops.");
cancelToken.WaitHandle.WaitOne();
overviewLog.Log("Cancelling all test-loops...");
taskFactory.WaitAll();
overviewLog.Log("All tasks cancelled.");
}
private void ClearAllCustomNamespaces(ContinuousTest[] allTests, FixtureLog log)
{
foreach (var test in allTests) ClearAllCustomNamespaces(test, log);
}
private void ClearAllCustomNamespaces(ContinuousTest test, FixtureLog log)
{
if (string.IsNullOrEmpty(test.CustomK8sNamespace)) return;
log.Log($"Clearing namespace '{test.CustomK8sNamespace}'...");
var (workflowCreator, _) = k8SFactory.CreateFacilities(config.KubeConfigFile, config.LogPath, config.DataPath, test.CustomK8sNamespace, new DefaultTimeSet(), log, config.RunnerLocation);
workflowCreator.CreateWorkflow().DeleteTestResources();
}
}
}
-41
View File
@@ -1,41 +0,0 @@
using DistTestCore.Codex;
using DistTestCore;
using KubernetesWorkflow;
using Logging;
namespace ContinuousTests
{
public class K8sFactory
{
public (WorkflowCreator, TestLifecycle) CreateFacilities(string kubeConfigFile, string logPath, string dataFilePath, string customNamespace, ITimeSet timeSet, BaseLog log, TestRunnerLocation runnerLocation)
{
var kubeConfig = GetKubeConfig(kubeConfigFile);
var lifecycleConfig = new DistTestCore.Configuration
(
kubeConfigFile: kubeConfig,
logPath: logPath,
logDebug: false,
dataFilesPath: dataFilePath,
codexLogLevel: CodexLogLevel.Debug,
runnerLocation: runnerLocation
);
var kubeFlowConfig = new KubernetesWorkflow.Configuration(
k8sNamespacePrefix: customNamespace,
kubeConfigFile: kubeConfig,
operationTimeout: timeSet.K8sOperationTimeout(),
retryDelay: timeSet.WaitForK8sServiceDelay());
var workflowCreator = new WorkflowCreator(log, kubeFlowConfig, testNamespacePostfix: string.Empty);
var lifecycle = new TestLifecycle(log, lifecycleConfig, timeSet, workflowCreator);
return (workflowCreator, lifecycle);
}
private static string? GetKubeConfig(string kubeConfigFile)
{
if (string.IsNullOrEmpty(kubeConfigFile) || kubeConfigFile.ToLowerInvariant() == "null") return null;
return kubeConfigFile;
}
}
}
-97
View File
@@ -1,97 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Marketplace;
using DistTestCore;
using KubernetesWorkflow;
using NUnit.Framework;
using Logging;
using Utils;
namespace ContinuousTests
{
public class NodeRunner
{
private readonly K8sFactory k8SFactory = new K8sFactory();
private readonly CodexAccess[] nodes;
private readonly Configuration config;
private readonly ITimeSet timeSet;
private readonly BaseLog log;
private readonly string customNamespace;
private readonly int ethereumAccountIndex;
public NodeRunner(CodexAccess[] nodes, Configuration config, ITimeSet timeSet, BaseLog log, string customNamespace, int ethereumAccountIndex)
{
this.nodes = nodes;
this.config = config;
this.timeSet = timeSet;
this.log = log;
this.customNamespace = customNamespace;
this.ethereumAccountIndex = ethereumAccountIndex;
}
public void RunNode(Action<CodexAccess, MarketplaceAccess, TestLifecycle> operation)
{
RunNode(nodes.ToList().PickOneRandom(), operation, 0.TestTokens());
}
public void RunNode(CodexAccess bootstrapNode, Action<CodexAccess, MarketplaceAccess, TestLifecycle> operation)
{
RunNode(bootstrapNode, operation, 0.TestTokens());
}
public void RunNode(CodexAccess bootstrapNode, Action<CodexAccess, MarketplaceAccess, TestLifecycle> operation, TestToken mintTestTokens)
{
var (workflowCreator, lifecycle) = CreateFacilities();
var flow = workflowCreator.CreateWorkflow();
try
{
var debugInfo = bootstrapNode.GetDebugInfo();
Assert.That(!string.IsNullOrEmpty(debugInfo.spr));
var startupConfig = new StartupConfig();
startupConfig.NameOverride = "TransientNode";
var codexStartConfig = new CodexStartupConfig(CodexLogLevel.Trace);
codexStartConfig.MarketplaceConfig = new MarketplaceInitialConfig(0.Eth(), 0.TestTokens(), false);
codexStartConfig.MarketplaceConfig.AccountIndexOverride = ethereumAccountIndex;
codexStartConfig.BootstrapSpr = debugInfo.spr;
startupConfig.Add(codexStartConfig);
startupConfig.Add(config.CodexDeployment.GethStartResult);
var rc = flow.Start(1, Location.Unspecified, new CodexContainerRecipe(), startupConfig);
var account = config.CodexDeployment.GethStartResult.CompanionNode.Accounts[ethereumAccountIndex];
var marketplaceNetwork = config.CodexDeployment.GethStartResult.MarketplaceNetwork;
if (mintTestTokens.Amount > 0)
{
var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress;
var interaction = marketplaceNetwork.Bootstrap.StartInteraction(lifecycle);
interaction.MintTestTokens(new[] { account.Account }, mintTestTokens.Amount, tokenAddress);
}
var container = rc.Containers[0];
var address = lifecycle.Configuration.GetAddress(container);
var codexAccess = new CodexAccess(log, container, lifecycle.TimeSet, address);
var marketAccess = new MarketplaceAccess(lifecycle, marketplaceNetwork, account, codexAccess);
try
{
operation(codexAccess, marketAccess, lifecycle);
}
catch
{
lifecycle.DownloadLog(container);
throw;
}
}
finally
{
flow.DeleteTestResources();
}
}
private (WorkflowCreator, TestLifecycle) CreateFacilities()
{
return k8SFactory.CreateFacilities(config.KubeConfigFile, config.LogPath, config.DataPath, customNamespace, timeSet, log, config.RunnerLocation);
}
}
}
-227
View File
@@ -1,227 +0,0 @@
using DistTestCore.Codex;
using DistTestCore;
using Logging;
using Utils;
using KubernetesWorkflow;
using NUnit.Framework.Internal;
using System.Reflection;
using static Program;
namespace ContinuousTests
{
public class SingleTestRun
{
private readonly CodexAccessFactory codexNodeFactory = new CodexAccessFactory();
private readonly List<Exception> exceptions = new List<Exception>();
private readonly TaskFactory taskFactory;
private readonly Configuration config;
private readonly BaseLog overviewLog;
private readonly TestHandle handle;
private readonly CancellationToken cancelToken;
private readonly CodexAccess[] nodes;
private readonly FileManager fileManager;
private readonly FixtureLog fixtureLog;
private readonly string testName;
private readonly string dataFolder;
public SingleTestRun(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, TestHandle handle, CancellationToken cancelToken)
{
this.taskFactory = taskFactory;
this.config = config;
this.overviewLog = overviewLog;
this.handle = handle;
this.cancelToken = cancelToken;
testName = handle.Test.GetType().Name;
fixtureLog = new FixtureLog(new LogConfig(config.LogPath, true), testName);
nodes = CreateRandomNodes(handle.Test.RequiredNumberOfNodes);
dataFolder = config.DataPath + "-" + Guid.NewGuid();
fileManager = new FileManager(fixtureLog, CreateFileManagerConfiguration());
}
public void Run(EventWaitHandle runFinishedHandle)
{
taskFactory.Run(() =>
{
try
{
RunTest();
fileManager.DeleteAllTestFiles();
Directory.Delete(dataFolder, true);
runFinishedHandle.Set();
}
catch (Exception ex)
{
overviewLog.Error("Test infra failure: SingleTestRun failed with " + ex);
Environment.Exit(-1);
}
});
}
private void RunTest()
{
try
{
RunTestMoments();
if (!config.KeepPassedTestLogs) fixtureLog.Delete();
}
catch (Exception ex)
{
fixtureLog.Error("Test run failed with exception: " + ex);
fixtureLog.MarkAsFailed();
if (config.StopOnFailure)
{
OverviewLog("Configured to stop on first failure. Downloading cluster logs...");
DownloadClusterLogs();
OverviewLog("Log download finished. Cancelling test runner...");
Cancellation.Cts.Cancel();
}
}
}
private void RunTestMoments()
{
var earliestMoment = handle.GetEarliestMoment();
var t = earliestMoment;
while (true)
{
cancelToken.ThrowIfCancellationRequested();
RunMoment(t);
if (handle.Test.TestFailMode == TestFailMode.StopAfterFirstFailure && exceptions.Any())
{
Log("Exception detected. TestFailMode = StopAfterFirstFailure. Stopping...");
ThrowFailTest();
}
var nextMoment = handle.GetNextMoment(t);
if (nextMoment != null)
{
var delta = TimeSpan.FromSeconds(nextMoment.Value - t);
Log($" > Next TestMoment in {Time.FormatDuration(delta)} seconds...");
cancelToken.WaitHandle.WaitOne(delta);
t = nextMoment.Value;
}
else
{
if (exceptions.Any())
{
ThrowFailTest();
}
OverviewLog(" > Test passed. " + FuturesInfo());
return;
}
}
}
private void ThrowFailTest()
{
var ex = UnpackException(exceptions.First());
Log(ex.ToString());
OverviewLog($" > Test failed {FuturesInfo()}: " + ex.Message);
throw ex;
}
private string FuturesInfo()
{
var containers = config.CodexDeployment.CodexContainers;
var nodes = codexNodeFactory.Create(config, containers, fixtureLog, handle.Test.TimeSet);
var f = nodes.Select(n => n.GetDebugFutures().ToString());
var msg = $"(Futures: [{string.Join(", ", f)}])";
return msg;
}
private void DownloadClusterLogs()
{
var k8sFactory = new K8sFactory();
var (_, lifecycle) = k8sFactory.CreateFacilities(config.KubeConfigFile, config.LogPath, "dataPath", config.CodexDeployment.Metadata.KubeNamespace, new DefaultTimeSet(), new NullLog(), config.RunnerLocation);
foreach (var container in config.CodexDeployment.CodexContainers)
{
lifecycle.DownloadLog(container);
}
}
private Exception UnpackException(Exception exception)
{
if (exception is AggregateException a)
{
return UnpackException(a.InnerExceptions.First());
}
if (exception is TargetInvocationException t)
{
return UnpackException(t.InnerException!);
}
return exception;
}
private void RunMoment(int t)
{
using (var context = new TestExecutionContext.IsolatedContext())
{
try
{
handle.InvokeMoment(t, InitializeTest);
}
catch (Exception ex)
{
exceptions.Add(ex);
}
}
DecommissionTest();
}
private void InitializeTest(string name)
{
Log($" > Running TestMoment '{name}'");
handle.Test.Initialize(nodes, fixtureLog, fileManager, config, cancelToken);
}
private void DecommissionTest()
{
handle.Test.Initialize(null!, null!, null!, null!, cancelToken);
}
private void Log(string msg)
{
fixtureLog.Log(msg);
}
private void OverviewLog(string msg)
{
Log(msg);
var containerNames = $"({string.Join(",", nodes.Select(n => n.Container.Name))})";
overviewLog.Log($"{containerNames} {testName}: {msg}");
}
private CodexAccess[] CreateRandomNodes(int number)
{
var containers = SelectRandomContainers(number);
fixtureLog.Log("Selected nodes: " + string.Join(",", containers.Select(c => c.Name)));
return codexNodeFactory.Create(config, containers, fixtureLog, handle.Test.TimeSet);
}
private RunningContainer[] SelectRandomContainers(int number)
{
var containers = config.CodexDeployment.CodexContainers.ToList();
var result = new RunningContainer[number];
for (var i = 0; i < number; i++)
{
result[i] = containers.PickOneRandom();
}
return result;
}
private DistTestCore.Configuration CreateFileManagerConfiguration()
{
return new DistTestCore.Configuration(null, string.Empty, false, dataFolder,
CodexLogLevel.Error, config.RunnerLocation);
}
}
}
-67
View File
@@ -1,67 +0,0 @@
using Logging;
namespace ContinuousTests
{
public class TestLoop
{
private readonly TaskFactory taskFactory;
private readonly Configuration config;
private readonly BaseLog overviewLog;
private readonly Type testType;
private readonly TimeSpan runsEvery;
private readonly CancellationToken cancelToken;
private readonly EventWaitHandle runFinishedHandle = new EventWaitHandle(true, EventResetMode.ManualReset);
public TestLoop(TaskFactory taskFactory, Configuration config, BaseLog overviewLog, Type testType, TimeSpan runsEvery, CancellationToken cancelToken)
{
this.taskFactory = taskFactory;
this.config = config;
this.overviewLog = overviewLog;
this.testType = testType;
this.runsEvery = runsEvery;
this.cancelToken = cancelToken;
Name = testType.Name;
}
public string Name { get; }
public void Begin()
{
taskFactory.Run(() =>
{
try
{
while (true)
{
WaitHandle.WaitAny(new[] { runFinishedHandle, cancelToken.WaitHandle });
cancelToken.ThrowIfCancellationRequested();
StartTest();
cancelToken.WaitHandle.WaitOne(runsEvery);
}
}
catch (OperationCanceledException)
{
overviewLog.Log("Test-loop " + testType.Name + " is cancelled.");
}
catch (Exception ex)
{
overviewLog.Error("Test infra failure: TestLoop failed with " + ex);
Environment.Exit(-1);
}
});
}
private void StartTest()
{
var test = (ContinuousTest)Activator.CreateInstance(testType)!;
var handle = new TestHandle(test);
var run = new SingleTestRun(taskFactory, config, overviewLog, handle, cancelToken);
runFinishedHandle.Reset();
run.Run(runFinishedHandle);
}
}
}
-60
View File
@@ -1,60 +0,0 @@
using DistTestCore;
using DistTestCore.Codex;
using NUnit.Framework;
namespace ContinuousTests.Tests
{
public class ThresholdChecks : ContinuousTest
{
public override int RequiredNumberOfNodes => 1;
public override TimeSpan RunTestEvery => TimeSpan.FromSeconds(30);
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
private static readonly List<string> previousBreaches = new List<string>();
[TestMoment(t: 0)]
public void CheckAllThresholds()
{
var allNodes = CreateAccessToAllNodes();
foreach (var n in allNodes) CheckThresholds(n);
}
private void CheckThresholds(CodexAccess n)
{
var breaches = n.GetDebugThresholdBreaches();
if (breaches.breaches.Any())
{
var newBreaches = new List<string>();
foreach (var b in breaches.breaches)
{
if (!previousBreaches.Contains(b))
{
newBreaches.Add(b);
previousBreaches.Add(b);
}
}
if (newBreaches.Any())
{
Assert.Fail(string.Join(",", newBreaches.Select(b => FormatBreach(n, b))));
Program.Cancellation.Cts.Cancel();
}
}
}
private string FormatBreach(CodexAccess n, string breach)
{
return $"{n.Container.Name} = '{breach}'";
}
private CodexAccess[] CreateAccessToAllNodes()
{
// Normally, a continuous test accesses only a subset of the nodes in the deployment.
// This time, we want to check all of them.
var factory = new CodexAccessFactory();
var allContainers = Configuration.CodexDeployment.CodexContainers;
return factory.Create(Configuration, allContainers, Log, new DefaultTimeSet());
}
}
}
-32
View File
@@ -1,32 +0,0 @@
using DistTestCore;
using NUnit.Framework;
namespace ContinuousTests.Tests
{
public class TwoClientTest : ContinuousTest
{
public override int RequiredNumberOfNodes => 2;
public override TimeSpan RunTestEvery => TimeSpan.FromSeconds(30);
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
private ContentId? cid;
private TestFile file = null!;
[TestMoment(t: Zero)]
public void UploadTestFile()
{
file = FileManager.GenerateTestFile(10.MB());
cid = UploadFile(Nodes[0], file);
Assert.That(cid, Is.Not.Null);
}
[TestMoment(t: 10)]
public void DownloadTestFile()
{
var dl = DownloadFile(Nodes[1], cid!);
file.AssertIsEqual(dl);
}
}
}
-4
View File
@@ -1,4 +0,0 @@
dotnet run \
--kube-config=/opt/kubeconfig.yaml \
--codex-deployment=codex-deployment.json \
--stop=1
-30
View File
@@ -1,30 +0,0 @@
using NUnit.Framework;
namespace DistTestCore
{
public class AutoBootstrapDistTest : DistTest
{
public override IOnlineCodexNode SetupCodexBootstrapNode(Action<ICodexSetup> setup)
{
throw new Exception("AutoBootstrapDistTest creates and attaches a single bootstrap node for you. " +
"If you want to control the bootstrap node from your test, please use DistTest instead.");
}
public override ICodexNodeGroup SetupCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
{
var codexSetup = CreateCodexSetup(numberOfNodes);
setup(codexSetup);
codexSetup.WithBootstrapNode(BootstrapNode);
return BringOnline(codexSetup);
}
[SetUp]
public void SetUpBootstrapNode()
{
var setup = CreateCodexSetup(1).WithName("BOOTSTRAP");
BootstrapNode = BringOnline(setup)[0];
}
protected IOnlineCodexNode BootstrapNode { get; private set; } = null!;
}
}
-45
View File
@@ -1,45 +0,0 @@
using KubernetesWorkflow;
using Logging;
namespace DistTestCore
{
public class BaseStarter
{
protected readonly TestLifecycle lifecycle;
protected readonly WorkflowCreator workflowCreator;
private Stopwatch? stopwatch;
public BaseStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
{
this.lifecycle = lifecycle;
this.workflowCreator = workflowCreator;
}
protected void LogStart(string msg)
{
Log(msg);
stopwatch = Stopwatch.Begin(lifecycle.Log, GetClassName());
}
protected void LogEnd(string msg)
{
stopwatch!.End(msg);
stopwatch = null;
}
protected void Log(string msg)
{
lifecycle.Log.Log($"{GetClassName()} {msg}");
}
protected void Debug(string msg)
{
lifecycle.Log.Debug($"{GetClassName()} {msg}", 1);
}
private string GetClassName()
{
return $"({GetType().Name})";
}
}
}
-92
View File
@@ -1,92 +0,0 @@
using KubernetesWorkflow;
using Logging;
using Utils;
namespace DistTestCore.Codex
{
public class CodexAccess
{
private readonly BaseLog log;
private readonly ITimeSet timeSet;
public CodexAccess(BaseLog log, RunningContainer container, ITimeSet timeSet, Address address)
{
this.log = log;
Container = container;
this.timeSet = timeSet;
Address = address;
}
public RunningContainer Container { get; }
public Address Address { get; }
public CodexDebugResponse GetDebugInfo()
{
return Http().HttpGetJson<CodexDebugResponse>("debug/info");
}
public CodexDebugPeerResponse GetDebugPeer(string peerId)
{
var http = Http();
var str = http.HttpGetString($"debug/peer/{peerId}");
if (str.ToLowerInvariant() == "unable to find peer!")
{
return new CodexDebugPeerResponse
{
IsPeerFound = false
};
}
var result = http.TryJsonDeserialize<CodexDebugPeerResponse>(str);
result.IsPeerFound = true;
return result;
}
public int GetDebugFutures()
{
// Some Codex images support debug/futures to count the number of open futures.
return 0; // Http().HttpGetJson<CodexDebugFutures>("debug/futures").futures;
}
public CodexDebugThresholdBreaches GetDebugThresholdBreaches()
{
return Http().HttpGetJson<CodexDebugThresholdBreaches>("debug/loop");
}
public string UploadFile(FileStream fileStream)
{
return Http().HttpPostStream("upload", fileStream);
}
public Stream DownloadFile(string contentId)
{
return Http().HttpGetStream("download/" + contentId);
}
public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request)
{
return Http().HttpPostJson<CodexSalesAvailabilityRequest, CodexSalesAvailabilityResponse>("sales/availability", request);
}
public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId)
{
return Http().HttpPostJson($"storage/request/{contentId}", request);
}
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
{
return Http().HttpGetJson<CodexStoragePurchase>($"storage/purchases/{purchaseId}");
}
public string ConnectToPeer(string peerId, string peerMultiAddress)
{
return Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
}
private Http Http()
{
return new Http(log, timeSet, Address, baseUrl: "/api/codex/v1", Container.Name);
}
}
}
@@ -1,96 +0,0 @@
using DistTestCore.Marketplace;
using KubernetesWorkflow;
namespace DistTestCore.Codex
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
#if Arm64
public const string DockerImage = "codexstorage/nim-codex:sha-7227a4a";
#else
//public const string DockerImage = "thatbenbierens/nim-codex:loopingyeah";
public const string DockerImage = "codexstorage/nim-codex:sha-7227a4a";
#endif
public const string MetricsPortTag = "metrics_port";
public const string DiscoveryPortTag = "discovery-port";
// Used by tests for time-constraint assersions.
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
public static string DockerImageOverride = string.Empty;
protected override string Image
{
get
{
if (!string.IsNullOrEmpty(DockerImageOverride)) return DockerImageOverride;
return DockerImage;
}
}
protected override void Initialize(StartupConfig startupConfig)
{
var config = startupConfig.Get<CodexStartupConfig>();
AddExposedPortAndVar("CODEX_API_PORT");
AddEnvVar("CODEX_API_BINDADDR", "0.0.0.0");
AddEnvVar("CODEX_DATA_DIR", $"datadir{ContainerNumber}");
AddInternalPortAndVar("CODEX_DISC_PORT", DiscoveryPortTag);
AddEnvVar("CODEX_LOG_LEVEL", config.LogLevel.ToString()!.ToUpperInvariant());
// This makes the node announce itself to its local (pod) IP address.
AddEnvVar("NAT_IP_AUTO", "true");
var listenPort = AddInternalPort();
AddEnvVar("CODEX_LISTEN_ADDRS", $"/ip4/0.0.0.0/tcp/{listenPort.Number}");
if (!string.IsNullOrEmpty(config.BootstrapSpr))
{
AddEnvVar("CODEX_BOOTSTRAP_NODE", config.BootstrapSpr);
}
if (config.StorageQuota != null)
{
AddEnvVar("CODEX_STORAGE_QUOTA", config.StorageQuota.SizeInBytes.ToString()!);
}
if (config.BlockTTL != null)
{
AddEnvVar("CODEX_BLOCK_TTL", config.BlockTTL.ToString()!);
}
if (config.MetricsEnabled)
{
AddEnvVar("CODEX_METRICS", "true");
AddEnvVar("CODEX_METRICS_ADDRESS", "0.0.0.0");
AddInternalPortAndVar("CODEX_METRICS_PORT", tag: MetricsPortTag);
}
if (config.MarketplaceConfig != null)
{
var gethConfig = startupConfig.Get<GethStartResult>();
var companionNode = gethConfig.CompanionNode;
var companionNodeAccount = companionNode.Accounts[GetAccountIndex(config.MarketplaceConfig)];
Additional(companionNodeAccount);
var ip = companionNode.RunningContainer.Pod.PodInfo.Ip;
var port = companionNode.RunningContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag).Number;
AddEnvVar("CODEX_ETH_PROVIDER", $"ws://{ip}:{port}");
AddEnvVar("CODEX_ETH_ACCOUNT", companionNodeAccount.Account);
AddEnvVar("CODEX_MARKETPLACE_ADDRESS", gethConfig.MarketplaceNetwork.Marketplace.Address);
AddEnvVar("CODEX_PERSISTENCE", "true");
if (config.MarketplaceConfig.IsValidator)
{
AddEnvVar("CODEX_VALIDATOR", "true");
}
}
}
private int GetAccountIndex(MarketplaceInitialConfig marketplaceConfig)
{
if (marketplaceConfig.AccountIndexOverride != null) return marketplaceConfig.AccountIndexOverride.Value;
return Index;
}
}
}
-49
View File
@@ -1,49 +0,0 @@
using DistTestCore.Marketplace;
using KubernetesWorkflow;
namespace DistTestCore.Codex
{
public class CodexDeployment
{
public CodexDeployment(GethStartResult gethStartResult, RunningContainer[] codexContainers, RunningContainer? prometheusContainer, DeploymentMetadata metadata)
{
GethStartResult = gethStartResult;
CodexContainers = codexContainers;
PrometheusContainer = prometheusContainer;
Metadata = metadata;
}
public GethStartResult GethStartResult { get; }
public RunningContainer[] CodexContainers { get; }
public RunningContainer? PrometheusContainer { get; }
public DeploymentMetadata Metadata { get; }
}
public class DeploymentMetadata
{
public DeploymentMetadata(string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration)
{
DeployDateTimeUtc = DateTime.UtcNow;
KubeNamespace = kubeNamespace;
NumberOfCodexNodes = numberOfCodexNodes;
NumberOfValidators = numberOfValidators;
StorageQuotaMB = storageQuotaMB;
CodexLogLevel = codexLogLevel;
InitialTestTokens = initialTestTokens;
MinPrice = minPrice;
MaxCollateral = maxCollateral;
MaxDuration = maxDuration;
}
public DateTime DeployDateTimeUtc { get; }
public string KubeNamespace { get; }
public int NumberOfCodexNodes { get; }
public int NumberOfValidators { get; }
public int StorageQuotaMB { get; }
public CodexLogLevel CodexLogLevel { get; }
public int InitialTestTokens { get; }
public int MinPrice { get; }
public int MaxCollateral { get; }
public int MaxDuration { get; }
}
}
-22
View File
@@ -1,22 +0,0 @@
using DistTestCore.Marketplace;
using KubernetesWorkflow;
namespace DistTestCore.Codex
{
public class CodexStartupConfig
{
public CodexStartupConfig(CodexLogLevel logLevel)
{
LogLevel = logLevel;
}
public string? NameOverride { get; set; }
public Location Location { get; set; }
public CodexLogLevel LogLevel { get; }
public ByteSize? StorageQuota { get; set; }
public bool MetricsEnabled { get; set; }
public MarketplaceInitialConfig? MarketplaceConfig { get; set; }
public string? BootstrapSpr { get; set; }
public int? BlockTTL { get; set; }
}
}
-32
View File
@@ -1,32 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Marketplace;
using DistTestCore.Metrics;
namespace DistTestCore
{
public interface ICodexNodeFactory
{
OnlineCodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group);
}
public class CodexNodeFactory : ICodexNodeFactory
{
private readonly TestLifecycle lifecycle;
private readonly IMetricsAccessFactory metricsAccessFactory;
private readonly IMarketplaceAccessFactory marketplaceAccessFactory;
public CodexNodeFactory(TestLifecycle lifecycle, IMetricsAccessFactory metricsAccessFactory, IMarketplaceAccessFactory marketplaceAccessFactory)
{
this.lifecycle = lifecycle;
this.metricsAccessFactory = metricsAccessFactory;
this.marketplaceAccessFactory = marketplaceAccessFactory;
}
public OnlineCodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
{
var metricsAccess = metricsAccessFactory.CreateMetricsAccess(access.Container);
var marketplaceAccess = marketplaceAccessFactory.CreateMarketplaceAccess(access);
return new OnlineCodexNode(lifecycle, access, group, metricsAccess, marketplaceAccess);
}
}
}
-83
View File
@@ -1,83 +0,0 @@
using DistTestCore.Codex;
using KubernetesWorkflow;
using System.Collections;
namespace DistTestCore
{
public interface ICodexNodeGroup : IEnumerable<IOnlineCodexNode>
{
ICodexSetup BringOffline();
IOnlineCodexNode this[int index] { get; }
}
public class CodexNodeGroup : ICodexNodeGroup
{
private readonly TestLifecycle lifecycle;
public CodexNodeGroup(TestLifecycle lifecycle, CodexSetup setup, RunningContainers containers, ICodexNodeFactory codexNodeFactory)
{
this.lifecycle = lifecycle;
Setup = setup;
Containers = containers;
Nodes = containers.Containers.Select(c => CreateOnlineCodexNode(c, codexNodeFactory)).ToArray();
}
public IOnlineCodexNode this[int index]
{
get
{
return Nodes[index];
}
}
public ICodexSetup BringOffline()
{
lifecycle.CodexStarter.BringOffline(this);
var result = Setup;
// Clear everything. Prevent accidental use.
Setup = null!;
Nodes = Array.Empty<OnlineCodexNode>();
Containers = null!;
return result;
}
public CodexSetup Setup { get; private set; }
public RunningContainers Containers { get; private set; }
public OnlineCodexNode[] Nodes { get; private set; }
public IEnumerator<IOnlineCodexNode> GetEnumerator()
{
return Nodes.Cast<IOnlineCodexNode>().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Nodes.GetEnumerator();
}
public string Describe()
{
return $"group:[{Containers.Describe()}]";
}
public void EnsureOnline()
{
foreach (var node in Nodes)
{
var debugInfo = node.CodexAccess.GetDebugInfo();
var nodePeerId = debugInfo.id;
var nodeName = node.CodexAccess.Container.Name;
lifecycle.Log.AddStringReplace(nodePeerId, nodeName);
lifecycle.Log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
}
}
private OnlineCodexNode CreateOnlineCodexNode(RunningContainer c, ICodexNodeFactory factory)
{
var access = new CodexAccess(lifecycle.Log, c, lifecycle.TimeSet, lifecycle.Configuration.GetAddress(c));
return factory.CreateOnlineCodexNode(access, this);
}
}
}
-88
View File
@@ -1,88 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Marketplace;
using KubernetesWorkflow;
namespace DistTestCore
{
public interface ICodexSetup
{
ICodexSetup WithName(string name);
ICodexSetup At(Location location);
ICodexSetup WithBootstrapNode(IOnlineCodexNode node);
ICodexSetup WithStorageQuota(ByteSize storageQuota);
ICodexSetup EnableMetrics();
ICodexSetup EnableMarketplace(TestToken initialBalance);
ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther);
ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther, bool isValidator);
}
public class CodexSetup : CodexStartupConfig, ICodexSetup
{
public int NumberOfNodes { get; }
public CodexSetup(int numberOfNodes, CodexLogLevel logLevel)
: base(logLevel)
{
NumberOfNodes = numberOfNodes;
}
public ICodexSetup WithName(string name)
{
NameOverride = name;
return this;
}
public ICodexSetup At(Location location)
{
Location = location;
return this;
}
public ICodexSetup WithBootstrapNode(IOnlineCodexNode node)
{
BootstrapSpr = node.GetDebugInfo().spr;
return this;
}
public ICodexSetup WithStorageQuota(ByteSize storageQuota)
{
StorageQuota = storageQuota;
return this;
}
public ICodexSetup EnableMetrics()
{
MetricsEnabled = true;
return this;
}
public ICodexSetup EnableMarketplace(TestToken initialBalance)
{
return EnableMarketplace(initialBalance, 1000.Eth());
}
public ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther)
{
return EnableMarketplace(initialBalance, initialEther, false);
}
public ICodexSetup EnableMarketplace(TestToken initialBalance, Ether initialEther, bool isValidator)
{
MarketplaceConfig = new MarketplaceInitialConfig(initialEther, initialBalance, isValidator);
return this;
}
public string Describe()
{
var args = string.Join(',', DescribeArgs());
return $"({NumberOfNodes} CodexNodes with args:[{args}])";
}
private IEnumerable<string> DescribeArgs()
{
yield return $"LogLevel={LogLevel}";
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
if (StorageQuota != null) yield return $"StorageQuote={StorageQuota}";
}
}
}
-102
View File
@@ -1,102 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Marketplace;
using DistTestCore.Metrics;
using KubernetesWorkflow;
using Logging;
namespace DistTestCore
{
public class CodexStarter : BaseStarter
{
public CodexStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator)
{
}
public List<CodexNodeGroup> RunningGroups { get; } = new List<CodexNodeGroup>();
public ICodexNodeGroup BringOnline(CodexSetup codexSetup)
{
LogSeparator();
LogStart($"Starting {codexSetup.Describe()}...");
var gethStartResult = lifecycle.GethStarter.BringOnlineMarketplaceFor(codexSetup);
var startupConfig = CreateStartupConfig(gethStartResult, codexSetup);
var containers = StartCodexContainers(startupConfig, codexSetup.NumberOfNodes, codexSetup.Location);
var metricAccessFactory = CollectMetrics(codexSetup, containers);
var codexNodeFactory = new CodexNodeFactory(lifecycle, metricAccessFactory, gethStartResult.MarketplaceAccessFactory);
var group = CreateCodexGroup(codexSetup, containers, codexNodeFactory);
var podInfo = group.Containers.RunningPod.PodInfo;
LogEnd($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers.First().Recipe.Image}' at location '{podInfo.K8SNodeName}'={podInfo.Ip}. They are: {group.Describe()}");
LogSeparator();
return group;
}
public void BringOffline(CodexNodeGroup group)
{
LogStart($"Stopping {group.Describe()}...");
var workflow = CreateWorkflow();
workflow.Stop(group.Containers);
RunningGroups.Remove(group);
LogEnd("Stopped.");
}
public void DeleteAllResources()
{
var workflow = CreateWorkflow();
workflow.DeleteTestResources();
RunningGroups.Clear();
}
public void DownloadLog(RunningContainer container, ILogHandler logHandler)
{
var workflow = CreateWorkflow();
workflow.DownloadContainerLog(container, logHandler);
}
private IMetricsAccessFactory CollectMetrics(CodexSetup codexSetup, RunningContainers containers)
{
if (!codexSetup.MetricsEnabled) return new MetricsUnavailableAccessFactory();
var runningContainers = lifecycle.PrometheusStarter.CollectMetricsFor(containers);
return new CodexNodeMetricsAccessFactory(lifecycle, runningContainers);
}
private StartupConfig CreateStartupConfig(GethStartResult gethStartResult, CodexSetup codexSetup)
{
var startupConfig = new StartupConfig();
startupConfig.NameOverride = codexSetup.NameOverride;
startupConfig.Add(codexSetup);
startupConfig.Add(gethStartResult);
return startupConfig;
}
private RunningContainers StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, Location location)
{
var workflow = CreateWorkflow();
return workflow.Start(numberOfNodes, location, new CodexContainerRecipe(), startupConfig);
}
private CodexNodeGroup CreateCodexGroup(CodexSetup codexSetup, RunningContainers runningContainers, CodexNodeFactory codexNodeFactory)
{
var group = new CodexNodeGroup(lifecycle, codexSetup, runningContainers, codexNodeFactory);
RunningGroups.Add(group);
Stopwatch.Measure(lifecycle.Log, "EnsureOnline", group.EnsureOnline, debug: true);
return group;
}
private StartupWorkflow CreateWorkflow()
{
return workflowCreator.CreateWorkflow();
}
private void LogSeparator()
{
Log("----------------------------------------------------------------------------");
}
}
}
-218
View File
@@ -1,218 +0,0 @@
using Logging;
using NUnit.Framework;
using Utils;
namespace DistTestCore
{
public interface IFileManager
{
TestFile CreateEmptyTestFile(string label = "");
TestFile GenerateTestFile(ByteSize size, string label = "");
void DeleteAllTestFiles();
void PushFileSet();
void PopFileSet();
}
public class FileManager : IFileManager
{
public const int ChunkSize = 1024 * 1024 * 100;
private static NumberSource folderNumberSource = new NumberSource(0);
private readonly Random random = new Random();
private readonly BaseLog log;
private readonly string folder;
private readonly List<List<TestFile>> fileSetStack = new List<List<TestFile>>();
public FileManager(BaseLog log, Configuration configuration)
{
folder = Path.Combine(configuration.GetFileManagerFolder(), folderNumberSource.GetNextNumber().ToString("D5"));
EnsureDirectory();
this.log = log;
}
public TestFile CreateEmptyTestFile(string label = "")
{
var path = Path.Combine(folder, Guid.NewGuid().ToString() + "_test.bin");
var result = new TestFile(log, path, label);
File.Create(result.Filename).Close();
if (fileSetStack.Any()) fileSetStack.Last().Add(result);
return result;
}
public TestFile GenerateTestFile(ByteSize size, string label)
{
var sw = Stopwatch.Begin(log);
var result = GenerateFile(size, label);
sw.End($"Generated file '{result.Describe()}'.");
return result;
}
public void DeleteAllTestFiles()
{
DeleteDirectory();
}
public void PushFileSet()
{
fileSetStack.Add(new List<TestFile>());
}
public void PopFileSet()
{
if (!fileSetStack.Any()) return;
var pop = fileSetStack.Last();
fileSetStack.Remove(pop);
foreach (var file in pop)
{
try
{
File.Delete(file.Filename);
}
catch { }
}
}
private TestFile GenerateFile(ByteSize size, string label)
{
var result = CreateEmptyTestFile(label);
CheckSpaceAvailable(result, size);
GenerateFileBytes(result, size);
return result;
}
private void CheckSpaceAvailable(TestFile testFile, ByteSize size)
{
var file = new FileInfo(testFile.Filename);
var drive = new DriveInfo(file.Directory!.Root.FullName);
var spaceAvailable = drive.TotalFreeSpace;
if (spaceAvailable < size.SizeInBytes)
{
var msg = $"Inconclusive: Not enough disk space to perform test. " +
$"{Formatter.FormatByteSize(size.SizeInBytes)} required. " +
$"{Formatter.FormatByteSize(spaceAvailable)} available.";
log.Log(msg);
Assert.Inconclusive(msg);
}
}
private void GenerateFileBytes(TestFile result, ByteSize size)
{
long bytesLeft = size.SizeInBytes;
int chunkSize = ChunkSize;
while (bytesLeft > 0)
{
try
{
var length = Math.Min(bytesLeft, chunkSize);
AppendRandomBytesToFile(result, length);
bytesLeft -= length;
}
catch
{
chunkSize = chunkSize / 2;
if (chunkSize < 1024) throw;
}
}
}
private void AppendRandomBytesToFile(TestFile result, long length)
{
var bytes = new byte[length];
random.NextBytes(bytes);
using var stream = new FileStream(result.Filename, FileMode.Append);
stream.Write(bytes, 0, bytes.Length);
}
private void EnsureDirectory()
{
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
}
private void DeleteDirectory()
{
Directory.Delete(folder, true);
}
}
public class TestFile
{
private readonly BaseLog log;
public TestFile(BaseLog log, string filename, string label)
{
this.log = log;
Filename = filename;
Label = label;
}
public string Filename { get; }
public string Label { get; }
public void AssertIsEqual(TestFile? actual)
{
var sw = Stopwatch.Begin(log);
try
{
AssertEqual(actual);
}
finally
{
sw.End($"{nameof(TestFile)}.{nameof(AssertIsEqual)}");
}
}
public string Describe()
{
var sizePostfix = $" ({Formatter.FormatByteSize(GetFileSize())})";
if (!string.IsNullOrEmpty(Label)) return Label + sizePostfix;
return $"'{Filename}'{sizePostfix}";
}
private void AssertEqual(TestFile? actual)
{
if (actual == null) Assert.Fail("TestFile is null.");
if (actual == this || actual!.Filename == Filename) Assert.Fail("TestFile is compared to itself.");
Assert.That(actual.GetFileSize(), Is.EqualTo(GetFileSize()), "Files are not of equal length.");
using var streamExpected = new FileStream(Filename, FileMode.Open, FileAccess.Read);
using var streamActual = new FileStream(actual.Filename, FileMode.Open, FileAccess.Read);
var bytesExpected = new byte[FileManager.ChunkSize];
var bytesActual = new byte[FileManager.ChunkSize];
var readExpected = 0;
var readActual = 0;
while (true)
{
readExpected = streamExpected.Read(bytesExpected, 0, FileManager.ChunkSize);
readActual = streamActual.Read(bytesActual, 0, FileManager.ChunkSize);
if (readExpected == 0 && readActual == 0)
{
log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'.");
return;
}
Assert.That(readActual, Is.EqualTo(readExpected), "Unable to read buffers of equal length.");
for (var i = 0; i < readActual; i++)
{
if (bytesExpected[i] != bytesActual[i]) Assert.Fail("File contents not equal.");
}
}
}
private long GetFileSize()
{
var info = new FileInfo(Filename);
return info.Length;
}
}
}
-89
View File
@@ -1,89 +0,0 @@
using DistTestCore.Marketplace;
using KubernetesWorkflow;
namespace DistTestCore
{
public class GethStarter : BaseStarter
{
private readonly MarketplaceNetworkCache marketplaceNetworkCache;
private readonly GethCompanionNodeStarter companionNodeStarter;
public GethStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator)
{
marketplaceNetworkCache = new MarketplaceNetworkCache(
new GethBootstrapNodeStarter(lifecycle, workflowCreator),
new CodexContractsStarter(lifecycle, workflowCreator));
companionNodeStarter = new GethCompanionNodeStarter(lifecycle, workflowCreator);
}
public GethStartResult BringOnlineMarketplaceFor(CodexSetup codexSetup)
{
if (codexSetup.MarketplaceConfig == null) return CreateMarketplaceUnavailableResult();
var marketplaceNetwork = marketplaceNetworkCache.Get();
var companionNode = StartCompanionNode(codexSetup, marketplaceNetwork);
LogStart("Setting up initial balance...");
TransferInitialBalance(marketplaceNetwork, codexSetup.MarketplaceConfig, companionNode);
LogEnd($"Initial balance of {codexSetup.MarketplaceConfig.InitialTestTokens} set for {codexSetup.NumberOfNodes} nodes.");
return CreateGethStartResult(marketplaceNetwork, companionNode);
}
private void TransferInitialBalance(MarketplaceNetwork marketplaceNetwork, MarketplaceInitialConfig marketplaceConfig, GethCompanionNodeInfo companionNode)
{
if (marketplaceConfig.InitialTestTokens.Amount == 0) return;
var interaction = marketplaceNetwork.StartInteraction(lifecycle);
var tokenAddress = marketplaceNetwork.Marketplace.TokenAddress;
var accounts = companionNode.Accounts.Select(a => a.Account).ToArray();
interaction.MintTestTokens(accounts, marketplaceConfig.InitialTestTokens.Amount, tokenAddress);
}
private GethStartResult CreateGethStartResult(MarketplaceNetwork marketplaceNetwork, GethCompanionNodeInfo companionNode)
{
return new GethStartResult(CreateMarketplaceAccessFactory(marketplaceNetwork), marketplaceNetwork, companionNode);
}
private GethStartResult CreateMarketplaceUnavailableResult()
{
return new GethStartResult(new MarketplaceUnavailableAccessFactory(), null!, null!);
}
private IMarketplaceAccessFactory CreateMarketplaceAccessFactory(MarketplaceNetwork marketplaceNetwork)
{
return new GethMarketplaceAccessFactory(lifecycle, marketplaceNetwork);
}
private GethCompanionNodeInfo StartCompanionNode(CodexSetup codexSetup, MarketplaceNetwork marketplaceNetwork)
{
return companionNodeStarter.StartCompanionNodeFor(codexSetup, marketplaceNetwork);
}
}
public class MarketplaceNetworkCache
{
private readonly GethBootstrapNodeStarter bootstrapNodeStarter;
private readonly CodexContractsStarter codexContractsStarter;
private MarketplaceNetwork? network;
public MarketplaceNetworkCache(GethBootstrapNodeStarter bootstrapNodeStarter, CodexContractsStarter codexContractsStarter)
{
this.bootstrapNodeStarter = bootstrapNodeStarter;
this.codexContractsStarter = codexContractsStarter;
}
public MarketplaceNetwork Get()
{
if (network == null)
{
var bootstrapInfo = bootstrapNodeStarter.StartGethBootstrapNode();
var marketplaceInfo = codexContractsStarter.Start(bootstrapInfo);
network = new MarketplaceNetwork(bootstrapInfo, marketplaceInfo );
}
return network;
}
}
}
@@ -1,257 +0,0 @@
using DistTestCore.Codex;
using NUnit.Framework;
using Utils;
namespace DistTestCore.Helpers
{
public class PeerConnectionTestHelpers
{
private readonly Random random = new Random();
private readonly DistTest test;
public PeerConnectionTestHelpers(DistTest test)
{
this.test = test;
}
public void AssertFullyConnected(IEnumerable<IOnlineCodexNode> nodes)
{
var n = nodes.ToArray();
AssertFullyConnected(n);
for (int i = 0; i < 5; i++)
{
Time.Sleep(TimeSpan.FromSeconds(30));
AssertFullyConnected(n);
}
}
private void AssertFullyConnected(IOnlineCodexNode[] nodes)
{
test.Log($"Asserting peers are fully-connected for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
var entries = CreateEntries(nodes);
var pairs = CreatePairs(entries);
RetryWhilePairs(pairs, () =>
{
CheckAndRemoveSuccessful(pairs);
});
if (pairs.Any())
{
test.Log($"Unsuccessful! Peers are not fully-connected: {string.Join(",", nodes.Select(n => n.GetName()))}");
Assert.Fail(string.Join(Environment.NewLine, pairs.Select(p => p.GetMessage())));
test.Log(string.Join(Environment.NewLine, pairs.Select(p => p.GetMessage())));
}
else
{
test.Log($"Success! Peers are fully-connected: {string.Join(",", nodes.Select(n => n.GetName()))}");
}
}
private static void RetryWhilePairs(List<Pair> pairs, Action action)
{
var timeout = DateTime.UtcNow + TimeSpan.FromSeconds(30);
while (pairs.Any() && timeout > DateTime.UtcNow)
{
action();
if (pairs.Any()) Time.Sleep(TimeSpan.FromSeconds(2));
}
}
private void CheckAndRemoveSuccessful(List<Pair> pairs)
{
var checkTasks = pairs.Select(p => Task.Run(() =>
{
ApplyRandomDelay();
p.Check();
})).ToArray();
Task.WaitAll(checkTasks);
foreach (var pair in pairs.ToArray())
{
if (pair.Success)
{
test.Log(pair.GetMessage());
pairs.Remove(pair);
}
}
}
private static Entry[] CreateEntries(IOnlineCodexNode[] nodes)
{
var entries = nodes.Select(n => new Entry(n)).ToArray();
var incorrectDiscoveryEndpoints = entries.SelectMany(e => e.GetInCorrectDiscoveryEndpoints(entries)).ToArray();
if (incorrectDiscoveryEndpoints.Any())
{
Assert.Fail("Some nodes contain peer records with incorrect discovery ip/port information: " +
string.Join(Environment.NewLine, incorrectDiscoveryEndpoints));
}
return entries;
}
private static List<Pair> CreatePairs(Entry[] entries)
{
return CreatePairsIterator(entries).ToList();
}
private static IEnumerable<Pair> CreatePairsIterator(Entry[] entries)
{
for (var x = 0; x < entries.Length; x++)
{
for (var y = x + 1; y < entries.Length; y++)
{
yield return new Pair(entries[x], entries[y]);
}
}
}
private void ApplyRandomDelay()
{
// Calling all the nodes all at the same time is not exactly nice.
Time.Sleep(TimeSpan.FromMicroseconds(random.Next(10, 1000)));
}
public class Entry
{
public Entry(IOnlineCodexNode node)
{
Node = node;
Response = node.GetDebugInfo();
}
public IOnlineCodexNode Node { get; }
public CodexDebugResponse Response { get; }
public IEnumerable<string> GetInCorrectDiscoveryEndpoints(Entry[] allEntries)
{
foreach (var peer in Response.table.nodes)
{
var expected = GetExpectedDiscoveryEndpoint(allEntries, peer);
if (expected != peer.address)
{
yield return $"Node:{Node.GetName()} has incorrect peer table entry. Was: '{peer.address}', expected: '{expected}'";
}
}
}
public override string ToString()
{
if (Response == null || string.IsNullOrEmpty(Response.id)) return "UNKNOWN";
return Response.id;
}
private static string GetExpectedDiscoveryEndpoint(Entry[] allEntries, CodexDebugTableNodeResponse node)
{
var peer = allEntries.SingleOrDefault(e => e.Response.table.localNode.peerId == node.peerId);
if (peer == null) return $"peerId: {node.peerId} is not known.";
var n = (OnlineCodexNode)peer.Node;
var ip = n.CodexAccess.Container.Pod.PodInfo.Ip;
var discPort = n.CodexAccess.Container.Recipe.GetPortByTag(CodexContainerRecipe.DiscoveryPortTag);
return $"{ip}:{discPort.Number}";
}
}
public enum PeerConnectionState
{
Unknown,
Connection,
NoConnection,
}
public class Pair
{
private TimeSpan aToBTime = TimeSpan.FromSeconds(0);
private TimeSpan bToATime = TimeSpan.FromSeconds(0);
public Pair(Entry a, Entry b)
{
A = a;
B = b;
}
public Entry A { get; }
public Entry B { get; }
public PeerConnectionState AKnowsB { get; private set; }
public PeerConnectionState BKnowsA { get; private set; }
public bool Success { get { return AKnowsB == PeerConnectionState.Connection && BKnowsA == PeerConnectionState.Connection; } }
public void Check()
{
aToBTime = Measure(() => AKnowsB = Knows(A, B));
bToATime = Measure(() => BKnowsA = Knows(B, A));
}
public string GetMessage()
{
return GetResultMessage() + GetTimePostfix();
}
public override string ToString()
{
return $"[{GetMessage()}]";
}
private string GetResultMessage()
{
var aName = A.ToString();
var bName = B.ToString();
if (Success)
{
return $"{aName} and {bName} know each other.";
}
return $"[{aName}-->{bName}] = {AKnowsB} AND [{aName}<--{bName}] = {BKnowsA}";
}
private string GetTimePostfix()
{
var aName = A.ToString();
var bName = B.ToString();
return $" ({aName}->{bName}: {aToBTime.TotalMinutes} seconds, {bName}->{aName}: {bToATime.TotalSeconds} seconds)";
}
private static TimeSpan Measure(Action action)
{
var start = DateTime.UtcNow;
action();
return DateTime.UtcNow - start;
}
private PeerConnectionState Knows(Entry a, Entry b)
{
lock (a)
{
var peerId = b.Response.id;
try
{
var response = a.Node.GetDebugPeer(peerId);
if (!response.IsPeerFound)
{
return PeerConnectionState.NoConnection;
}
if (!string.IsNullOrEmpty(response.peerId) && response.addresses.Any())
{
return PeerConnectionState.Connection;
}
}
catch
{
}
// Didn't get a conclusive answer. Try again later.
return PeerConnectionState.Unknown;
}
}
}
}
}
@@ -1,75 +0,0 @@
using DistTestCore.Codex;
using NUnit.Framework;
namespace DistTestCore.Helpers
{
public class PeerDownloadTestHelpers
{
private readonly DistTest test;
public PeerDownloadTestHelpers(DistTest test)
{
this.test = test;
}
public void AssertFullDownloadInterconnectivity(IEnumerable<IOnlineCodexNode> nodes, ByteSize testFileSize)
{
test.Log($"Asserting full download interconnectivity for nodes: '{string.Join(",", nodes.Select(n => n.GetName()))}'...");
var start = DateTime.UtcNow;
foreach (var node in nodes)
{
var uploader = node;
var downloaders = nodes.Where(n => n != uploader).ToArray();
test.ScopedTestFiles(() =>
{
PerformTest(uploader, downloaders, testFileSize);
});
}
test.Log($"Success! Full download interconnectivity for nodes: {string.Join(",", nodes.Select(n => n.GetName()))}");
var timeTaken = DateTime.UtcNow - start;
AssertTimePerMB(timeTaken, nodes.Count(), testFileSize);
}
private void AssertTimePerMB(TimeSpan timeTaken, int numberOfNodes, ByteSize size)
{
var numberOfDownloads = numberOfNodes * (numberOfNodes - 1);
var timePerDownload = timeTaken / numberOfDownloads;
float sizeInMB = size.ToMB();
var timePerMB = timePerDownload / sizeInMB;
test.Log($"Performed {numberOfDownloads} downloads of {size} in {timeTaken.TotalSeconds} seconds, for an average of {timePerMB.TotalSeconds} seconds per MB.");
Assert.That(timePerMB, Is.LessThan(CodexContainerRecipe.MaxDownloadTimePerMegabyte), "MaxDownloadTimePerMegabyte performance threshold breached.");
}
private void PerformTest(IOnlineCodexNode uploader, IOnlineCodexNode[] downloaders, ByteSize testFileSize)
{
// Generate 1 test file per downloader.
var files = downloaders.Select(d => GenerateTestFile(uploader, d, testFileSize)).ToArray();
// Upload all the test files to the uploader.
var contentIds = files.Select(uploader.UploadFile).ToArray();
// Each downloader should retrieve its own test file.
for (var i = 0; i < downloaders.Length; i++)
{
var expectedFile = files[i];
var downloadedFile = downloaders[i].DownloadContent(contentIds[i], $"{expectedFile.Label}DOWNLOADED");
expectedFile.AssertIsEqual(downloadedFile);
}
}
private TestFile GenerateTestFile(IOnlineCodexNode uploader, IOnlineCodexNode downloader, ByteSize testFileSize)
{
var up = uploader.GetName().Replace("<", "").Replace(">", "");
var down = downloader.GetName().Replace("<", "").Replace(">", "");
var label = $"FROM{up}TO{down}";
return test.GenerateTestFile(testFileSize, label);
}
}
}
-138
View File
@@ -1,138 +0,0 @@
using Logging;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Utils;
namespace DistTestCore
{
public class Http
{
private readonly BaseLog log;
private readonly ITimeSet timeSet;
private readonly Address address;
private readonly string baseUrl;
private readonly string? logAlias;
public Http(BaseLog log, ITimeSet timeSet, Address address, string baseUrl, string? logAlias = null)
{
this.log = log;
this.timeSet = timeSet;
this.address = address;
this.baseUrl = baseUrl;
this.logAlias = logAlias;
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
}
public string HttpGetString(string route)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str; ;
}, $"HTTP-GET:{route}");
}
public T HttpGetJson<T>(string route)
{
var json = HttpGetString(route);
return TryJsonDeserialize<T>(json);
}
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{
var json = HttpPostJson(route, body);
return TryJsonDeserialize<TResponse>(json);
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-JSON: {route}");
}
public string HttpPostStream(string route, Stream stream)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = Time.Wait(client.PostAsync(url, content));
var str =Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STREAM: {route}");
}
public Stream HttpGetStream(string route)
{
return Retry(() =>
{
var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url));
}, $"HTTP-GET-STREAM: {route}");
}
public T TryJsonDeserialize<T>(string json)
{
try
{
return JsonConvert.DeserializeObject<T>(json)!;
}
catch (Exception exception)
{
var msg = $"Failed to deserialize JSON: '{json}' with exception: {exception}";
throw new InvalidOperationException(msg, exception);
}
}
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
if (logAlias != null)
{
log.Debug($"({logAlias})({url}) = '{message}'", 3);
}
else
{
log.Debug($"({url}) = '{message}'", 3);
}
}
private T Retry<T>(Func<T> operation, string description)
{
return Time.Retry(operation, timeSet.HttpCallRetryTime(), timeSet.HttpCallRetryDelay(), description);
}
private HttpClient GetClient()
{
var client = new HttpClient();
client.Timeout = timeSet.HttpCallTimeout();
return client;
}
}
}
@@ -1,15 +0,0 @@
using NUnit.Framework;
namespace DistTestCore.Logs
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class DontDownloadLogsAndMetricsOnFailureAttribute : PropertyAttribute
{
public const string DontDownloadKey = "DontDownloadLogsAndMetrics";
public DontDownloadLogsAndMetricsOnFailureAttribute()
: base(DontDownloadKey)
{
}
}
}
-37
View File
@@ -1,37 +0,0 @@
using Logging;
using NUnit.Framework;
namespace DistTestCore.Logs
{
public interface IDownloadedLog
{
void AssertLogContains(string expectedString);
}
public class DownloadedLog : IDownloadedLog
{
private readonly LogFile logFile;
private readonly string owner;
public DownloadedLog(LogFile logFile, string owner)
{
this.logFile = logFile;
this.owner = owner;
}
public void AssertLogContains(string expectedString)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var line = streamReader.ReadLine();
while (line != null)
{
if (line.Contains(expectedString)) return;
line = streamReader.ReadLine();
}
Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}");
}
}
}
@@ -1,16 +0,0 @@
using KubernetesWorkflow;
namespace DistTestCore.Marketplace
{
public class CodexContractsContainerConfig
{
public CodexContractsContainerConfig(string bootstrapNodeIp, Port jsonRpcPort)
{
BootstrapNodeIp = bootstrapNodeIp;
JsonRpcPort = jsonRpcPort;
}
public string BootstrapNodeIp { get; }
public Port JsonRpcPort { get; }
}
}
@@ -1,29 +0,0 @@
using KubernetesWorkflow;
namespace DistTestCore.Marketplace
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
#if Arm64
public const string DockerImage = "emizzle/codex-contracts-deployment:latest";
#else
public const string DockerImage = "thatbenbierens/codex-contracts-deployment:nomint2";
#endif
public const string MarketplaceAddressFilename = "/usr/app/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/usr/app/artifacts/contracts/Marketplace.sol/Marketplace.json";
protected override string Image => DockerImage;
protected override void Initialize(StartupConfig startupConfig)
{
var config = startupConfig.Get<CodexContractsContainerConfig>();
var ip = config.BootstrapNodeIp;
var port = config.JsonRpcPort.Number;
AddEnvVar("DISTTEST_NETWORK_URL", $"http://{ip}:{port}");
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
AddEnvVar("KEEP_ALIVE", "1");
}
}
}
@@ -1,98 +0,0 @@
using KubernetesWorkflow;
using Utils;
namespace DistTestCore.Marketplace
{
public class CodexContractsStarter : BaseStarter
{
public CodexContractsStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator)
{
}
public MarketplaceInfo Start(GethBootstrapNodeInfo bootstrapNode)
{
LogStart("Deploying Codex Marketplace...");
var workflow = workflowCreator.CreateWorkflow();
var startupConfig = CreateStartupConfig(bootstrapNode.RunningContainers.Containers[0]);
var containers = workflow.Start(1, Location.Unspecified, new CodexContractsContainerRecipe(), startupConfig);
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
var container = containers.Containers[0];
WaitUntil(() =>
{
var logHandler = new ContractsReadyLogHandler(Debug);
workflow.DownloadContainerLog(container, logHandler);
return logHandler.Found;
});
Log("Contracts deployed. Extracting addresses...");
var extractor = new ContainerInfoExtractor(lifecycle.Log, workflow, container);
var marketplaceAddress = extractor.ExtractMarketplaceAddress();
var abi = extractor.ExtractMarketplaceAbi();
var interaction = bootstrapNode.StartInteraction(lifecycle);
var tokenAddress = interaction.GetTokenAddress(marketplaceAddress);
LogEnd("Extract completed. Marketplace deployed.");
return new MarketplaceInfo(marketplaceAddress, abi, tokenAddress);
}
private void WaitUntil(Func<bool> predicate)
{
Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2));
}
private StartupConfig CreateStartupConfig(RunningContainer bootstrapContainer)
{
var startupConfig = new StartupConfig();
var contractsConfig = new CodexContractsContainerConfig(bootstrapContainer.Pod.PodInfo.Ip, bootstrapContainer.Recipe.GetPortByTag(GethContainerRecipe.HttpPortTag));
startupConfig.Add(contractsConfig);
return startupConfig;
}
}
public class MarketplaceInfo
{
public MarketplaceInfo(string address, string abi, string tokenAddress)
{
Address = address;
Abi = abi;
TokenAddress = tokenAddress;
}
public string Address { get; }
public string Abi { get; }
public string TokenAddress { get; }
}
public class ContractsReadyLogHandler : LogHandler
{
// Log should contain 'Compiled 15 Solidity files successfully' at some point.
private const string RequiredCompiledString = "Solidity files successfully";
// When script is done, it prints the ready-string.
private const string ReadyString = "Done! Sleeping indefinitely...";
private readonly Action<string> debug;
public ContractsReadyLogHandler(Action<string> debug)
{
this.debug = debug;
debug($"Looking for '{RequiredCompiledString}' and '{ReadyString}' in container logs...");
}
public bool SeenCompileString { get; private set; }
public bool Found { get; private set; }
protected override void ProcessLine(string line)
{
debug(line);
if (line.Contains(RequiredCompiledString)) SeenCompileString = true;
if (SeenCompileString && line.Contains(ReadyString)) Found = true;
}
}
}
@@ -1,42 +0,0 @@
using KubernetesWorkflow;
using NethereumWorkflow;
namespace DistTestCore.Marketplace
{
public class GethBootstrapNodeInfo
{
public GethBootstrapNodeInfo(RunningContainers runningContainers, AllGethAccounts allAccounts, string pubKey, Port discoveryPort)
{
RunningContainers = runningContainers;
AllAccounts = allAccounts;
Account = allAccounts.Accounts[0];
PubKey = pubKey;
DiscoveryPort = discoveryPort;
}
public RunningContainers RunningContainers { get; }
public AllGethAccounts AllAccounts { get; }
public GethAccount Account { get; }
public string PubKey { get; }
public Port DiscoveryPort { get; }
public NethereumInteraction StartInteraction(TestLifecycle lifecycle)
{
var address = lifecycle.Configuration.GetAddress(RunningContainers.Containers[0]);
var account = Account;
var creator = new NethereumInteractionCreator(lifecycle.Log, address.Host, address.Port, account.PrivateKey);
return creator.CreateWorkflow();
}
}
public class AllGethAccounts
{
public GethAccount[] Accounts { get; }
public AllGethAccounts(GethAccount[] accounts)
{
Accounts = accounts;
}
}
}
@@ -1,40 +0,0 @@
using KubernetesWorkflow;
namespace DistTestCore.Marketplace
{
public class GethBootstrapNodeStarter : BaseStarter
{
public GethBootstrapNodeStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator)
{
}
public GethBootstrapNodeInfo StartGethBootstrapNode()
{
LogStart("Starting Geth bootstrap node...");
var startupConfig = CreateBootstrapStartupConfig();
var workflow = workflowCreator.CreateWorkflow();
var containers = workflow.Start(1, Location.Unspecified, new GethContainerRecipe(), startupConfig);
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Geth bootstrap node to be created. Test infra failure.");
var bootstrapContainer = containers.Containers[0];
var extractor = new ContainerInfoExtractor(lifecycle.Log, workflow, bootstrapContainer);
var accounts = extractor.ExtractAccounts();
var pubKey = extractor.ExtractPubKey();
var discoveryPort = bootstrapContainer.Recipe.GetPortByTag(GethContainerRecipe.DiscoveryPortTag);
var result = new GethBootstrapNodeInfo(containers, accounts, pubKey, discoveryPort);
LogEnd($"Geth bootstrap node started with account '{result.Account.Account}'");
return result;
}
private StartupConfig CreateBootstrapStartupConfig()
{
var config = new StartupConfig();
config.Add(new GethStartupConfig(true, null!, 0, 0));
return config;
}
}
}
@@ -1,38 +0,0 @@
using KubernetesWorkflow;
using NethereumWorkflow;
namespace DistTestCore.Marketplace
{
public class GethCompanionNodeInfo
{
public GethCompanionNodeInfo(RunningContainer runningContainer, GethAccount[] accounts)
{
RunningContainer = runningContainer;
Accounts = accounts;
}
public RunningContainer RunningContainer { get; }
public GethAccount[] Accounts { get; }
public NethereumInteraction StartInteraction(TestLifecycle lifecycle, GethAccount account)
{
var address = lifecycle.Configuration.GetAddress(RunningContainer);
var privateKey = account.PrivateKey;
var creator = new NethereumInteractionCreator(lifecycle.Log, address.Host, address.Port, privateKey);
return creator.CreateWorkflow();
}
}
public class GethAccount
{
public GethAccount(string account, string privateKey)
{
Account = account;
PrivateKey = privateKey;
}
public string Account { get; }
public string PrivateKey { get; }
}
}
@@ -1,77 +0,0 @@
using KubernetesWorkflow;
using Utils;
namespace DistTestCore.Marketplace
{
public class GethCompanionNodeStarter : BaseStarter
{
private int companionAccountIndex = 0;
public GethCompanionNodeStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator)
{
}
public GethCompanionNodeInfo StartCompanionNodeFor(CodexSetup codexSetup, MarketplaceNetwork marketplace)
{
LogStart($"Initializing companion for {codexSetup.NumberOfNodes} Codex nodes.");
var config = CreateCompanionNodeStartupConfig(marketplace.Bootstrap, codexSetup.NumberOfNodes);
var workflow = workflowCreator.CreateWorkflow();
var containers = workflow.Start(1, Location.Unspecified, new GethContainerRecipe(), CreateStartupConfig(config));
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected one Geth companion node to be created. Test infra failure.");
var container = containers.Containers[0];
var node = CreateCompanionInfo(container, marketplace, config);
EnsureCompanionNodeIsSynced(node, marketplace);
LogEnd($"Initialized one companion node for {codexSetup.NumberOfNodes} Codex nodes. Their accounts: [{string.Join(",", node.Accounts.Select(a => a.Account))}]");
return node;
}
private GethCompanionNodeInfo CreateCompanionInfo(RunningContainer container, MarketplaceNetwork marketplace, GethStartupConfig config)
{
var accounts = ExtractAccounts(marketplace, config);
return new GethCompanionNodeInfo(container, accounts);
}
private static GethAccount[] ExtractAccounts(MarketplaceNetwork marketplace, GethStartupConfig config)
{
return marketplace.Bootstrap.AllAccounts.Accounts
.Skip(1 + config.CompanionAccountStartIndex)
.Take(config.NumberOfCompanionAccounts)
.ToArray();
}
private void EnsureCompanionNodeIsSynced(GethCompanionNodeInfo node, MarketplaceNetwork marketplace)
{
try
{
Time.WaitUntil(() =>
{
var interaction = node.StartInteraction(lifecycle, node.Accounts.First());
return interaction.IsSynced(marketplace.Marketplace.Address, marketplace.Marketplace.Abi);
}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(3));
}
catch (Exception e)
{
throw new Exception("Geth companion node did not sync within timeout. Test infra failure.", e);
}
}
private GethStartupConfig CreateCompanionNodeStartupConfig(GethBootstrapNodeInfo bootstrapNode, int numberOfAccounts)
{
var config = new GethStartupConfig(false, bootstrapNode, companionAccountIndex, numberOfAccounts);
companionAccountIndex += numberOfAccounts;
return config;
}
private StartupConfig CreateStartupConfig(GethStartupConfig gethConfig)
{
var config = new StartupConfig();
config.Add(gethConfig);
return config;
}
}
}
@@ -1,78 +0,0 @@
using KubernetesWorkflow;
namespace DistTestCore.Marketplace
{
public class GethContainerRecipe : ContainerRecipeFactory
{
#if Arm64
public const string DockerImage = "emizzle/geth-confenv:latest";
#else
public const string DockerImage = "thatbenbierens/geth-confenv:onethousand";
#endif
public const string HttpPortTag = "http_port";
public const string DiscoveryPortTag = "disc_port";
private const string defaultArgs = "--ipcdisable --syncmode full";
public const string AccountsFilename = "accounts.csv";
protected override string Image => DockerImage;
protected override void Initialize(StartupConfig startupConfig)
{
var config = startupConfig.Get<GethStartupConfig>();
var args = CreateArgs(config);
AddEnvVar("GETH_ARGS", args);
}
private string CreateArgs(GethStartupConfig config)
{
var discovery = AddInternalPort(tag: DiscoveryPortTag);
if (config.IsBootstrapNode)
{
return CreateBootstapArgs(discovery);
}
return CreateCompanionArgs(discovery, config);
}
private string CreateBootstapArgs(Port discovery)
{
AddEnvVar("ENABLE_MINER", "1");
UnlockAccounts(0, 1);
var exposedPort = AddExposedPort(tag: HttpPortTag);
return $"--http.port {exposedPort.Number} --port {discovery.Number} --discovery.port {discovery.Number} {defaultArgs}";
}
private string CreateCompanionArgs(Port discovery, GethStartupConfig config)
{
UnlockAccounts(
config.CompanionAccountStartIndex + 1,
config.NumberOfCompanionAccounts);
var port = AddInternalPort();
var authRpc = AddInternalPort();
var httpPort = AddExposedPort(tag: HttpPortTag);
var bootPubKey = config.BootstrapNode.PubKey;
var bootIp = config.BootstrapNode.RunningContainers.Containers[0].Pod.PodInfo.Ip;
var bootPort = config.BootstrapNode.DiscoveryPort.Number;
var bootstrapArg = $"--bootnodes enode://{bootPubKey}@{bootIp}:{bootPort} --nat=extip:{bootIp}";
return $"--port {port.Number} --discovery.port {discovery.Number} --authrpc.port {authRpc.Number} --http.addr 0.0.0.0 --http.port {httpPort.Number} --ws --ws.addr 0.0.0.0 --ws.port {httpPort.Number} {bootstrapArg} {defaultArgs}";
}
private void UnlockAccounts(int startIndex, int numberOfAccounts)
{
if (startIndex < 0) throw new ArgumentException();
if (numberOfAccounts < 1) throw new ArgumentException();
if (startIndex + numberOfAccounts > 1000) throw new ArgumentException("Out of accounts!");
AddEnvVar("UNLOCK_START_INDEX", startIndex.ToString());
AddEnvVar("UNLOCK_NUMBER", numberOfAccounts.ToString());
}
}
}
@@ -1,19 +0,0 @@
using Newtonsoft.Json;
namespace DistTestCore.Marketplace
{
public class GethStartResult
{
public GethStartResult(IMarketplaceAccessFactory marketplaceAccessFactory, MarketplaceNetwork marketplaceNetwork, GethCompanionNodeInfo companionNode)
{
MarketplaceAccessFactory = marketplaceAccessFactory;
MarketplaceNetwork = marketplaceNetwork;
CompanionNode = companionNode;
}
[JsonIgnore]
public IMarketplaceAccessFactory MarketplaceAccessFactory { get; }
public MarketplaceNetwork MarketplaceNetwork { get; }
public GethCompanionNodeInfo CompanionNode { get; }
}
}
@@ -1,18 +0,0 @@
namespace DistTestCore.Marketplace
{
public class GethStartupConfig
{
public GethStartupConfig(bool isBootstrapNode, GethBootstrapNodeInfo bootstrapNode, int companionAccountStartIndex, int numberOfCompanionAccounts)
{
IsBootstrapNode = isBootstrapNode;
BootstrapNode = bootstrapNode;
CompanionAccountStartIndex = companionAccountStartIndex;
NumberOfCompanionAccounts = numberOfCompanionAccounts;
}
public bool IsBootstrapNode { get; }
public GethBootstrapNodeInfo BootstrapNode { get; }
public int CompanionAccountStartIndex { get; }
public int NumberOfCompanionAccounts { get; }
}
}
@@ -1,153 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Helpers;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using System.Numerics;
using Utils;
namespace DistTestCore.Marketplace
{
public interface IMarketplaceAccess
{
string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan maxDuration);
string RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
void AssertThatBalance(IResolveConstraint constraint, string message = "");
TestToken GetBalance();
}
public class MarketplaceAccess : IMarketplaceAccess
{
private readonly TestLifecycle lifecycle;
private readonly MarketplaceNetwork marketplaceNetwork;
private readonly GethAccount account;
private readonly CodexAccess codexAccess;
public MarketplaceAccess(TestLifecycle lifecycle, MarketplaceNetwork marketplaceNetwork, GethAccount account, CodexAccess codexAccess)
{
this.lifecycle = lifecycle;
this.marketplaceNetwork = marketplaceNetwork;
this.account = account;
this.codexAccess = codexAccess;
}
public string RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
{
var request = new CodexSalesRequestStorageRequest
{
duration = ToDecInt(duration.TotalSeconds),
proofProbability = ToDecInt(proofProbability),
reward = ToDecInt(pricePerSlotPerSecond),
collateral = ToDecInt(requiredCollateral),
expiry = null,
nodes = minRequiredNumberOfNodes,
tolerance = null,
};
Log($"Requesting storage for: {contentId.Id}... (" +
$"pricePerSlotPerSecond: {pricePerSlotPerSecond}, " +
$"requiredCollateral: {requiredCollateral}, " +
$"minRequiredNumberOfNodes: {minRequiredNumberOfNodes}, " +
$"proofProbability: {proofProbability}, " +
$"duration: {Time.FormatDuration(duration)})");
var response = codexAccess.RequestStorage(request, contentId.Id);
if (response == "Purchasing not available")
{
throw new InvalidOperationException(response);
}
Log($"Storage requested successfully. PurchaseId: {response}");
return response;
}
public string MakeStorageAvailable(ByteSize totalSpace, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration)
{
var request = new CodexSalesAvailabilityRequest
{
size = ToDecInt(totalSpace.SizeInBytes),
duration = ToDecInt(maxDuration.TotalSeconds),
maxCollateral = ToDecInt(maxCollateral),
minPrice = ToDecInt(minPriceForTotalSpace)
};
Log($"Making storage available... (" +
$"size: {totalSpace}, " +
$"minPriceForTotalSpace: {minPriceForTotalSpace}, " +
$"maxCollateral: {maxCollateral}, " +
$"maxDuration: {Time.FormatDuration(maxDuration)})");
var response = codexAccess.SalesAvailability(request);
Log($"Storage successfully made available. Id: {response.id}");
return response.id;
}
private string ToDecInt(double d)
{
var i = new BigInteger(d);
return i.ToString("D");
}
public string ToDecInt(TestToken t)
{
var i = new BigInteger(t.Amount);
return i.ToString("D");
}
public void AssertThatBalance(IResolveConstraint constraint, string message = "")
{
AssertHelpers.RetryAssert(constraint, GetBalance, message);
}
public TestToken GetBalance()
{
var interaction = marketplaceNetwork.StartInteraction(lifecycle);
var amount = interaction.GetBalance(marketplaceNetwork.Marketplace.TokenAddress, account.Account);
var balance = new TestToken(amount);
Log($"Balance of {account.Account} is {balance}.");
return balance;
}
private void Log(string msg)
{
lifecycle.Log.Log($"{codexAccess.Container.Name} {msg}");
}
}
public class MarketplaceUnavailable : IMarketplaceAccess
{
public string RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
{
Unavailable();
return string.Empty;
}
public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration)
{
Unavailable();
return string.Empty;
}
public void AssertThatBalance(IResolveConstraint constraint, string message = "")
{
Unavailable();
}
public TestToken GetBalance()
{
Unavailable();
return new TestToken(0);
}
private void Unavailable()
{
Assert.Fail("Incorrect test setup: Marketplace was not enabled for this group of Codex nodes. Add 'EnableMarketplace(...)' after 'SetupCodexNodes()' to enable it.");
throw new InvalidOperationException();
}
}
}
@@ -1,41 +0,0 @@
using DistTestCore.Codex;
namespace DistTestCore.Marketplace
{
public interface IMarketplaceAccessFactory
{
IMarketplaceAccess CreateMarketplaceAccess(CodexAccess access);
}
public class MarketplaceUnavailableAccessFactory : IMarketplaceAccessFactory
{
public IMarketplaceAccess CreateMarketplaceAccess(CodexAccess access)
{
return new MarketplaceUnavailable();
}
}
public class GethMarketplaceAccessFactory : IMarketplaceAccessFactory
{
private readonly TestLifecycle lifecycle;
private readonly MarketplaceNetwork marketplaceNetwork;
public GethMarketplaceAccessFactory(TestLifecycle lifecycle, MarketplaceNetwork marketplaceNetwork)
{
this.lifecycle = lifecycle;
this.marketplaceNetwork = marketplaceNetwork;
}
public IMarketplaceAccess CreateMarketplaceAccess(CodexAccess access)
{
var companionNode = GetGethCompanionNode(access);
return new MarketplaceAccess(lifecycle, marketplaceNetwork, companionNode, access);
}
private GethAccount GetGethCompanionNode(CodexAccess access)
{
var account = access.Container.Recipe.Additionals.Single(a => a is GethAccount);
return (GethAccount)account;
}
}
}
@@ -1,17 +0,0 @@
namespace DistTestCore.Marketplace
{
public class MarketplaceInitialConfig
{
public MarketplaceInitialConfig(Ether initialEth, TestToken initialTestTokens, bool isValidator)
{
InitialEth = initialEth;
InitialTestTokens = initialTestTokens;
IsValidator = isValidator;
}
public Ether InitialEth { get; }
public TestToken InitialTestTokens { get; }
public bool IsValidator { get; }
public int? AccountIndexOverride { get; set; }
}
}
@@ -1,21 +0,0 @@
using NethereumWorkflow;
namespace DistTestCore.Marketplace
{
public class MarketplaceNetwork
{
public MarketplaceNetwork(GethBootstrapNodeInfo bootstrap, MarketplaceInfo marketplace)
{
Bootstrap = bootstrap;
Marketplace = marketplace;
}
public GethBootstrapNodeInfo Bootstrap { get; }
public MarketplaceInfo Marketplace { get; }
public NethereumInteraction StartInteraction(TestLifecycle lifecycle)
{
return Bootstrap.StartInteraction(lifecycle);
}
}
}
-81
View File
@@ -1,81 +0,0 @@
using DistTestCore.Helpers;
using KubernetesWorkflow;
using Logging;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using Utils;
namespace DistTestCore.Metrics
{
public interface IMetricsAccess
{
void AssertThat(string metricName, IResolveConstraint constraint, string message = "");
}
public class MetricsAccess : IMetricsAccess
{
private readonly BaseLog log;
private readonly ITimeSet timeSet;
private readonly MetricsQuery query;
private readonly RunningContainer node;
public MetricsAccess(BaseLog log, ITimeSet timeSet, MetricsQuery query, RunningContainer node)
{
this.log = log;
this.timeSet = timeSet;
this.query = query;
this.node = node;
}
public void AssertThat(string metricName, IResolveConstraint constraint, string message = "")
{
AssertHelpers.RetryAssert(constraint, () =>
{
var metricSet = GetMetricWithTimeout(metricName);
var metricValue = metricSet.Values[0].Value;
log.Log($"{node.Name} metric '{metricName}' = {metricValue}");
return metricValue;
}, message);
}
public Metrics? GetAllMetrics()
{
return query.GetAllMetricsForNode(node);
}
private MetricsSet GetMetricWithTimeout(string metricName)
{
var start = DateTime.UtcNow;
while (true)
{
var mostRecent = GetMostRecent(metricName);
if (mostRecent != null) return mostRecent;
if (DateTime.UtcNow - start > timeSet.WaitForMetricTimeout())
{
Assert.Fail($"Timeout: Unable to get metric '{metricName}'.");
throw new TimeoutException();
}
Time.Sleep(TimeSpan.FromSeconds(2));
}
}
private MetricsSet? GetMostRecent(string metricName)
{
var result = query.GetMostRecent(metricName, node);
if (result == null) return null;
return result.Sets.LastOrDefault();
}
}
public class MetricsUnavailable : IMetricsAccess
{
public void AssertThat(string metricName, IResolveConstraint constraint, string message = "")
{
Assert.Fail("Incorrect test setup: Metrics were not enabled for this group of Codex nodes. Add 'EnableMetrics()' after 'SetupCodexNodes()' to enable it.");
throw new InvalidOperationException();
}
}
}
@@ -1,35 +0,0 @@
using KubernetesWorkflow;
namespace DistTestCore.Metrics
{
public interface IMetricsAccessFactory
{
IMetricsAccess CreateMetricsAccess(RunningContainer codexContainer);
}
public class MetricsUnavailableAccessFactory : IMetricsAccessFactory
{
public IMetricsAccess CreateMetricsAccess(RunningContainer codexContainer)
{
return new MetricsUnavailable();
}
}
public class CodexNodeMetricsAccessFactory : IMetricsAccessFactory
{
private readonly TestLifecycle lifecycle;
private readonly RunningContainers prometheusContainer;
public CodexNodeMetricsAccessFactory(TestLifecycle lifecycle, RunningContainers prometheusContainer)
{
this.lifecycle = lifecycle;
this.prometheusContainer = prometheusContainer;
}
public IMetricsAccess CreateMetricsAccess(RunningContainer codexContainer)
{
var query = new MetricsQuery(lifecycle, prometheusContainer);
return new MetricsAccess(lifecycle.Log, lifecycle.TimeSet, query, codexContainer);
}
}
}
-162
View File
@@ -1,162 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Logs;
using DistTestCore.Marketplace;
using DistTestCore.Metrics;
using Logging;
using NUnit.Framework;
namespace DistTestCore
{
public interface IOnlineCodexNode
{
string GetName();
CodexDebugResponse GetDebugInfo();
CodexDebugPeerResponse GetDebugPeer(string peerId);
ContentId UploadFile(TestFile file);
TestFile? DownloadContent(ContentId contentId, string fileLabel = "");
void ConnectToPeer(IOnlineCodexNode node);
IDownloadedLog DownloadLog();
IMetricsAccess Metrics { get; }
IMarketplaceAccess Marketplace { get; }
ICodexSetup BringOffline();
}
public class OnlineCodexNode : IOnlineCodexNode
{
private const string SuccessfullyConnectedMessage = "Successfully connected to peer";
private const string UploadFailedMessage = "Unable to store block";
private readonly TestLifecycle lifecycle;
public OnlineCodexNode(TestLifecycle lifecycle, CodexAccess codexAccess, CodexNodeGroup group, IMetricsAccess metricsAccess, IMarketplaceAccess marketplaceAccess)
{
this.lifecycle = lifecycle;
CodexAccess = codexAccess;
Group = group;
Metrics = metricsAccess;
Marketplace = marketplaceAccess;
}
public CodexAccess CodexAccess { get; }
public CodexNodeGroup Group { get; }
public IMetricsAccess Metrics { get; }
public IMarketplaceAccess Marketplace { get; }
public string GetName()
{
return CodexAccess.Container.Name;
}
public CodexDebugResponse GetDebugInfo()
{
var debugInfo = CodexAccess.GetDebugInfo();
var known = string.Join(",", debugInfo.table.nodes.Select(n => n.peerId));
Log($"Got DebugInfo with id: '{debugInfo.id}'. This node knows: {known}");
return debugInfo;
}
public CodexDebugPeerResponse GetDebugPeer(string peerId)
{
return CodexAccess.GetDebugPeer(peerId);
}
public ContentId UploadFile(TestFile file)
{
using var fileStream = File.OpenRead(file.Filename);
var logMessage = $"Uploading file {file.Describe()}...";
var response = Stopwatch.Measure(lifecycle.Log, logMessage, () =>
{
return CodexAccess.UploadFile(fileStream);
});
if (string.IsNullOrEmpty(response)) Assert.Fail("Received empty response.");
if (response.StartsWith(UploadFailedMessage)) Assert.Fail("Node failed to store block.");
var logReplacement = $"(CID:{file.Describe()})";
Log($"ContentId '{response}' is {logReplacement}");
lifecycle.Log.AddStringReplace(response, logReplacement);
Log($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
}
public TestFile? DownloadContent(ContentId contentId, string fileLabel = "")
{
var logMessage = $"Downloading for contentId: '{contentId.Id}'...";
var file = lifecycle.FileManager.CreateEmptyTestFile(fileLabel);
Stopwatch.Measure(lifecycle.Log, logMessage, () => DownloadToFile(contentId.Id, file));
Log($"Downloaded file {file.Describe()} to '{file.Filename}'.");
return file;
}
public void ConnectToPeer(IOnlineCodexNode node)
{
var peer = (OnlineCodexNode)node;
Log($"Connecting to peer {peer.GetName()}...");
var peerInfo = node.GetDebugInfo();
var response = CodexAccess.ConnectToPeer(peerInfo.id, GetPeerMultiAddress(peer, peerInfo));
Assert.That(response, Is.EqualTo(SuccessfullyConnectedMessage), "Unable to connect codex nodes.");
Log($"Successfully connected to peer {peer.GetName()}.");
}
public IDownloadedLog DownloadLog()
{
return lifecycle.DownloadLog(CodexAccess.Container);
}
public ICodexSetup BringOffline()
{
if (Group.Count() > 1) throw new InvalidOperationException("Codex-nodes that are part of a group cannot be " +
"individually shut down. Use 'BringOffline()' on the group object to stop the group. This method is only " +
"available for codex-nodes in groups of 1.");
return Group.BringOffline();
}
private string GetPeerMultiAddress(OnlineCodexNode peer, CodexDebugResponse peerInfo)
{
var multiAddress = peerInfo.addrs.First();
// Todo: Is there a case where First address in list is not the way?
if (Group == peer.Group)
{
return multiAddress;
}
// The peer we want to connect is in a different pod.
// We must replace the default IP with the pod IP in the multiAddress.
return multiAddress.Replace("0.0.0.0", peer.Group.Containers.RunningPod.PodInfo.Ip);
}
private void DownloadToFile(string contentId, TestFile file)
{
using var fileStream = File.OpenWrite(file.Filename);
try
{
using var downloadStream = CodexAccess.DownloadFile(contentId);
downloadStream.CopyTo(fileStream);
}
catch
{
Log($"Failed to download file '{contentId}'.");
throw;
}
}
private void Log(string msg)
{
lifecycle.Log.Log($"{GetName()}: {msg}");
}
}
public class ContentId
{
public ContentId(string id)
{
Id = id;
}
public string Id { get; }
}
}
-54
View File
@@ -1,54 +0,0 @@
using DistTestCore.Codex;
using DistTestCore.Metrics;
using KubernetesWorkflow;
using System.Text;
namespace DistTestCore
{
public class PrometheusStarter : BaseStarter
{
public PrometheusStarter(TestLifecycle lifecycle, WorkflowCreator workflowCreator)
: base(lifecycle, workflowCreator)
{
}
public RunningContainers CollectMetricsFor(RunningContainers containers)
{
LogStart($"Starting metrics server for {containers.Describe()}");
var startupConfig = new StartupConfig();
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(containers.Containers)));
var workflow = workflowCreator.CreateWorkflow();
var runningContainers = workflow.Start(1, Location.Unspecified, new PrometheusContainerRecipe(), startupConfig);
if (runningContainers.Containers.Length != 1) throw new InvalidOperationException("Expected only 1 Prometheus container to be created.");
LogEnd("Metrics server started.");
return runningContainers;
}
private string GeneratePrometheusConfig(RunningContainer[] nodes)
{
var config = "";
config += "global:\n";
config += " scrape_interval: 30s\n";
config += " scrape_timeout: 10s\n";
config += "\n";
config += "scrape_configs:\n";
config += " - job_name: services\n";
config += " metrics_path: /metrics\n";
config += " static_configs:\n";
config += " - targets:\n";
foreach (var node in nodes)
{
var ip = node.Pod.PodInfo.Ip;
var port = node.Recipe.GetPortByTag(CodexContainerRecipe.MetricsPortTag).Number;
config += $" - '{ip}:{port}'\n";
}
var bytes = Encoding.ASCII.GetBytes(config);
return Convert.ToBase64String(bytes);
}
}
}
-62
View File
@@ -1,62 +0,0 @@
using DistTestCore.Logs;
using KubernetesWorkflow;
using Logging;
using Utils;
namespace DistTestCore
{
public class TestLifecycle
{
private DateTime testStart = DateTime.MinValue;
public TestLifecycle(BaseLog log, Configuration configuration, ITimeSet timeSet)
: this(log, configuration, timeSet, new WorkflowCreator(log, configuration.GetK8sConfiguration(timeSet)))
{
}
public TestLifecycle(BaseLog log, Configuration configuration, ITimeSet timeSet, WorkflowCreator workflowCreator)
{
Log = log;
Configuration = configuration;
TimeSet = timeSet;
FileManager = new FileManager(Log, configuration);
CodexStarter = new CodexStarter(this, workflowCreator);
PrometheusStarter = new PrometheusStarter(this, workflowCreator);
GethStarter = new GethStarter(this, workflowCreator);
testStart = DateTime.UtcNow;
}
public BaseLog Log { get; }
public Configuration Configuration { get; }
public ITimeSet TimeSet { get; }
public FileManager FileManager { get; }
public CodexStarter CodexStarter { get; }
public PrometheusStarter PrometheusStarter { get; }
public GethStarter GethStarter { get; }
public void DeleteAllResources()
{
CodexStarter.DeleteAllResources();
FileManager.DeleteAllTestFiles();
}
public IDownloadedLog DownloadLog(RunningContainer container)
{
var subFile = Log.CreateSubfile();
var description = container.Name;
var handler = new LogDownloadHandler(container, description, subFile);
Log.Log($"Downloading logs for {description} to file '{subFile.FullFilename}'");
CodexStarter.DownloadLog(container, handler);
return new DownloadedLog(subFile, description);
}
public string GetTestDuration()
{
var testDuration = DateTime.UtcNow - testStart;
return Time.FormatDuration(testDuration);
}
}
}
-97
View File
@@ -1,97 +0,0 @@
namespace DistTestCore
{
public class Ether : IComparable<Ether>
{
public Ether(decimal wei)
{
Wei = wei;
}
public decimal Wei { get; }
public int CompareTo(Ether? other)
{
return Wei.CompareTo(other!.Wei);
}
public override bool Equals(object? obj)
{
return obj is Ether ether && Wei == ether.Wei;
}
public override int GetHashCode()
{
return HashCode.Combine(Wei);
}
public override string ToString()
{
return $"{Wei} Wei";
}
}
public class TestToken : IComparable<TestToken>
{
public TestToken(decimal amount)
{
Amount = amount;
}
public decimal Amount { get; }
public int CompareTo(TestToken? other)
{
return Amount.CompareTo(other!.Amount);
}
public override bool Equals(object? obj)
{
return obj is TestToken token && Amount == token.Amount;
}
public override int GetHashCode()
{
return HashCode.Combine(Amount);
}
public override string ToString()
{
return $"{Amount} TestTokens";
}
}
public static class TokensIntExtensions
{
private const decimal weiPerEth = 1000000000000000000;
public static TestToken TestTokens(this int i)
{
return TestTokens(Convert.ToDecimal(i));
}
public static TestToken TestTokens(this decimal i)
{
return new TestToken(i);
}
public static Ether Eth(this int i)
{
return Eth(Convert.ToDecimal(i));
}
public static Ether Wei(this int i)
{
return Wei(Convert.ToDecimal(i));
}
public static Ether Eth(this decimal i)
{
return new Ether(i * weiPerEth);
}
public static Ether Wei(this decimal i)
{
return new Ether(i);
}
}
}
@@ -233,7 +233,8 @@ namespace ArgsUniform
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
{
if (value != null)
var s = value.ToString();
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
{
uniformProperty.SetValue(result, true);
}
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\FileUtils\FileUtils.csproj" />
<ProjectReference Include="..\KubernetesWorkflow\KubernetesWorkflow.csproj" />
</ItemGroup>
</Project>
+55
View File
@@ -0,0 +1,55 @@
using KubernetesWorkflow;
namespace Core
{
public sealed class CoreInterface
{
private readonly EntryPoint entryPoint;
internal CoreInterface(EntryPoint entryPoint)
{
this.entryPoint = entryPoint;
}
public T GetPlugin<T>() where T : IProjectPlugin
{
return entryPoint.GetPlugin<T>();
}
public IKnownLocations GetKnownLocations()
{
return entryPoint.Tools.CreateWorkflow().GetAvailableLocations();
}
public IDownloadedLog DownloadLog(IHasContainer containerSource, int? tailLines = null)
{
return DownloadLog(containerSource.Container, tailLines);
}
public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null)
{
var workflow = entryPoint.Tools.CreateWorkflow();
var file = entryPoint.Tools.GetLog().CreateSubfile();
entryPoint.Tools.GetLog().Log($"Downloading container log for '{container.Name}' to file '{file.FullFilename}'...");
var logHandler = new LogDownloadHandler(container.Name, file);
workflow.DownloadContainerLog(container, logHandler, tailLines);
return logHandler.DownloadLog();
}
public string ExecuteContainerCommand(IHasContainer containerSource, string command, params string[] args)
{
return ExecuteContainerCommand(containerSource.Container, command, args);
}
public string ExecuteContainerCommand(RunningContainer container, string command, params string[] args)
{
var workflow = entryPoint.Tools.CreateWorkflow();
return workflow.ExecuteCommand(container, command, args);
}
}
public interface IHasContainer
{
RunningContainer Container { get; }
}
}
+62
View File
@@ -0,0 +1,62 @@
using Logging;
namespace Core
{
public interface IDownloadedLog
{
bool DoesLogContain(string expectedString);
string[] FindLinesThatContain(params string[] tags);
void DeleteFile();
}
internal class DownloadedLog : IDownloadedLog
{
private readonly LogFile logFile;
internal DownloadedLog(LogFile logFile)
{
this.logFile = logFile;
}
public bool DoesLogContain(string expectedString)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var line = streamReader.ReadLine();
while (line != null)
{
if (line.Contains(expectedString)) return true;
line = streamReader.ReadLine();
}
//Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}");
return false;
}
public string[] FindLinesThatContain(params string[] tags)
{
var result = new List<string>();
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var line = streamReader.ReadLine();
while (line != null)
{
if (tags.All(line.Contains))
{
result.Add(line);
}
line = streamReader.ReadLine();
}
return result.ToArray();
}
public void DeleteFile()
{
File.Delete(logFile.FullFilename);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
using KubernetesWorkflow;
using Logging;
namespace Core
{
public class EntryPoint
{
private readonly IToolsFactory toolsFactory;
private readonly PluginManager manager = new PluginManager();
public EntryPoint(ILog log, Configuration configuration, string fileManagerRootFolder, ITimeSet timeSet)
{
toolsFactory = new ToolsFactory(log, configuration, fileManagerRootFolder, timeSet);
Tools = toolsFactory.CreateTools();
manager.InstantiatePlugins(PluginFinder.GetPluginTypes(), toolsFactory);
}
public EntryPoint(ILog log, Configuration configuration, string fileManagerRootFolder)
: this(log, configuration, fileManagerRootFolder, new DefaultTimeSet())
{
}
public IPluginTools Tools { get; }
public void Announce()
{
manager.AnnouncePlugins();
}
public Dictionary<string, string> GetPluginMetadata()
{
return manager.GatherPluginMetadata().Get();
}
public CoreInterface CreateInterface()
{
return new CoreInterface(this);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
{
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles);
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
}
internal T GetPlugin<T>() where T : IProjectPlugin
{
return manager.GetPlugin<T>();
}
}
}
+228
View File
@@ -0,0 +1,228 @@
using Logging;
using Newtonsoft.Json;
using Serialization = Newtonsoft.Json.Serialization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Utils;
namespace Core
{
public interface IHttp
{
string HttpGetString(string route);
T HttpGetJson<T>(string route);
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
string HttpPostJson<TRequest>(string route, TRequest body);
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
T Deserialize<T>(string json);
}
internal class Http : IHttp
{
private static readonly object httpLock = new object();
private readonly ILog log;
private readonly ITimeSet timeSet;
private readonly Address address;
private readonly string baseUrl;
private readonly Action<HttpClient> onClientCreated;
private readonly string? logAlias;
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, string? logAlias = null)
: this(log, timeSet, address, baseUrl, DoNothing, logAlias)
{
}
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
{
this.log = log;
this.timeSet = timeSet;
this.address = address;
this.baseUrl = baseUrl;
this.onClientCreated = onClientCreated;
this.logAlias = logAlias;
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
}
public string HttpGetString(string route)
{
return LockRetry(() =>
{
return GetString(route);
}, $"HTTP-GET:{route}");
}
public T HttpGetJson<T>(string route)
{
return LockRetry(() =>
{
var json = GetString(route);
return Deserialize<T>(json);
}, $"HTTP-GET:{route}");
}
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{
return LockRetry(() =>
{
var response = PostJson(route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(json);
}
Log(GetUrl() + route, json);
return Deserialize<TResponse>(json);
}, $"HTTP-POST-JSON: {route}");
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
return LockRetry(() =>
{
var response = PostJson(route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}, $"HTTP-POST-JSON: {route}");
}
public TResponse HttpPostString<TResponse>(string route, string body)
{
return LockRetry(() =>
{
var response = PostJsonString(route, body);
if (response == null) throw new Exception("Received no response.");
var result = Deserialize<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
}, $"HTTO-POST-JSON: {route}");
}
public string HttpPostStream(string route, Stream stream)
{
return LockRetry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STREAM: {route}");
}
public Stream HttpGetStream(string route)
{
return LockRetry(() =>
{
var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url));
}, $"HTTP-GET-STREAM: {route}");
}
public T Deserialize<T>(string json)
{
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate(object? sender, Serialization.ErrorEventArgs args)
{
if (args.CurrentObject == args.ErrorContext.OriginalObject)
{
errors.Add($"""
Member: '{args.ErrorContext.Member?.ToString() ?? "<null>"}'
Path: {args.ErrorContext.Path}
Error: {args.ErrorContext.Error.Message}
""");
args.ErrorContext.Handled = true;
}
}
});
if (errors.Count > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
}
else if (deserialized == null)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
}
return deserialized;
}
private string GetString(string route)
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private HttpResponseMessage PostJson<TRequest>(string route, TRequest body)
{
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}
private string PostJsonString(string route, string body)
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
if (logAlias != null)
{
log.Debug($"({logAlias})({url}) = '{message}'", 3);
}
else
{
log.Debug($"({url}) = '{message}'", 3);
}
}
private T LockRetry<T>(Func<T> operation, string description)
{
lock (httpLock)
{
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
}
}
private HttpClient GetClient()
{
var client = new HttpClient();
client.Timeout = timeSet.HttpCallTimeout();
onClientCreated(client);
return client;
}
private static void DoNothing(HttpClient client)
{
}
}
}
@@ -1,25 +1,23 @@
using KubernetesWorkflow;
using Logging;
namespace DistTestCore.Logs
namespace Core
{
public class LogDownloadHandler : LogHandler, ILogHandler
internal class LogDownloadHandler : LogHandler, ILogHandler
{
private readonly RunningContainer container;
private readonly LogFile log;
public LogDownloadHandler(RunningContainer container, string description, LogFile log)
internal LogDownloadHandler(string description, LogFile log)
{
this.container = container;
this.log = log;
log.Write($"{description} -->> {log.FullFilename}");
log.WriteRaw(description);
}
public DownloadedLog DownloadLog()
internal IDownloadedLog DownloadLog()
{
return new DownloadedLog(log, container.Name);
return new DownloadedLog(log);
}
protected override void ProcessLine(string line)
+46
View File
@@ -0,0 +1,46 @@
using System.Reflection;
namespace Core
{
internal static class PluginFinder
{
private static Type[]? pluginTypes = null;
internal static Type[] GetPluginTypes()
{
if (pluginTypes != null) return pluginTypes;
// Reflection can be costly. Do this only once.
FindAndLoadPluginAssemblies();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
pluginTypes = assemblies.SelectMany(a => a.GetTypes().Where(t =>
typeof(IProjectPlugin).IsAssignableFrom(t) &&
!t.IsAbstract)
).ToArray();
return pluginTypes;
}
private static void FindAndLoadPluginAssemblies()
{
var files = Directory.GetFiles(".");
foreach (var file in files)
{
var f = file.ToLowerInvariant();
if (f.Contains("plugin") && f.EndsWith("dll"))
{
var name = Path.GetFileNameWithoutExtension(file);
try
{
Assembly.Load(name);
}
catch (Exception ex)
{
throw new Exception($"Failed to load plugin from file '{name}'.", ex);
}
}
}
}
}
}
+78
View File
@@ -0,0 +1,78 @@
namespace Core
{
internal class PluginManager
{
private readonly List<PluginToolsPair> pairs = new List<PluginToolsPair>();
internal void InstantiatePlugins(Type[] pluginTypes, IToolsFactory provider)
{
pairs.Clear();
foreach (var pluginType in pluginTypes)
{
var tools = provider.CreateTools();
var plugin = InstantiatePlugins(pluginType, tools);
ApplyLogPrefix(plugin, tools);
}
}
internal void AnnouncePlugins()
{
foreach (var pair in pairs) pair.Plugin.Announce();
}
internal PluginMetadata GatherPluginMetadata()
{
var metadata = new PluginMetadata();
foreach (var pair in pairs)
{
if (pair.Plugin is IHasMetadata m)
{
m.AddMetadata(metadata);
}
}
return metadata;
}
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles)
{
foreach (var pair in pairs)
{
pair.Plugin.Decommission();
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
}
}
internal T GetPlugin<T>() where T : IProjectPlugin
{
return (T)pairs.Single(p => p.Plugin.GetType() == typeof(T)).Plugin;
}
private IProjectPlugin InstantiatePlugins(Type pluginType, PluginTools tools)
{
var plugin = (IProjectPlugin)Activator.CreateInstance(pluginType, args: tools)!;
pairs.Add(new PluginToolsPair(plugin, tools));
return plugin;
}
private void ApplyLogPrefix(IProjectPlugin plugin, PluginTools tools)
{
if (plugin is IHasLogPrefix hasLogPrefix)
{
tools.ApplyLogPrefix(hasLogPrefix.LogPrefix);
}
}
private class PluginToolsPair
{
public PluginToolsPair(IProjectPlugin plugin, IPluginTools tools)
{
Plugin = plugin;
Tools = tools;
}
public IProjectPlugin Plugin { get; }
public IPluginTools Tools { get; }
}
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace Core
{
internal interface IPluginMetadata
{
Dictionary<string, string> Get();
}
public interface IAddMetadata
{
void Add(string key, string value);
}
internal class PluginMetadata : IPluginMetadata, IAddMetadata
{
private readonly Dictionary<string, string> metadata = new Dictionary<string, string>();
public void Add(string key, string value)
{
metadata.Add(key, value);
}
public Dictionary<string, string> Get()
{
return new Dictionary<string, string>(metadata);
}
}
}
+91
View File
@@ -0,0 +1,91 @@
using FileUtils;
using KubernetesWorkflow;
using Logging;
using Utils;
namespace Core
{
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool
{
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles);
}
public interface IWorkflowTool
{
IStartupWorkflow CreateWorkflow(string? namespaceOverride = null);
}
public interface ILogTool
{
ILog GetLog();
}
public interface IHttpFactoryTool
{
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null);
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet timeSet, string? logAlias = null);
IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null);
}
public interface IFileTool
{
IFileManager GetFileManager();
}
internal class PluginTools : IPluginTools
{
private readonly ITimeSet timeSet;
private readonly WorkflowCreator workflowCreator;
private readonly IFileManager fileManager;
private ILog log;
internal PluginTools(ILog log, WorkflowCreator workflowCreator, string fileManagerRootFolder, ITimeSet timeSet)
{
this.log = log;
this.workflowCreator = workflowCreator;
this.timeSet = timeSet;
fileManager = new FileManager(log, fileManagerRootFolder);
}
public void ApplyLogPrefix(string prefix)
{
log = new LogPrefixer(log, prefix);
}
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
{
return CreateHttp(address, baseUrl, onClientCreated, timeSet, logAlias);
}
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet ts, string? logAlias = null)
{
return new Http(log, ts, address, baseUrl, onClientCreated, logAlias);
}
public IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null)
{
return new Http(log, timeSet, address, baseUrl, logAlias);
}
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
{
return workflowCreator.CreateWorkflow(namespaceOverride);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
{
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace();
if (deleteTrackedFiles) fileManager.DeleteAllFiles();
}
public IFileManager GetFileManager()
{
return fileManager;
}
public ILog GetLog()
{
return log;
}
}
}
+34
View File
@@ -0,0 +1,34 @@
using Utils;
namespace Core
{
public interface IProjectPlugin
{
void Announce();
void Decommission();
}
public interface IHasLogPrefix
{
string LogPrefix { get; }
}
public interface IHasMetadata
{
void AddMetadata(IAddMetadata metadata);
}
public static class ProjectPlugin
{
/// <summary>
/// On some platforms and in some cases, not all required plugin assemblies are automatically loaded into the app domain.
/// In this case, the runtime needs a slight push to load it before the EntryPoint class is instantiated.
/// Used ProjectPlugin.Load<>() before you create an EntryPoint to ensure all plugins you want to use are loaded.
/// </summary>
public static void Load<T>() where T : IProjectPlugin
{
var type = typeof(T);
FrameworkAssert.That(type != null, $"Unable to load plugin.");
}
}
}
+18
View File
@@ -0,0 +1,18 @@
using Newtonsoft.Json;
namespace Core
{
public static class SerializeGate
{
/// <summary>
/// SerializeGate was added to help ensure deployment objects are serializable and remain viable after deserialization.
/// Tools can be built on top of the core interface that rely on deployment objects being serializable.
/// Insert the serialization gate after deployment but before wrapping to ensure any future changes don't break this requirement.
/// </summary>
public static T Gate<T>(T anything)
{
var json = JsonConvert.SerializeObject(anything);
return JsonConvert.DeserializeObject<T>(json)!;
}
}
}
@@ -1,32 +1,24 @@
using NUnit.Framework;
namespace DistTestCore
namespace Core
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class UseLongTimeoutsAttribute : PropertyAttribute
{
}
public interface ITimeSet
{
TimeSpan HttpCallTimeout();
TimeSpan HttpCallRetryTime();
int HttpMaxNumberOfRetries();
TimeSpan HttpCallRetryDelay();
TimeSpan WaitForK8sServiceDelay();
TimeSpan K8sOperationTimeout();
TimeSpan WaitForMetricTimeout();
}
public class DefaultTimeSet : ITimeSet
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromSeconds(10);
return TimeSpan.FromMinutes(3);
}
public TimeSpan HttpCallRetryTime()
public int HttpMaxNumberOfRetries()
{
return TimeSpan.FromMinutes(1);
return 3;
}
public TimeSpan HttpCallRetryDelay()
@@ -36,17 +28,12 @@ namespace DistTestCore
public TimeSpan WaitForK8sServiceDelay()
{
return TimeSpan.FromSeconds(1);
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(1);
}
public TimeSpan WaitForMetricTimeout()
{
return TimeSpan.FromSeconds(30);
return TimeSpan.FromMinutes(30);
}
}
@@ -57,9 +44,9 @@ namespace DistTestCore
return TimeSpan.FromHours(2);
}
public TimeSpan HttpCallRetryTime()
public int HttpMaxNumberOfRetries()
{
return TimeSpan.FromHours(5);
return 1;
}
public TimeSpan HttpCallRetryDelay()
@@ -76,10 +63,5 @@ namespace DistTestCore
{
return TimeSpan.FromMinutes(15);
}
public TimeSpan WaitForMetricTimeout()
{
return TimeSpan.FromMinutes(5);
}
}
}
+31
View File
@@ -0,0 +1,31 @@
using KubernetesWorkflow;
using Logging;
namespace Core
{
internal interface IToolsFactory
{
PluginTools CreateTools();
}
internal class ToolsFactory : IToolsFactory
{
private readonly ILog log;
private readonly WorkflowCreator workflowCreator;
private readonly string fileManagerRootFolder;
private readonly ITimeSet timeSet;
public ToolsFactory(ILog log, Configuration configuration, string fileManagerRootFolder, ITimeSet timeSet)
{
this.log = log;
workflowCreator = new WorkflowCreator(log, configuration);
this.fileManagerRootFolder = fileManagerRootFolder;
this.timeSet = timeSet;
}
public PluginTools CreateTools()
{
return new PluginTools(log, workflowCreator, fileManagerRootFolder, timeSet);
}
}
}
+157
View File
@@ -0,0 +1,157 @@
using Logging;
using Utils;
namespace FileUtils
{
public interface IFileManager
{
TrackedFile CreateEmptyFile(string label = "");
TrackedFile GenerateFile(ByteSize size, string label = "");
void DeleteAllFiles();
void ScopedFiles(Action action);
T ScopedFiles<T>(Func<T> action);
}
public class FileManager : IFileManager
{
public const int ChunkSize = 1024 * 1024 * 100;
private static NumberSource folderNumberSource = new NumberSource(0);
private readonly Random random = new Random();
private readonly ILog log;
private readonly string rootFolder;
private readonly string folder;
private readonly List<List<TrackedFile>> fileSetStack = new List<List<TrackedFile>>();
public FileManager(ILog log, string rootFolder)
{
folder = Path.Combine(rootFolder, folderNumberSource.GetNextNumber().ToString("D5"));
this.log = log;
this.rootFolder = rootFolder;
}
public TrackedFile CreateEmptyFile(string label = "")
{
var path = Path.Combine(folder, Guid.NewGuid().ToString() + ".bin");
EnsureDirectory();
var result = new TrackedFile(log, path, label);
File.Create(result.Filename).Close();
if (fileSetStack.Any()) fileSetStack.Last().Add(result);
return result;
}
public TrackedFile GenerateFile(ByteSize size, string label)
{
var sw = Stopwatch.Begin(log);
var result = GenerateRandomFile(size, label);
sw.End($"Generated file '{result.Describe()}'.");
return result;
}
public void DeleteAllFiles()
{
DeleteDirectory();
}
public void ScopedFiles(Action action)
{
PushFileSet();
action();
PopFileSet();
}
public T ScopedFiles<T>(Func<T> action)
{
PushFileSet();
var result = action();
PopFileSet();
return result;
}
private void PushFileSet()
{
fileSetStack.Add(new List<TrackedFile>());
}
private void PopFileSet()
{
if (!fileSetStack.Any()) return;
var pop = fileSetStack.Last();
fileSetStack.Remove(pop);
foreach (var file in pop)
{
File.Delete(file.Filename);
}
// If the folder is now empty, delete it too.
if (!Directory.GetFiles(folder).Any()) DeleteDirectory();
}
private TrackedFile GenerateRandomFile(ByteSize size, string label)
{
var result = CreateEmptyFile(label);
CheckSpaceAvailable(result, size);
GenerateFileBytes(result, size);
return result;
}
private void CheckSpaceAvailable(TrackedFile testFile, ByteSize size)
{
var file = new FileInfo(testFile.Filename);
var drive = new DriveInfo(file.Directory!.Root.FullName);
var spaceAvailable = drive.TotalFreeSpace;
if (spaceAvailable < size.SizeInBytes)
{
var msg = $"Not enough disk space. " +
$"{Formatter.FormatByteSize(size.SizeInBytes)} required. " +
$"{Formatter.FormatByteSize(spaceAvailable)} available.";
log.Log(msg);
throw new Exception(msg);
}
}
private void GenerateFileBytes(TrackedFile result, ByteSize size)
{
long bytesLeft = size.SizeInBytes;
int chunkSize = ChunkSize;
while (bytesLeft > 0)
{
try
{
var length = Math.Min(bytesLeft, chunkSize);
AppendRandomBytesToFile(result, length);
bytesLeft -= length;
}
catch
{
chunkSize = chunkSize / 2;
if (chunkSize < 1024) throw;
}
}
}
private void AppendRandomBytesToFile(TrackedFile result, long length)
{
var bytes = new byte[length];
random.NextBytes(bytes);
using var stream = new FileStream(result.Filename, FileMode.Append);
stream.Write(bytes, 0, bytes.Length);
}
private void EnsureDirectory()
{
Directory.CreateDirectory(folder);
}
private void DeleteDirectory()
{
if (Directory.Exists(folder)) Directory.Delete(folder, true);
}
}
}
+14
View File
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Logging\Logging.csproj" />
<ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>
</Project>
+82
View File
@@ -0,0 +1,82 @@
using Logging;
using Utils;
namespace FileUtils
{
public class TrackedFile
{
private readonly ILog log;
public TrackedFile(ILog log, string filename, string label)
{
this.log = log;
Filename = filename;
Label = label;
}
public string Filename { get; }
public string Label { get; }
public void AssertIsEqual(TrackedFile? actual)
{
var sw = Stopwatch.Begin(log);
try
{
AssertEqual(actual);
}
finally
{
sw.End($"{nameof(TrackedFile)}.{nameof(AssertIsEqual)}");
}
}
public string Describe()
{
var sizePostfix = $" ({Formatter.FormatByteSize(GetFileSize())})";
if (!string.IsNullOrEmpty(Label)) return Label + sizePostfix;
return $"'{Filename}'{sizePostfix}";
}
private void AssertEqual(TrackedFile? actual)
{
if (actual == null) FrameworkAssert.Fail("TestFile is null.");
if (actual == this || actual!.Filename == Filename) FrameworkAssert.Fail("TestFile is compared to itself.");
FrameworkAssert.That(actual.GetFileSize() == GetFileSize(), "Files are not of equal length.");
using var streamExpected = new FileStream(Filename, FileMode.Open, FileAccess.Read);
using var streamActual = new FileStream(actual.Filename, FileMode.Open, FileAccess.Read);
var bytesExpected = new byte[FileManager.ChunkSize];
var bytesActual = new byte[FileManager.ChunkSize];
var readExpected = 0;
var readActual = 0;
while (true)
{
readExpected = streamExpected.Read(bytesExpected, 0, FileManager.ChunkSize);
readActual = streamActual.Read(bytesActual, 0, FileManager.ChunkSize);
if (readExpected == 0 && readActual == 0)
{
log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'.");
return;
}
FrameworkAssert.That(readActual == readExpected, "Unable to read buffers of equal length.");
for (var i = 0; i < readActual; i++)
{
if (bytesExpected[i] != bytesActual[i]) FrameworkAssert.Fail("File contents not equal.");
}
}
}
private long GetFileSize()
{
var info = new FileInfo(Filename);
return info.Length;
}
}
}
@@ -0,0 +1,41 @@
using Utils;
namespace KubernetesWorkflow
{
public static class ByteSizeExtensions
{
public static string ToSuffixNotation(this ByteSize b)
{
long x = 1024;
var map = new Dictionary<long, string>
{
{ Pow(x, 4), "Ti" },
{ Pow(x, 3), "Gi" },
{ Pow(x, 2), "Mi" },
{ (x), "Ki" },
};
var bytes = b.SizeInBytes;
foreach (var pair in map)
{
if (bytes > pair.Key)
{
double bytesD = bytes;
double divD = pair.Key;
double numD = Math.Ceiling(bytesD / divD);
var v = Convert.ToInt64(numD);
return $"{v}{pair.Value}";
}
}
return $"{bytes}";
}
private static long Pow(long x, int v)
{
long result = 1;
for (var i = 0; i < v; i++) result *= x;
return result;
}
}
}
@@ -1,5 +1,4 @@
using k8s;
using Utils;
using Utils;
namespace KubernetesWorkflow
{
@@ -0,0 +1,25 @@
using Newtonsoft.Json;
namespace KubernetesWorkflow
{
public class Configuration
{
public Configuration(string? kubeConfigFile, TimeSpan operationTimeout, TimeSpan retryDelay, string kubernetesNamespace)
{
KubeConfigFile = kubeConfigFile;
OperationTimeout = operationTimeout;
RetryDelay = retryDelay;
KubernetesNamespace = kubernetesNamespace;
}
public string? KubeConfigFile { get; }
public TimeSpan OperationTimeout { get; }
public TimeSpan RetryDelay { get; }
public string KubernetesNamespace { get; }
public bool AllowNamespaceOverride { get; set; } = true;
public bool AddAppPodLabel { get; set; } = true;
[JsonIgnore]
public IK8sHooks Hooks { get; set; } = new DoNothingK8sHooks();
}
}
@@ -0,0 +1,53 @@
using Newtonsoft.Json.Linq;
namespace KubernetesWorkflow
{
public class ContainerAdditionals
{
public ContainerAdditionals(Additional[] additionals)
{
Additionals = additionals;
}
public static ContainerAdditionals CreateFromUserData(IEnumerable<object> userData)
{
return new ContainerAdditionals(userData.Select(ConvertToAdditional).ToArray());
}
public Additional[] Additionals { get; }
public T? Get<T>()
{
var typeName = GetTypeName(typeof(T));
var userData = Additionals.SingleOrDefault(a => a.Type == typeName);
if (userData == null) return default(T);
var jobject = (JObject)userData.UserData;
return jobject.ToObject<T>();
}
private static Additional ConvertToAdditional(object userData)
{
var typeName = GetTypeName(userData.GetType());
return new Additional(typeName, userData);
}
private static string GetTypeName(Type type)
{
var typeName = type.FullName;
if (string.IsNullOrEmpty(typeName)) throw new Exception("Object type fullname is null or empty: " + type);
return typeName;
}
}
public class Additional
{
public Additional(string type, object userData)
{
Type = type;
UserData = userData;
}
public string Type { get; }
public object UserData { get; }
}
}
@@ -0,0 +1,132 @@
namespace KubernetesWorkflow
{
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)
{
Number = number;
NameOverride = nameOverride;
Image = image;
Resources = resources;
ExposedPorts = exposedPorts;
InternalPorts = internalPorts;
EnvVars = envVars;
PodLabels = podLabels;
PodAnnotations = podAnnotations;
Volumes = volumes;
Additionals = additionals;
if (NameOverride != null)
{
Name = $"{K8sNameUtils.Format(NameOverride)}-{Number}";
}
else
{
Name = $"ctnr{Number}";
}
if (exposedPorts.Any(p => string.IsNullOrEmpty(p.Tag))) throw new Exception("Port tags are required for all exposed ports.");
}
public string Name { get; }
public int Number { get; }
public string? NameOverride { get; }
public ContainerResources Resources { get; }
public string Image { get; }
public Port[] ExposedPorts { get; }
public Port[] InternalPorts { get; }
public EnvVar[] EnvVars { get; }
public PodLabels PodLabels { get; }
public PodAnnotations PodAnnotations { get; }
public VolumeMount[] Volumes { get; }
public ContainerAdditionals Additionals { get; }
public Port? GetPortByTag(string tag)
{
return ExposedPorts.Concat(InternalPorts).SingleOrDefault(p => p.Tag == tag);
}
public override string ToString()
{
return $"(container-recipe: {Name}, image: {Image}, " +
$"exposedPorts: {string.Join(",", ExposedPorts.Select(p => p.Number))}, " +
$"internalPorts: {string.Join(",", InternalPorts.Select(p => p.Number))}, " +
$"envVars: {string.Join(",", EnvVars.Select(v => v.ToString()))}, " +
$"limits: {Resources}, " +
$"volumes: {string.Join(",", Volumes.Select(v => $"'{v.MountPath}'"))}";
}
}
public class Port
{
public Port(int number, string tag, PortProtocol protocol)
{
Number = number;
Tag = tag;
Protocol = protocol;
}
public int Number { get; }
public string Tag { get; }
public PortProtocol Protocol { get; }
public bool IsTcp()
{
return Protocol == PortProtocol.TCP;
}
public bool IsUdp()
{
return Protocol == PortProtocol.UDP;
}
public override string ToString()
{
if (string.IsNullOrEmpty(Tag)) return $"untagged-port={Number}/{Protocol}";
return $"{Tag}={Number}/{Protocol}";
}
}
public enum PortProtocol
{
TCP,
UDP
}
public class EnvVar
{
public EnvVar(string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
public override string ToString()
{
return $"'{Name}' = '{Value}'";
}
}
public class VolumeMount
{
public VolumeMount(string volumeName, string mountPath, string? subPath = null, string? resourceQuantity = null, string? secret = null, string? hostPath = null)
{
VolumeName = volumeName;
MountPath = mountPath;
SubPath = subPath;
ResourceQuantity = resourceQuantity;
Secret = secret;
HostPath = hostPath;
}
public string VolumeName { get; }
public string MountPath { get; }
public string? SubPath { get; }
public string? ResourceQuantity { get; }
public string? Secret { get; }
public string? HostPath { get; }
}
}
@@ -0,0 +1,145 @@
using Utils;
namespace KubernetesWorkflow
{
public abstract class ContainerRecipeFactory
{
private readonly List<Port> exposedPorts = new List<Port>();
private readonly List<Port> internalPorts = new List<Port>();
private readonly List<EnvVar> envVars = new List<EnvVar>();
private readonly PodLabels podLabels = new PodLabels();
private readonly PodAnnotations podAnnotations = new PodAnnotations();
private readonly List<VolumeMount> volumeMounts = new List<VolumeMount>();
private readonly List<object> additionals = new List<object>();
private RecipeComponentFactory factory = null!;
private ContainerResources resources = new ContainerResources();
public ContainerRecipe CreateRecipe(int index, int containerNumber, RecipeComponentFactory factory, StartupConfig config)
{
this.factory = factory;
ContainerNumber = containerNumber;
Index = index;
Initialize(config);
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources,
exposedPorts.ToArray(),
internalPorts.ToArray(),
envVars.ToArray(),
podLabels.Clone(),
podAnnotations.Clone(),
volumeMounts.ToArray(),
ContainerAdditionals.CreateFromUserData(additionals));
exposedPorts.Clear();
internalPorts.Clear();
envVars.Clear();
podLabels.Clear();
podAnnotations.Clear();
volumeMounts.Clear();
additionals.Clear();
this.factory = null!;
resources = new ContainerResources();
return recipe;
}
public abstract string AppName { get; }
public abstract string Image { get; }
protected int ContainerNumber { get; private set; } = 0;
protected int Index { get; private set; } = 0;
protected abstract void Initialize(StartupConfig config);
protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP)
{
return AddExposedPort(factory.CreatePort(tag, protocol));
}
protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP)
{
return AddExposedPort(factory.CreatePort(number, tag, protocol));
}
protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
{
var p = factory.CreatePort(tag, protocol);
internalPorts.Add(p);
return p;
}
protected void AddExposedPortAndVar(string name, string tag, PortProtocol protocol = PortProtocol.TCP)
{
AddEnvVar(name, AddExposedPort(tag, protocol));
}
protected void AddInternalPortAndVar(string name, string tag = "", PortProtocol protocol = PortProtocol.TCP)
{
AddEnvVar(name, AddInternalPort(tag, protocol));
}
protected void AddEnvVar(string name, string value)
{
envVars.Add(factory.CreateEnvVar(name, value));
}
protected void AddEnvVar(string name, Port value)
{
envVars.Add(factory.CreateEnvVar(name, value.Number));
}
protected void AddPodLabel(string name, string value)
{
podLabels.Add(name, value);
}
protected void AddPodAnnotation(string name, string value)
{
podAnnotations.Add(name, value);
}
protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null)
{
var size = 10.MB().ToSuffixNotation();
volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath));
}
protected void AddVolume(string mountPath, ByteSize volumeSize)
{
volumeMounts.Add(new VolumeMount(
$"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}",
mountPath,
resourceQuantity: volumeSize.ToSuffixNotation()));
}
protected void Additional(object userData)
{
additionals.Add(userData);
}
protected void SetResourcesRequest(int milliCPUs, ByteSize memory)
{
SetResourcesRequest(new ContainerResourceSet(milliCPUs, memory));
}
protected void SetResourceLimits(int milliCPUs, ByteSize memory)
{
SetResourceLimits(new ContainerResourceSet(milliCPUs, memory));
}
protected void SetResourcesRequest(ContainerResourceSet requests)
{
resources.Requests = requests;
}
protected void SetResourceLimits(ContainerResourceSet limits)
{
resources.Limits = limits;
}
private Port AddExposedPort(Port port)
{
exposedPorts.Add(port);
return port;
}
}
}
@@ -0,0 +1,52 @@
using Utils;
namespace KubernetesWorkflow
{
public class ContainerResources
{
public ContainerResourceSet Requests { get; set; } = new ContainerResourceSet();
public ContainerResourceSet Limits { get; set; } = new ContainerResourceSet();
public override string ToString()
{
return $"requests:{Requests}, limits:{Limits}";
}
}
public class ContainerResourceSet
{
public ContainerResourceSet(int milliCPUs, ByteSize memory)
{
MilliCPUs = milliCPUs;
Memory = memory;
}
public ContainerResourceSet(int milliCPUs)
: this(milliCPUs, new ByteSize(0))
{
}
public ContainerResourceSet(ByteSize memory)
: this(0, memory)
{
}
public ContainerResourceSet()
: this(0)
{
}
public int MilliCPUs { get; }
public ByteSize Memory { get; }
public override string ToString()
{
var result = new List<string>();
if (MilliCPUs == 0) result.Add("cpu: unlimited");
else result.Add($"cpu: {MilliCPUs} milliCPUs");
if (Memory.SizeInBytes == 0) result.Add("memory: unlimited");
else result.Add($"memory: {Memory}");
return string.Join(", ", result);
}
}
}
@@ -0,0 +1,95 @@
using k8s;
using Logging;
namespace KubernetesWorkflow
{
public class CrashWatcher
{
private readonly ILog log;
private readonly KubernetesClientConfiguration config;
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)
{
this.log = log;
this.config = config;
this.k8sNamespace = k8sNamespace;
this.container = container;
cts = new CancellationTokenSource();
}
public void Start(ILogHandler logHandler)
{
if (worker != null) throw new InvalidOperationException();
this.logHandler = logHandler;
cts = new CancellationTokenSource();
worker = Task.Run(Worker);
}
public void Stop()
{
if (worker == null) throw new InvalidOperationException();
cts.Cancel();
worker.Wait();
worker = null;
if (workerException != null) throw new Exception("Exception occurred in CrashWatcher worker thread.", workerException);
}
public bool HasContainerCrashed()
{
using var client = new Kubernetes(config);
return HasContainerBeenRestarted(client, container.Pod.PodInfo.Name);
}
private void Worker()
{
try
{
MonitorContainer(cts.Token);
}
catch (Exception ex)
{
workerException = ex;
}
}
private void MonitorContainer(CancellationToken token)
{
using var client = new Kubernetes(config);
while (!token.IsCancellationRequested)
{
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1));
var pod = container.Pod;
var recipe = container.Recipe;
var podName = pod.PodInfo.Name;
if (HasContainerBeenRestarted(client, podName))
{
DownloadCrashedContainerLogs(client, podName, recipe);
return;
}
}
}
private bool HasContainerBeenRestarted(Kubernetes client, string podName)
{
var podInfo = client.ReadNamespacedPod(podName, k8sNamespace);
return podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
}
private void DownloadCrashedContainerLogs(Kubernetes client, string podName, ContainerRecipe recipe)
{
log.Log("Pod crash detected for " + container.Name);
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipe.Name, previous: true);
logHandler!.Log(stream);
}
}
}
@@ -8,10 +8,9 @@ namespace KubernetesWorkflow
{
Configuration = configuration;
}
public Configuration Configuration { get; }
public string HostAddress { get; private set; } = string.Empty;
public K8sNodeLabel[] AvailableK8sNodes { get; set; } = new K8sNodeLabel[0];
public KubernetesClientConfiguration GetK8sClientConfig()
{
@@ -20,26 +19,12 @@ namespace KubernetesWorkflow
return config;
}
public K8sNodeLabel? GetNodeLabelForLocation(Location location)
{
switch (location)
{
case Location.One:
return K8sNodeIfAvailable(0);
case Location.Two:
return K8sNodeIfAvailable(1);
case Location.Three:
return K8sNodeIfAvailable(2);
}
return null;
}
public TimeSpan K8sOperationTimeout()
{
return Configuration.OperationTimeout;
}
public TimeSpan WaitForK8sServiceDelay()
public TimeSpan K8sOperationRetryDelay()
{
return Configuration.RetryDelay;
}
@@ -68,12 +53,6 @@ namespace KubernetesWorkflow
HostAddress = config.Host;
}
}
private K8sNodeLabel? K8sNodeIfAvailable(int index)
{
if (AvailableK8sNodes.Length <= index) return null;
return AvailableK8sNodes[index];
}
}
public class K8sNodeLabel
@@ -7,41 +7,56 @@ namespace KubernetesWorkflow
{
public class K8sController
{
private readonly BaseLog log;
private readonly ILog log;
private readonly K8sCluster cluster;
private readonly KnownK8sPods knownPods;
private readonly WorkflowNumberSource workflowNumberSource;
private readonly K8sClient client;
private const string podLabelKey = "pod-uuid";
public K8sController(BaseLog log, K8sCluster cluster, KnownK8sPods knownPods, WorkflowNumberSource workflowNumberSource, string testNamespace)
public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
{
this.log = log;
this.cluster = cluster;
this.knownPods = knownPods;
this.workflowNumberSource = workflowNumberSource;
client = new K8sClient(cluster.GetK8sClientConfig());
K8sTestNamespace = cluster.Configuration.K8sNamespacePrefix + testNamespace;
K8sNamespace = k8sNamespace;
}
public void Dispose()
{
client.Dispose();
}
public RunningPod BringOnline(ContainerRecipe[] containerRecipes, Location location)
public RunningPod BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
{
log.Debug();
DiscoverK8sNodes();
EnsureTestNamespace();
EnsureNamespace();
var deploymentName = CreateDeployment(containerRecipes, location);
var podLabel = K8sNameUtils.Format(Guid.NewGuid().ToString());
var deploymentName = CreateDeployment(containerRecipes, location, podLabel);
var (serviceName, servicePortsMap) = CreateService(containerRecipes);
var podInfo = FetchNewPod();
var pod = FindPodByLabel(podLabel);
var podInfo = CreatePodInfo(pod);
return new RunningPod(cluster, podInfo, deploymentName, serviceName, servicePortsMap.ToArray());
}
private V1Pod FindPodByLabel(string podLabel)
{
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.");
}
public void Stop(RunningPod pod)
{
log.Debug();
@@ -51,10 +66,10 @@ namespace KubernetesWorkflow
WaitUntilPodOffline(pod.PodInfo.Name);
}
public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler)
public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler, int? tailLines)
{
log.Debug();
using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.PodInfo.Name, K8sTestNamespace, recipe.Name));
using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.PodInfo.Name, K8sNamespace, recipe.Name, tailLines: tailLines));
logHandler.Log(stream);
}
@@ -63,7 +78,7 @@ namespace KubernetesWorkflow
var cmdAndArgs = $"{containerName}: {command} ({string.Join(",", args)})";
log.Debug(cmdAndArgs);
var runner = new CommandRunner(client, K8sTestNamespace, pod, containerName, command, args);
var runner = new CommandRunner(client, K8sNamespace, pod, containerName, command, args);
runner.Run();
var result = runner.GetStdOut();
@@ -71,31 +86,26 @@ namespace KubernetesWorkflow
return result;
}
public void DeleteAllResources()
public void DeleteAllNamespacesStartingWith(string prefix)
{
log.Debug();
var all = client.Run(c => c.ListNamespace().Items);
var namespaces = all.Select(n => n.Name()).Where(n => n.StartsWith(cluster.Configuration.K8sNamespacePrefix));
var namespaces = all.Select(n => n.Name()).Where(n => n.StartsWith(prefix));
foreach (var ns in namespaces)
{
DeleteNamespace(ns);
}
foreach (var ns in namespaces)
{
WaitUntilNamespaceDeleted(ns);
}
}
public void DeleteTestNamespace()
public void DeleteNamespace()
{
log.Debug();
if (IsTestNamespaceOnline())
if (IsNamespaceOnline(K8sNamespace))
{
client.Run(c => c.DeleteNamespace(K8sTestNamespace, null, null, gracePeriodSeconds: 0));
client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0));
}
WaitUntilNamespaceDeleted();
}
public void DeleteNamespace(string ns)
@@ -109,19 +119,7 @@ namespace KubernetesWorkflow
#region Discover K8s Nodes
private void DiscoverK8sNodes()
{
if (cluster.AvailableK8sNodes == null || !cluster.AvailableK8sNodes.Any())
{
cluster.AvailableK8sNodes = GetAvailableK8sNodes();
if (cluster.AvailableK8sNodes.Length < 3)
{
log.Debug($"Warning: For full location support, at least 3 Kubernetes Nodes are required in the cluster. Nodes found: '{string.Join(",", cluster.AvailableK8sNodes.Select(p => $"{p.Key}={p.Value}"))}'.");
}
}
}
private K8sNodeLabel[] GetAvailableK8sNodes()
public K8sNodeLabel[] GetAvailableK8sNodes()
{
var nodes = client.Run(c => c.ListNode());
@@ -145,19 +143,19 @@ namespace KubernetesWorkflow
#region Namespace management
private string K8sTestNamespace { get; }
private string K8sNamespace { get; }
private void EnsureTestNamespace()
private void EnsureNamespace()
{
if (IsTestNamespaceOnline()) return;
if (IsNamespaceOnline(K8sNamespace)) return;
var namespaceSpec = new V1Namespace
{
ApiVersion = "v1",
Metadata = new V1ObjectMeta
{
Name = K8sTestNamespace,
Labels = new Dictionary<string, string> { { "name", K8sTestNamespace } }
Name = K8sNamespace,
Labels = new Dictionary<string, string> { { "name", K8sNamespace } }
}
};
client.Run(c => c.CreateNamespace(namespaceSpec));
@@ -166,11 +164,6 @@ namespace KubernetesWorkflow
CreatePolicy();
}
private bool IsTestNamespaceOnline()
{
return IsNamespaceOnline(K8sTestNamespace);
}
private bool IsNamespaceOnline(string name)
{
return client.Run(c => c.ListNamespace().Items.Any(n => n.Metadata.Name == name));
@@ -185,7 +178,7 @@ namespace KubernetesWorkflow
Metadata = new V1ObjectMeta
{
Name = "isolate-policy",
NamespaceProperty = K8sTestNamespace
NamespaceProperty = K8sNamespace
},
Spec = new V1NetworkPolicySpec
{
@@ -219,6 +212,19 @@ namespace KubernetesWorkflow
}
}
}
},
new V1NetworkPolicyIngressRule
{
FromProperty = new List<V1NetworkPolicyPeer>
{
new V1NetworkPolicyPeer
{
NamespaceSelector = new V1LabelSelector
{
MatchLabels = GetPrometheusNamespaceSelector()
}
}
}
}
},
Egress = new List<V1NetworkPolicyEgressRule>
@@ -272,7 +278,7 @@ namespace KubernetesWorkflow
{
IpBlock = new V1IPBlock
{
Cidr = "0.0.0.0/0"
Cidr = "0.0.0.0/0"
}
}
},
@@ -301,7 +307,7 @@ namespace KubernetesWorkflow
}
};
c.CreateNamespacedNetworkPolicy(body, K8sTestNamespace);
c.CreateNamespacedNetworkPolicy(body, K8sNamespace);
});
}
@@ -309,35 +315,37 @@ namespace KubernetesWorkflow
#region Deployment management
private string CreateDeployment(ContainerRecipe[] containerRecipes, Location location)
private string CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location, string podLabel)
{
var deploymentSpec = new V1Deployment
{
ApiVersion = "apps/v1",
Metadata = CreateDeploymentMetadata(),
Metadata = CreateDeploymentMetadata(containerRecipes),
Spec = new V1DeploymentSpec
{
Replicas = 1,
Selector = new V1LabelSelector
{
MatchLabels = GetSelector()
MatchLabels = GetSelector(containerRecipes)
},
Template = new V1PodTemplateSpec
{
Metadata = new V1ObjectMeta
{
Labels = GetSelector()
Labels = GetSelector(containerRecipes, podLabel),
Annotations = GetAnnotations(containerRecipes)
},
Spec = new V1PodSpec
{
NodeSelector = CreateNodeSelector(location),
Containers = CreateDeploymentContainers(containerRecipes)
Containers = CreateDeploymentContainers(containerRecipes),
Volumes = CreateVolumes(containerRecipes)
}
}
}
};
client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sTestNamespace));
client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sNamespace));
WaitUntilDeploymentOnline(deploymentSpec.Metadata.Name);
return deploymentSpec.Metadata.Name;
@@ -345,13 +353,13 @@ namespace KubernetesWorkflow
private void DeleteDeployment(string deploymentName)
{
client.Run(c => c.DeleteNamespacedDeployment(deploymentName, K8sTestNamespace));
client.Run(c => c.DeleteNamespacedDeployment(deploymentName, K8sNamespace));
WaitUntilDeploymentOffline(deploymentName);
}
private IDictionary<string, string> CreateNodeSelector(Location location)
private IDictionary<string, string> CreateNodeSelector(ILocation location)
{
var nodeLabel = cluster.GetNodeLabelForLocation(location);
var nodeLabel = GetNodeLabelForLocation(location);
if (nodeLabel == null) return new Dictionary<string, string>();
return new Dictionary<string, string>
@@ -360,9 +368,22 @@ namespace KubernetesWorkflow
};
}
private IDictionary<string, string> GetSelector()
private K8sNodeLabel? GetNodeLabelForLocation(ILocation location)
{
return new Dictionary<string, string> { { "codex-test-node", "dist-test-" + workflowNumberSource.WorkflowNumber } };
var l = (Location)location;
return l.NodeLabel;
}
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes)
{
return containerRecipes.First().PodLabels.GetLabels();
}
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes, string podLabel)
{
var labels = containerRecipes.First().PodLabels.Clone();
labels.Add(podLabelKey, podLabel);
return labels.GetLabels();
}
private IDictionary<string, string> GetRunnerNamespaceSelector()
@@ -370,19 +391,30 @@ namespace KubernetesWorkflow
return new Dictionary<string, string> { { "kubernetes.io/metadata.name", "default" } };
}
private V1ObjectMeta CreateDeploymentMetadata()
private IDictionary<string, string> GetPrometheusNamespaceSelector()
{
return new Dictionary<string, string> { { "kubernetes.io/metadata.name", "monitoring" } };
}
private IDictionary<string, string> GetAnnotations(ContainerRecipe[] containerRecipes)
{
return containerRecipes.First().PodAnnotations.GetAnnotations();
}
private V1ObjectMeta CreateDeploymentMetadata(ContainerRecipe[] containerRecipes)
{
return new V1ObjectMeta
{
Name = "deploy-" + workflowNumberSource.WorkflowNumber,
NamespaceProperty = K8sTestNamespace,
Labels = GetSelector()
Name = string.Join('-',containerRecipes.Select(r => r.Name)),
NamespaceProperty = K8sNamespace,
Labels = GetSelector(containerRecipes),
Annotations = GetAnnotations(containerRecipes)
};
}
private List<V1Container> CreateDeploymentContainers(ContainerRecipe[] containerRecipes)
{
return containerRecipes.Select(r => CreateDeploymentContainer(r)).ToList();
return containerRecipes.Select(CreateDeploymentContainer).ToList();
}
private V1Container CreateDeploymentContainer(ContainerRecipe recipe)
@@ -393,7 +425,136 @@ namespace KubernetesWorkflow
Image = recipe.Image,
ImagePullPolicy = "Always",
Ports = CreateContainerPorts(recipe),
Env = CreateEnv(recipe)
Env = CreateEnv(recipe),
VolumeMounts = CreateContainerVolumeMounts(recipe),
Resources = CreateResourceLimits(recipe)
};
}
private V1ResourceRequirements CreateResourceLimits(ContainerRecipe recipe)
{
return new V1ResourceRequirements
{
Requests = CreateResourceQuantities(recipe.Resources.Requests),
Limits = CreateResourceQuantities(recipe.Resources.Limits)
};
}
private Dictionary<string, ResourceQuantity> CreateResourceQuantities(ContainerResourceSet set)
{
var result = new Dictionary<string, ResourceQuantity>();
if (set.MilliCPUs != 0)
{
result.Add("cpu", new ResourceQuantity($"{set.MilliCPUs}m"));
}
if (set.Memory.SizeInBytes != 0)
{
result.Add("memory", new ResourceQuantity(set.Memory.ToSuffixNotation()));
}
return result;
}
private List<V1VolumeMount> CreateContainerVolumeMounts(ContainerRecipe recipe)
{
return recipe.Volumes.Select(CreateContainerVolumeMount).ToList();
}
private V1VolumeMount CreateContainerVolumeMount(VolumeMount v)
{
return new V1VolumeMount
{
Name = v.VolumeName,
MountPath = v.MountPath,
SubPath = v.SubPath,
};
}
private List<V1Volume> CreateVolumes(ContainerRecipe[] containerRecipes)
{
return containerRecipes.Where(c => c.Volumes.Any()).SelectMany(CreateVolumes).ToList();
}
private List<V1Volume> CreateVolumes(ContainerRecipe recipe)
{
return recipe.Volumes.Select(CreateVolume).ToList();
}
private V1Volume CreateVolume(VolumeMount v)
{
CreatePersistentVolumeClaimIfNeeded(v);
if (!string.IsNullOrEmpty(v.HostPath))
{
return new V1Volume
{
Name = v.VolumeName,
HostPath = new V1HostPathVolumeSource
{
Path = v.HostPath
}
};
}
if (!string.IsNullOrEmpty(v.Secret))
{
return new V1Volume
{
Name = v.VolumeName,
Secret = CreateVolumeSecret(v)
};
}
return new V1Volume
{
Name = v.VolumeName,
PersistentVolumeClaim = new V1PersistentVolumeClaimVolumeSource
{
ClaimName = v.VolumeName
}
};
}
private void CreatePersistentVolumeClaimIfNeeded(VolumeMount v)
{
var pvcs = client.Run(c => c.ListNamespacedPersistentVolumeClaim(K8sNamespace));
if (pvcs != null && pvcs.Items.Any(i => i.Name() == v.VolumeName)) return;
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
{
ApiVersion = "v1",
Metadata = new V1ObjectMeta
{
Name = v.VolumeName,
},
Spec = new V1PersistentVolumeClaimSpec
{
AccessModes = new List<string>
{
"ReadWriteOnce"
},
Resources = CreateVolumeResourceRequirements(v),
},
}, K8sNamespace));
}
private V1SecretVolumeSource CreateVolumeSecret(VolumeMount v)
{
if (string.IsNullOrWhiteSpace(v.Secret)) return null!;
return new V1SecretVolumeSource
{
SecretName = v.Secret
};
}
private V1ResourceRequirements CreateVolumeResourceRequirements(VolumeMount v)
{
if (v.ResourceQuantity == null) return null!;
return new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>()
{
{"storage", new ResourceQuantity(v.ResourceQuantity) }
}
};
}
@@ -413,23 +574,42 @@ namespace KubernetesWorkflow
private List<V1ContainerPort> CreateContainerPorts(ContainerRecipe recipe)
{
var exposedPorts = recipe.ExposedPorts.Select(p => CreateContainerPort(recipe, p));
var internalPorts = recipe.InternalPorts.Select(p => CreateContainerPort(recipe, p));
var exposedPorts = recipe.ExposedPorts.SelectMany(p => CreateContainerPort(recipe, p));
var internalPorts = recipe.InternalPorts.SelectMany(p => CreateContainerPort(recipe, p));
return exposedPorts.Concat(internalPorts).ToList();
}
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port)
private List<V1ContainerPort> CreateContainerPort(ContainerRecipe recipe, Port port)
{
var result = new List<V1ContainerPort>();
if (port.IsTcp()) CreateTcpContainerPort(result, recipe, port);
if (port.IsUdp()) CreateUdpContainerPort(result, recipe, port);
return result;
}
private void CreateUdpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
{
result.Add(CreateContainerPort(recipe, port, "UDP"));
}
private void CreateTcpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
{
result.Add(CreateContainerPort(recipe, port, "TCP"));
}
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port, string protocol)
{
return new V1ContainerPort
{
Name = GetNameForPort(recipe, port),
ContainerPort = port.Number
ContainerPort = port.Number,
Protocol = protocol
};
}
private string GetNameForPort(ContainerRecipe recipe, Port port)
{
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}";
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}-{port.Protocol.ToString().ToLowerInvariant()}";
}
#endregion
@@ -456,12 +636,12 @@ namespace KubernetesWorkflow
Spec = new V1ServiceSpec
{
Type = "NodePort",
Selector = GetSelector(),
Selector = GetSelector(containerRecipes),
Ports = ports
}
};
client.Run(c => c.CreateNamespacedService(serviceSpec, K8sTestNamespace));
client.Run(c => c.CreateNamespacedService(serviceSpec, K8sNamespace));
ReadBackServiceAndMapPorts(serviceSpec, containerRecipes, result);
@@ -471,19 +651,18 @@ namespace KubernetesWorkflow
private void ReadBackServiceAndMapPorts(V1Service serviceSpec, ContainerRecipe[] containerRecipes, List<ContainerRecipePortMapEntry> result)
{
// For each container-recipe, we need to figure out which service-ports it was assigned by K8s.
var readback = client.Run(c => c.ReadNamespacedService(serviceSpec.Metadata.Name, K8sTestNamespace));
var readback = client.Run(c => c.ReadNamespacedService(serviceSpec.Metadata.Name, K8sNamespace));
foreach (var r in containerRecipes)
{
if (r.ExposedPorts.Any())
foreach (var port in r.ExposedPorts)
{
var firstExposedPort = r.ExposedPorts.First();
var portName = GetNameForPort(r, firstExposedPort);
var portName = GetNameForPort(r, port);
var matchingServicePorts = readback.Spec.Ports.Where(p => p.Name == portName);
if (matchingServicePorts.Any())
{
// These service ports belongs to this recipe.
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, portName));
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, port.Tag, port.Protocol));
var ports = optionals.Where(p => p != null).Select(p => p!).ToArray();
result.Add(new ContainerRecipePortMapEntry(r.Number, ports));
@@ -492,15 +671,15 @@ namespace KubernetesWorkflow
}
}
private Port? MapNodePortIfAble(V1ServicePort p, string tag)
private Port? MapNodePortIfAble(V1ServicePort p, string tag, PortProtocol protocol)
{
if (p.NodePort == null) return null;
return new Port(p.NodePort.Value, tag);
return new Port(p.NodePort.Value, tag, protocol);
}
private void DeleteService(string serviceName)
{
client.Run(c => c.DeleteNamespacedService(serviceName, K8sTestNamespace));
client.Run(c => c.DeleteNamespacedService(serviceName, K8sNamespace));
}
private V1ObjectMeta CreateServiceMetadata()
@@ -508,7 +687,7 @@ namespace KubernetesWorkflow
return new V1ObjectMeta
{
Name = "service-" + workflowNumberSource.WorkflowNumber,
NamespaceProperty = K8sTestNamespace
NamespaceProperty = K8sNamespace
};
}
@@ -527,42 +706,37 @@ namespace KubernetesWorkflow
var result = new List<V1ServicePort>();
foreach (var port in recipe.ExposedPorts)
{
result.Add(new V1ServicePort
{
Name = GetNameForPort(recipe, port),
Protocol = "TCP",
Port = port.Number,
TargetPort = GetNameForPort(recipe, port),
});
if (port.IsTcp()) CreateServicePort(result, recipe, port, "TCP");
if (port.IsUdp()) CreateServicePort(result, recipe, port, "UDP");
}
return result;
}
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol)
{
result.Add(new V1ServicePort
{
Name = GetNameForPort(recipe, port),
Protocol = protocol,
Port = port.Number,
TargetPort = GetNameForPort(recipe, port),
});
}
#endregion
#region Waiting
private void WaitUntilNamespaceCreated()
{
WaitUntil(() => IsTestNamespaceOnline());
}
private void WaitUntilNamespaceDeleted()
{
WaitUntil(() => !IsTestNamespaceOnline());
}
private void WaitUntilNamespaceDeleted(string name)
{
WaitUntil(() => !IsNamespaceOnline(name));
WaitUntil(() => IsNamespaceOnline(K8sNamespace));
}
private void WaitUntilDeploymentOnline(string deploymentName)
{
WaitUntil(() =>
{
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sTestNamespace));
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace));
return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0;
});
}
@@ -571,7 +745,7 @@ namespace KubernetesWorkflow
{
WaitUntil(() =>
{
var deployments = client.Run(c => c.ListNamespacedDeployment(K8sTestNamespace));
var deployments = client.Run(c => c.ListNamespacedDeployment(K8sNamespace));
var deployment = deployments.Items.SingleOrDefault(d => d.Metadata.Name == deploymentName);
return deployment == null || deployment.Status.AvailableReplicas == 0;
});
@@ -581,7 +755,7 @@ namespace KubernetesWorkflow
{
WaitUntil(() =>
{
var pods = client.Run(c => c.ListNamespacedPod(K8sTestNamespace)).Items;
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace)).Items;
var pod = pods.SingleOrDefault(p => p.Metadata.Name == podName);
return pod == null;
});
@@ -592,7 +766,7 @@ namespace KubernetesWorkflow
var sw = Stopwatch.Begin(log, true);
try
{
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.WaitForK8sServiceDelay());
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay());
}
finally
{
@@ -602,22 +776,20 @@ namespace KubernetesWorkflow
#endregion
private PodInfo FetchNewPod()
public CrashWatcher CreateCrashWatcher(RunningContainer container)
{
var pods = client.Run(c => c.ListNamespacedPod(K8sTestNamespace)).Items;
return new CrashWatcher(log, cluster.GetK8sClientConfig(), K8sNamespace, container);
}
var newPods = pods.Where(p => !knownPods.Contains(p.Name())).ToArray();
if (newPods.Length != 1) throw new InvalidOperationException("Expected only 1 pod to be created. Test infra failure.");
var newPod = newPods.Single();
var name = newPod.Name();
var ip = newPod.Status.PodIP;
var k8sNodeName = newPod.Spec.NodeName;
private PodInfo CreatePodInfo(V1Pod pod)
{
var name = pod.Name();
var ip = pod.Status.PodIP;
var k8sNodeName = pod.Spec.NodeName;
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure.");
if (string.IsNullOrEmpty(ip)) throw new InvalidOperationException("Invalid pod IP received. Test infra failure.");
knownPods.Add(name);
return new PodInfo(name, ip, k8sNodeName);
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace KubernetesWorkflow
{
public interface IK8sHooks
{
void OnContainersStarted(RunningContainers runningContainers);
void OnContainersStopped(RunningContainers runningContainers);
void OnContainerRecipeCreated(ContainerRecipe recipe);
}
public class DoNothingK8sHooks : IK8sHooks
{
public void OnContainersStarted(RunningContainers runningContainers)
{
}
public void OnContainersStopped(RunningContainers runningContainers)
{
}
public void OnContainerRecipeCreated(ContainerRecipe recipe)
{
}
}
}
@@ -0,0 +1,22 @@
namespace KubernetesWorkflow
{
public static class K8sNameUtils
{
public static string Format(string s)
{
var result = s.ToLowerInvariant()
.Replace(" ", "-")
.Replace(":", "-")
.Replace("/", "-")
.Replace("\\", "-")
.Replace("[", "-")
.Replace("]", "-")
.Replace(",", "-");
result = result.Trim('-');
if (result.Length > 62) result = result.Substring(0, 62);
return result;
}
}
}
@@ -0,0 +1,47 @@
namespace KubernetesWorkflow
{
public interface IKnownLocations
{
/// <summary>
/// Returns a known location given an index.
/// Each index guarantees a different location.
/// </summary>
ILocation Get(int index);
int NumberOfLocations { get; }
/// <summary>
/// Returns the location object for a specific kubernetes node. Throws if it doesn't exist.
/// </summary>
ILocation Get(string kubeNodeName, bool allowPartialMatches = false);
}
public class KnownLocations : IKnownLocations
{
private readonly Location[] locations;
public KnownLocations(Location[] locations)
{
this.locations = locations;
if (locations.Any(l => l.NodeLabel == null)) throw new Exception("Must not contain unspecified location");
}
public static ILocation UnspecifiedLocation { get; } = new Location();
public int NumberOfLocations => locations.Length;
public ILocation Get(int index)
{
return locations[index];
}
public ILocation Get(string kubeNodeName, bool allowPartialMatches = false)
{
if (allowPartialMatches)
{
return locations.Single(l => l.NodeLabel != null && l.NodeLabel.Value.Contains(kubeNodeName));
}
return locations.Single(l => l.NodeLabel != null && l.NodeLabel.Value == kubeNodeName);
}
}
}
@@ -9,6 +9,7 @@
<ItemGroup>
<PackageReference Include="KubernetesClient" Version="10.1.4" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
+22
View File
@@ -0,0 +1,22 @@
namespace KubernetesWorkflow
{
public interface ILocation
{
}
public class Location : ILocation
{
internal Location(K8sNodeLabel? nodeLabel = null)
{
NodeLabel = nodeLabel;
}
internal K8sNodeLabel? NodeLabel { get; }
public override string ToString()
{
if (NodeLabel == null) return "Location:Unspecified";
return $"Location:KubeNode-'{NodeLabel.Key}:{NodeLabel.Value}'";
}
}
}
@@ -0,0 +1,49 @@
using Logging;
namespace KubernetesWorkflow
{
public class LocationProvider
{
private readonly TimeSpan locationsExpirationTime = TimeSpan.FromMinutes(10);
private readonly ILog log;
private readonly Action<Action<K8sController>> onController;
private Location[] knownLocations = Array.Empty<Location>();
private DateTime lastUpdate = DateTime.UtcNow;
public LocationProvider(ILog log, Action<Action<K8sController>> onController)
{
this.log = log;
this.onController = onController;
}
public IKnownLocations GetAvailableLocations()
{
if (ShouldUpdateKnownLocations())
{
onController(UpdateKnownLocations);
}
return new KnownLocations(knownLocations);
}
private void UpdateKnownLocations(K8sController controller)
{
knownLocations = controller.GetAvailableK8sNodes().Select(CreateLocation).ToArray();
lastUpdate = DateTime.UtcNow;
log.Log($"Detected {knownLocations.Length} available locations: '{string.Join(",", knownLocations.Select(l => l.ToString()))}'");
}
private Location CreateLocation(K8sNodeLabel k8sNode)
{
return new Location(k8sNode);
}
private bool ShouldUpdateKnownLocations()
{
if (!knownLocations.Any()) return true;
if (DateTime.UtcNow - lastUpdate > locationsExpirationTime) return true;
return false;
}
}
}
@@ -0,0 +1,29 @@
namespace KubernetesWorkflow
{
public class PodAnnotations
{
private readonly Dictionary<string, string> annotations = new Dictionary<string, string>();
public void Add(string key, string value)
{
annotations.Add(key, value);
}
public PodAnnotations Clone()
{
var result = new PodAnnotations();
foreach (var entry in annotations) result.Add(entry.Key, entry.Value);
return result;
}
public void Clear()
{
annotations.Clear();
}
internal Dictionary<string, string> GetAnnotations()
{
return annotations;
}
}
}
+29
View File
@@ -0,0 +1,29 @@
namespace KubernetesWorkflow
{
public class PodLabels
{
private readonly Dictionary<string, string> labels = new Dictionary<string, string>();
public void Add(string key, string value)
{
labels.Add(key, K8sNameUtils.Format(value));
}
public PodLabels Clone()
{
var result = new PodLabels();
foreach (var entry in labels) result.Add(entry.Key, entry.Value);
return result;
}
public void Clear()
{
labels.Clear();
}
internal Dictionary<string, string> GetLabels()
{
return labels;
}
}
}
@@ -7,9 +7,14 @@ namespace KubernetesWorkflow
{
private NumberSource portNumberSource = new NumberSource(8080);
public Port CreatePort(string tag)
public Port CreatePort(int number, string tag, PortProtocol protocol)
{
return new Port(portNumberSource.GetNextNumber(), tag);
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)
@@ -0,0 +1,63 @@
using System.Net.NetworkInformation;
using Utils;
namespace KubernetesWorkflow
{
internal enum RunnerLocation
{
ExternalToCluster,
InternalToCluster,
}
internal static class RunnerLocationUtils
{
private static RunnerLocation? knownLocation = null;
internal static RunnerLocation DetermineRunnerLocation(RunningContainer container)
{
if (knownLocation != null) return knownLocation.Value;
knownLocation = PingForLocation(container);
return knownLocation.Value;
}
private static RunnerLocation PingForLocation(RunningContainer container)
{
if (PingHost(container.Pod.PodInfo.Ip))
{
return RunnerLocation.InternalToCluster;
}
foreach (var port in container.ContainerPorts)
{
if (port.ExternalAddress.IsValid() && PingHost(Format(port.ExternalAddress)))
{
return RunnerLocation.ExternalToCluster;
}
}
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;
}
}
}

Some files were not shown because too many files have changed in this diff Show More