Compare commits

...
Author SHA1 Message Date
benbierens 4be9b9df9a Finished! 2024-06-21 10:19:09 +02:00
benbierens a905f0ce53 fixes crash in market buffer again 2024-06-21 09:59:24 +02:00
benbierens 0c4d3be912 Fixes crash in marketBuffer 2024-06-21 09:33:52 +02:00
benbierens 2b7ba61543 Cleanup 2024-06-21 09:18:00 +02:00
benbierens f9c7e18985 Merge branch 'master' into chainstate-update 2024-06-21 08:56:43 +02:00
benbierens 32e7028029 wip 2024-06-21 08:56:20 +02:00
Ben cc3eddf02d wip 2024-06-20 12:15:59 +02:00
Ben d73ad6db15 Merge branch 'feature/keymaker' 2024-06-19 14:06:20 +02:00
Ben 5280f0732c Sets up docker image for creating keys 2024-06-19 14:04:27 +02:00
Ben 5ca135646a Container crash detection during start-up 2024-06-19 10:39:14 +02:00
Ben e1da38b6e8 Adds test for making sure node does not crash after mysterious CID is downloaded 2024-06-18 11:04:04 +02:00
Ben f7c45d17d7 wip 2024-06-18 09:57:38 +02:00
Ben cb4cdfe69a better logging 2024-06-17 16:00:51 +02:00
Ben bed57dd35b Finish replace of old chainstate object in rewarder bot 2024-06-17 15:34:08 +02:00
benbierens c38a2242ba wip 2024-06-14 11:09:51 +02:00
benbierens d3488dc907 Adds block numbers to chainstate event logging 2024-06-14 10:22:30 +02:00
benbierens 1fa7787d3b Merge branch 'master' into chainstate-update
# Conflicts:
#	ProjectPlugins/CodexPlugin/CodexNode.cs
#	ProjectPlugins/CodexPlugin/MarketplaceAccess.cs
2024-06-14 09:28:34 +02:00
benbierens 54f053cfcc Improves crash detection 2024-06-14 09:05:56 +02:00
Ben 7b5e802efb Applies delete in scalability tests 2024-06-13 08:51:52 +02:00
Ben 390b9de54a Replaces delete with disk info log. 2024-06-12 15:28:08 +02:00
Ben 4fc9835f43 Attempt to log disk space before and after uploads and downloads 2024-06-12 10:48:52 +02:00
Ben 9a46e20b4a Fixes exception in CodexDistTests 2024-06-11 11:33:02 +02:00
Ben 74ca512f92 Sets up test for downloading content by contract CID 2024-06-10 15:15:27 +02:00
Ben 1c856f7615 Fixes log 2024-06-10 14:17:19 +02:00
Ben cc2513bd2f Better chain state representation 2024-06-10 14:04:25 +02:00
Ben 22527a5d93 Deletes block repostore folder in container when Codex node stops. 2024-06-10 10:58:50 +02:00
benbierens 25663b59a0 sets trace level for scalability tests 2024-06-08 10:50:30 +02:00
benbierens aa416d50b3 ensuring enough mounted disk space 2024-06-08 10:36:23 +02:00
benbierens 16bf5ce5ab I think I may be on to something 2024-06-07 18:16:19 +02:00
benbierens 52ae671234 focussing on the interesting numbers for now 2024-06-07 17:08:14 +02:00
benbierens 5ffff1ed07 Disables metrics in scalability test. Downloads container log when retry attempt failed too quickly to be a timeout. 2024-06-07 17:07:35 +02:00
benbierens 18a02b1717 Implements mapping of debug/info routing table nodes 2024-06-07 16:37:31 +02:00
benbierens 3a61fc89c6 Adds WaitForCleanup test attribute to allow tests to wait for resources to be cleaned up 2024-06-06 15:09:52 +02:00
Ben 38c2d1749a Links up status method for checking. 2024-06-06 10:31:31 +02:00
Ben 9900db8d25 simply scalability test failure handling 2024-06-06 10:12:11 +02:00
Ben e53a644718 Updates to new codex image 2024-06-06 10:11:51 +02:00
Ben 62b56e198b Adds logging of node status to base codexdistTest 2024-06-06 09:54:50 +02:00
Ben f4d1dae478 Disables large network scalability test for now 2024-06-05 10:00:50 +02:00
Ben bf75c25152 Cannot assume retry uploads will require as much space as file is large 2024-06-05 09:59:03 +02:00
Ben b888999b61 Adds space check for upload call 2024-06-05 09:34:43 +02:00
Ben e9555cf99e Upload/download failure now automatically investigates node 2024-06-05 09:20:00 +02:00
Ben 11b866986d wip retry upgrade 2024-06-04 11:06:19 +02:00
benbierens 67fc2183b7 Better logging for slot-filled event checking 2024-06-03 12:56:19 +02:00
benbierens 02baa72c1e DebugInfo log 2024-06-03 11:42:52 +02:00
benbierens 383102b988 Adds container name to crash messge. 2024-06-03 11:36:48 +02:00
benbierens 3da36725e3 Updates codex image 2024-06-03 10:58:04 +02:00
benbierens ac7d323201 wip 2024-06-03 10:38:48 +02:00
benbierens a846d51c0c Implements ChainState in CodexContracts plugin 2024-05-31 11:19:50 +02:00
benbierens f5da80dc9c Sets up ChainEvents object 2024-05-31 10:16:57 +02:00
benbierens f94a67adb4 Extracts event methods from CodexContractsAccess 2024-05-31 10:06:34 +02:00
Ben f1d453251c cleanup 2024-05-30 11:33:16 +02:00
Ben 12dc7efd5b wip 2024-05-30 11:17:13 +02:00
Ben 117a30bb82 wip 2024-05-30 10:55:33 +02:00
Ben ccc6c815e4 Logging of entire chain state as seen by rewarder bot 2024-05-29 14:05:16 +02:00
benbierens ba9e4b098f checking that all rewards are sent. 2024-05-24 16:11:51 +02:00
benbierens 69aa3a998f Fixes bot test contract 2024-05-24 15:34:42 +02:00
benbierens 00fd2cebf9 Merge branch 'master' into feature/rewardbot-tests 2024-05-24 14:34:16 +02:00
benbierens 684a99027b nice checks for timings of successful contract 2024-05-24 14:33:59 +02:00
Ben 5143361dcd wip 2024-05-23 11:37:57 +02:00
Ben 6c956c1a64 wip 2024-05-23 11:37:17 +02:00
Ben a749f82ca5 Switches blocktime logs to debug 2024-05-23 11:24:32 +02:00
Ben 6e9ea47b7d Tracking contract state change moments in MarketplaceAccess 2024-05-23 11:15:14 +02:00
Ben b1818400ca Merge branch 'master' into feature/rewardbot-tests
# Conflicts:
#	Tests/CodexTests/UtilityTests/DiscordBotTests.cs
2024-05-22 11:11:47 +02:00
Ben f7bdafbdc5 Improved representation of TestToken amounts 2024-05-22 11:06:34 +02:00
Ben a236544ee9 parameterizing the bot test 2024-05-21 16:10:14 +02:00
Ben e082f8a31c Changes Mint-TT bot parameter to BigInt type. 2024-05-21 13:09:46 +02:00
Ben fa1b560a91 Adds waiting for debug-mode start message 2024-05-21 12:58:17 +02:00
benbierens d6f7e225be successful downloading of bot log 2024-05-20 16:18:01 +02:00
Ben 22cf82b99b Adds no-discord debug option to discord bot. 2024-05-16 16:00:19 +02:00
Ben ad2181db0b Fixes invariant-culture in assigner. Adds support for ulong. 2024-05-16 14:58:42 +02:00
Ben 3525d13e69 Merge branch 'feature/bot-use-ids' 2024-05-16 14:07:29 +02:00
Ben 8847de116d cleanup rewarder bot config 2024-05-16 14:07:14 +02:00
Ben 7d9dcb263d Prints default ArgsUniform values. Applies discord IDs instead of names. 2024-05-16 14:05:58 +02:00
Ben b3771cce32 Extract assigner from argsUniform 2024-05-16 12:14:03 +02:00
Ben 9717591224 Updates bot config to use ids instead of names 2024-05-16 11:54:31 +02:00
Ben 64ae7c8efe Latest image of Codex 2024-05-15 10:34:40 +02:00
benbierens d8d7605ce5 Increases marketplace graceperiod timespan. 2024-05-10 10:08:45 +02:00
benbierens 6995cbfb23 hacky way to download container logs while retries are failing. 2024-05-09 09:38:04 +02:00
benbierens c6a757d6fb Rename AddCodex. Better retry logging 2024-05-09 09:32:48 +02:00
benbierens c01f9dbb21 Fixes crash in blocktimefinder. 2024-05-09 08:57:19 +02:00
Ben 0ec43a9325 Shuts up json-serialization codex logging 2024-05-07 11:04:32 +02:00
Ben 266c661958 Updates codex API 2024-05-07 10:21:05 +02:00
Ben 8ea4c4ee37 Clean up codex long tests 2024-05-07 10:15:02 +02:00
Ben 725dfc23a6 Cleanup of codex tests 2024-05-07 09:49:00 +02:00
Ben 39ff757b39 Update + instructions for codex contracts 2024-05-06 16:12:26 +02:00
benbierens e187bfc941 Changes time.retry to fixed timelength instead of fixed number of retries 2024-05-02 08:41:20 +02:00
benbierens dd36929a81 Sets scalability tests to current codex image 2024-05-01 13:51:46 +02:00
Ben 2902f6baab latest master image of Codex 2024-05-01 10:05:46 +02:00
benbierens f38f7861a0 Log comparing test 2024-04-27 18:08:54 +02:00
benbierens 84a68521b0 sets up parser for codex log lines 2024-04-27 14:20:31 +02:00
benbierens ce7ba47a32 trying to detect upload slowdown related to number of blocks in node 2024-04-27 11:33:13 +02:00
Ben a20e9a30cf bumps to image with proof support 2024-04-26 11:10:14 +02:00
Ben 52cec0e9f3 bump codex image 2024-04-26 10:43:36 +02:00
Ben 9a6866cf8e clean up logs in Codex 2024-04-25 15:04:02 +02:00
Ben 15d7e6483e Allows parsing longs only from a certain timestamp 2024-04-25 10:57:19 +02:00
Ben e1c710a093 Merge branch 'less-announce-tests' 2024-04-25 10:25:29 +02:00
Ben 0c700ded9d Test for detecting common log lines 2024-04-25 10:25:12 +02:00
Ben 50b7e2300d sets quota 2024-04-25 09:25:00 +02:00
Ben a6379d02f1 Adds test for upload/download large file with single node 2024-04-24 09:41:05 +02:00
Ben 6545f3469f dumb mistake by me 2024-04-23 13:16:11 +02:00
Ben 58f7f9384a Test for disc speeds 2024-04-23 09:10:10 +02:00
Ben 7ec9934751 Update to multipeer download test 2024-04-22 11:17:47 +02:00
Ben c4c3f61a23 parameterizes tests 2024-04-19 11:52:39 +02:00
Ben c856f404e3 Adds test to show from which hosts blocks are downloaded. 2024-04-19 11:40:32 +02:00
benbierens eed989cbf5 Handle errors during log download 2024-04-15 11:37:14 +02:00
benbierens 630dc2814a disable new test 2024-04-15 08:13:43 +02:00
benbierens 570b174a00 Adds everyone-test-a-file test 2024-04-15 08:12:57 +02:00
benbierens 700fc0ea40 Sets quota for codex nodes. Sets loglevel for bootstrap node. 2024-04-15 07:57:13 +02:00
benbierens 23ebd4166b Disables downloading logs 2024-04-15 07:36:12 +02:00
benbierens 3683044bf7 Dont download container logs 2024-04-14 11:17:59 +02:00
benbierens fb10906816 removes 20node value 2024-04-14 10:43:06 +02:00
benbierens 86074dab6a Bump k8s operation timeout for long timeset 2024-04-14 09:29:13 +02:00
benbierens 015d8da21d Better logging for Time.WaitUntil. 2024-04-14 09:22:55 +02:00
benbierens d847c4f3ec Adds names to kube wait functions 2024-04-14 08:56:22 +02:00
gmega a2e4869403 use long timeouts 2024-04-13 17:33:21 +03:00
gmega 5ffe34bb83 stop crash watcher before stopping pod 2024-04-13 17:20:23 +03:00
gmega e3b16fd742 add ability to stop single containers 2024-04-13 17:12:14 +03:00
gmega 80261959e7 set log level to info 2024-04-13 16:36:16 +03:00
gmega 7db9360ba4 add network/file scalability test 2024-04-13 16:27:47 +03:00
benbierens 899d775873 Adds finished requests 2024-04-13 11:15:39 +02:00
benbierens 581cc80d5d Implements getting blockTimeEntries 2024-04-13 09:19:20 +02:00
benbierens 24cf6c70b8 Improve event logging 2024-04-13 08:57:46 +02:00
benbierens ae25b58610 Fixes exception in market averages 2024-04-12 09:12:51 +02:00
benbierens 2cf5a26934 Sends updates to discord bot when no rewards are found. 2024-04-12 08:44:14 +02:00
benbierens 5ed78da30b Fixes possible crash in market averages 2024-04-11 17:28:12 +02:00
benbierens 3fb1b212b6 Merge branch 'feature/bot-upgrade' 2024-04-11 13:15:55 +02:00
benbierens ca7258ef28 async dispatches sending of event overview to discord channel 2024-04-11 13:14:21 +02:00
benbierens 1dbb749732 Removes ChangedEvents field from chain-state, not necessary 2024-04-11 08:50:03 +02:00
benbierens 525fdcf4a7 Merge branch 'master' into feature/bot-upgrade 2024-04-11 08:12:02 +02:00
benbierens 40208373e4 Merge branch 'feature/better-start-waiting' 2024-04-11 08:11:47 +02:00
benbierens 4a0885cf2d Check for marketplace contract up-to-date 2024-04-11 07:46:00 +02:00
benbierens 0224f17733 fixes serialization of request state 2024-04-09 13:24:30 +02:00
benbierens b7ab2f994e Fixes market test 2024-04-09 10:57:02 +02:00
benbierens 0182ce134f Applies future container to existing plugins 2024-04-09 10:23:07 +02:00
benbierens 69666d3fee Setting up future-containers 2024-04-09 09:30:45 +02:00
benbierens 605004286a Merge branch 'feature/bot-upgrade' 2024-04-08 16:08:05 +02:00
benbierens bbc975141f Creates events overview in rewarder bot 2024-04-08 16:07:52 +02:00
benbierens 0b2dcef57e Sets up showing of chain events in discord bot 2024-04-08 13:55:39 +02:00
benbierens 9f7e95c515 Applies async/await to getbalance and mint commands 2024-04-08 13:36:55 +02:00
benbierens 8db84bfe7d Merge branch 'feature/bot-upgrade' 2024-04-07 14:06:14 +02:00
benbierens 9c8151bdb7 Adds Market command 2024-04-07 14:04:31 +02:00
benbierens 43aaed225d Updates codex image 2024-04-03 20:40:13 +02:00
Ben b805c5f004 Prevents duplicate addresses to be sent in the same reward request 2024-04-03 09:21:10 +02:00
Ben 29344451d6 workaround for getpurchases api call 2024-04-02 14:53:45 +02:00
Ben 5511f8ed32 Extra logging for discordbot reward API 2024-04-02 13:32:06 +02:00
benbierens 4cd22f3719 Rather embarrasing mistake in rewarderbot interval config param. 2024-04-01 15:55:47 +02:00
benbierens f5291517c1 Merge branch 'feature/bot-upgrade'
# Conflicts:
#	Tests/CodexTests/BasicTests/MarketplaceTests.cs
2024-04-01 15:32:43 +02:00
benbierens ceed913143 moves bot tests 2024-04-01 15:31:54 +02:00
benbierens 3086aa78d5 Uses 3 hosts in marketplace test 2024-04-01 15:22:12 +02:00
benbierens 6c4b9345cb sensible time range for bot test 2024-04-01 14:08:30 +02:00
benbierens 2ab84e2a61 Time handling for rewarder 2024-04-01 13:56:07 +02:00
benbierens 48e7f98956 Adds some logging 2024-04-01 13:46:30 +02:00
benbierens d7c7d47a61 Fixes crash in blocktimefinder 2024-04-01 13:42:07 +02:00
benbierens be268ace42 Find bug in blocktime finder 2024-04-01 13:21:21 +02:00
benbierens a6b0b16909 ready for bot test 2024-04-01 11:22:28 +02:00
benbierens 8c64352acf Merge branch 'master' into feature/bot-upgrade 2024-04-01 11:07:16 +02:00
Ben Bierens fd38cc336b Merge pull request #101 from codex-storage/feat/multi-stage-builds-for-rewarder
Use multi-stage builds for Rewarder
2024-04-01 11:06:52 +02:00
benbierens f5429b9c14 Merge branch 'master' into feature/bot-upgrade
# Conflicts:
#	Tests/CodexTests/BasicTests/ExampleTests.cs
2024-04-01 11:06:06 +02:00
benbierens 1344a6e3d2 Cleanup 2024-04-01 11:01:21 +02:00
benbierens 9a2e889ef9 Fixes convert issue in mapper. Removes CodexPlugin from discord bot dependencies. 2024-04-01 10:11:44 +02:00
Slava a58c9aba59 Use multi-stage builds for BiblioTech 2024-04-01 11:05:17 +03:00
benbierens 0263cc4eff Force rebuild of openapi client code each CodexPlugin build. 2024-04-01 10:04:32 +02:00
benbierens b40215dd36 Updates image and api 2024-04-01 08:58:45 +02:00
benbierens 4e978bd5b5 Restores continuous tests 2024-04-01 08:29:55 +02:00
Slava 0a8083d4d5 Use multi-stage builds for Rewarder 2024-03-31 22:18:48 +03:00
Ben 376fd60974 debugging image 2024-03-29 12:09:01 +01:00
Ben 8bc63c1fdb Configurable testtoken and eth amounts for mint command 2024-03-29 11:24:11 +01:00
Ben 4f461e4cb3 Checking onchain events to debug missing slotFilled event 2024-03-28 13:49:02 +01:00
Ben b25c747522 Ready for test. Need new bot images 2024-03-27 15:49:08 +01:00
Ben 01d6b8f227 Applies blockInterval 2024-03-27 15:39:42 +01:00
Ben 330552563c Merge branch 'master' into feature/bot-upgrade
# Conflicts:
#	Tests/CodexTests/BasicTests/ExampleTests.cs
2024-03-27 15:25:58 +01:00
Ben 581cf2a813 Merge branch 'feature/codex-openapi' 2024-03-27 15:24:40 +01:00
Ben f6edf6cbd5 Overdue readme updates 2024-03-27 15:01:32 +01:00
Ben db6db4d38c Fixes metrics tests 2024-03-27 09:41:12 +01:00
Ben 127b7daecc Improves plugin log initialization 2024-03-27 08:41:18 +01:00
Ben 6597728e5c Makes contract-clock log topic configurable 2024-03-27 08:13:36 +01:00
Ben 9e9773f45d Merge branch 'master' into feature/codex-openapi
# Conflicts:
#	ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs
2024-03-26 16:16:39 +01:00
Ben 9fe97f5d40 Debugs compatibility check. 2024-03-26 16:10:50 +01:00
Ben a53d2de13b Moves ethAccount to Geth plugin. Enables setting ethAccount when Codex starts. 2024-03-26 15:35:26 +01:00
Ben d9d8441b6e compiletime checking of all map calls 2024-03-26 15:12:28 +01:00
Ben e72c1b037c Implements api compatiblity checker 2024-03-26 14:42:47 +01:00
Ben bce9a2c124 Lines up new openAPI.yaml 2024-03-26 14:07:06 +01:00
Ben f57188914e Restores marketplace access 2024-03-26 12:23:38 +01:00
Ben 3bd297a22d restores connect-to-peer 2024-03-26 12:14:02 +01:00
Ben 85774847b7 Restoring marketplace types 2024-03-26 11:39:59 +01:00
Ben 8fcf351613 Restores debug/peer 2024-03-26 10:31:49 +01:00
Ben 05d28d1d0a Restores debug info 2024-03-26 10:03:52 +01:00
Ben a3253b42d8 Introduces endpoint component from old http functionality 2024-03-26 09:10:06 +01:00
Ben b69059fd37 restoring debug-info 2024-03-26 08:58:16 +01:00
Ben c122aa9910 sets up http client 2024-03-25 15:46:45 +01:00
Ben 03a3ccb4de prototype of pre-build yaml hash injection 2024-03-25 13:48:20 +01:00
Ben 87ec67778b poc: upload and download streams working 2024-03-25 11:37:41 +01:00
Ben bd5fe0cae1 Sets up client generation with NSwag 2024-03-25 10:20:44 +01:00
Ben e8213b9515 Updates codex image 2024-03-25 10:17:32 +01:00
Slava 617b656cf7 Use just folder as a path for digests upload (#99) 2024-03-23 21:13:25 +02:00
Ben 042285e664 increases block cache size 2024-03-22 14:16:55 +01:00
Ben 7d28b62206 new image 2024-03-22 13:36:33 +01:00
Ben a27b294031 Updates codex marketplace API types 2024-03-22 11:15:22 +01:00
Ben 0a4c4c60c5 Merge branch 'master' into feature/bot-upgrade
# Conflicts:
#	Framework/NethereumWorkflow/BlockTimeFinder.cs
#	Framework/NethereumWorkflow/NethereumInteraction.cs
2024-03-22 10:59:57 +01:00
Ben 635849c37a Merge branch 'feature/persistence-prover' 2024-03-22 10:56:50 +01:00
Ben 3cc3a2a9dd cleanup 2024-03-21 16:26:48 +01:00
Ben 6d4d6fcdb9 working blocktime finder 2024-03-21 16:11:28 +01:00
Ben 4fdb310ca4 Sets up tests for blocktimefinder. 2024-03-21 12:18:04 +01:00
Ben Bierens 9ce82cb011 Merge pull request #72 from GoodDaisy/master
fix Continous typo
2024-03-20 14:45:09 +01:00
Ben db37143db6 cleanup 2024-03-20 13:36:06 +01:00
Ben 2a5b3e0eec fixes marketplace test 2024-03-20 11:32:16 +01:00
Ben ef53fe02a3 Improve logfile checking 2024-03-20 11:11:59 +01:00
Ben a6c2bf5230 Cleanup marketplace-access methods 2024-03-20 11:11:41 +01:00
Ben 32f56e8213 Marketplace test fails: submitted proof is invalid. 2024-03-15 16:07:10 +01:00
Ben f757e64ba4 wip 2024-03-15 13:50:39 +01:00
benbierens 188f765b5c bash, not sh 2024-03-15 12:48:25 +01:00
benbierens 8c8b1748ee Merge branch 'master' into feature/bot-upgrade
# Conflicts:
#	Tests/CodexTests/BasicTests/ExampleTests.cs
2024-03-15 09:35:27 +01:00
Ben c204ab09d1 combinatorial tests for finding issue 2024-03-14 14:30:00 +01:00
Ben db5c5444c5 Removes contract clock from logging 2024-03-14 14:29:28 +01:00
Ben 209eb31c58 fixes issue where kubernetes does not honor container entrypoints when command override is used 2024-03-13 12:09:25 +01:00
Ben 956cdbcfbe sets contracts image to latest 2024-03-13 10:57:35 +01:00
Ben c5fb066c75 Allows for non-blocking stop of containers 2024-03-13 10:57:26 +01:00
Ben 90b90be3cb Updates usages of EnableMarketplace 2024-03-13 10:29:26 +01:00
Ben a6a8c3f1e7 Update codex container recipe 2024-03-13 10:18:10 +01:00
Ben b7904f7ee0 Updates codexSetup and Marketplace configuration 2024-03-13 10:14:11 +01:00
Ben 23396f54e0 Revert "Adds support for command overrides to container recipes."
This reverts commit d1a70d2465.
2024-03-13 10:01:47 +01:00
Ben e42f1ddbd7 Adds support for command overrides to container recipes. 2024-03-13 10:01:37 +01:00
Ben d1a70d2465 Adds support for command overrides to container recipes. 2024-03-13 10:01:14 +01:00
Slava 3c8efbd0a0 ci: upldate actions to the latest versions (#98) 2024-03-12 16:32:31 +02:00
benbierens 247e010fba Cleanup marketplace test 2024-02-28 10:06:11 +01:00
benbierens f0167a800f Speed up basic tests 2024-02-27 13:55:05 +01:00
benbierens b35cf3139d Bumps contracts image to latest 2024-02-26 15:08:04 +01:00
benbierens 64ecf531bd Adds missing error message check to marketplaceAccess 2024-02-26 09:02:46 +01:00
Giuliano Mega ec0f7a6790 Remove run ID and introduce deploy ID (#93)
This PR removes the notion of a run id and replaces it with a deploy id in continuous tests. Deploy ids can be set at deploy time only (duh), and will be picked up by the test runner from the deploy file on subsequent runs of the continuous test runner. As a consequence, the deploy id becomes a deployer parameter, and can no longer be overridden at the runner. For non-continuous tests, the deploy ID is created on-the-fly.
2024-02-22 10:41:07 -03:00
Slava 930c44f037 ci: use temporary branch for setup-kubectl action (#96) 2024-02-19 20:50:42 +02:00
benbierens a6fce1084d Check connection no more than once every 30 seconds 2024-02-19 15:59:49 +01:00
benbierens 23b7bbd548 Fixes sending of rewards to bot 2024-02-19 15:57:28 +01:00
benbierens 42d3c5cd2c debugging rewards 2024-02-19 15:41:48 +01:00
benbierens 1bf693938c Block range type 2024-02-19 15:20:12 +01:00
benbierens da4101a042 Debugging time segmenter 2024-02-19 14:56:49 +01:00
Slava dc33f0655c ci: upldate actions to the latest versions (#95) 2024-02-19 13:05:51 +02:00
benbierens 3c210f96fc debugging reward api 2024-02-19 09:11:36 +01:00
benbierens dd5baeda46 Adds reward api port to bot config 2024-02-01 17:02:10 -05:00
benbierens 7d33c1c113 Sets up rewarder bot container recipe 2024-01-31 11:52:02 -05:00
benbierens 8a4d9fecea Merge branch 'feature/bot-upgrade' 2024-01-31 11:31:52 -05:00
benbierens cb259a9086 Setting up docker for rewarder bot 2024-01-31 11:30:29 -05:00
benbierens 9d8ec6f794 loads up reward-repo with test server roles 2024-01-31 11:18:22 -05:00
benbierens acc9526cd5 Extracts reward config from bot and rewarder 2024-01-29 11:02:47 -05:00
benbierens 43a160a9cc Wires everything up 2024-01-27 09:12:20 -05:00
benbierens 4de2626a33 wip 2024-01-26 18:17:56 -05:00
benbierens 255e08a301 Implements check classes 2024-01-26 17:29:57 -05:00
benbierens 71409b06ae Increases timeout for codex contracts deployment 2024-01-26 11:37:18 -05:00
benbierens f25bca727d Fixes crash in blocktime finder. Increases smart contract deploy timeout. 2024-01-26 11:27:37 -05:00
benbierens fb73312e80 Merge branch 'master' into feature/bot-upgrade 2024-01-26 08:47:23 -05:00
Slava 9a626b7260 Updates contracts image to latest working one (#92) 2024-01-25 19:10:49 +02:00
Slava ede80410ea Disable Report workflows (#91) 2024-01-23 10:47:36 +02:00
benbierens 00c720137a Adds method for getting request state and receiving cancelled events 2024-01-22 16:27:32 +01:00
benbierens 890cff93d5 Adds method for getting request-fulfilled events 2024-01-22 16:05:04 +01:00
benbierens 678b719cef wip 2024-01-22 11:47:28 +01:00
benbierens 1b7c11b849 Setting up rewards 2024-01-22 10:27:07 +01:00
benbierens c6a7489f11 wip 2024-01-20 13:07:56 +01:00
benbierens f7fcef56c7 upgrades log filtering 2024-01-18 10:24:59 +01:00
benbierens 29fa554146 Adds log topics for block exchange 2024-01-18 09:55:07 +01:00
benbierens 2f10b30283 Sets up rewards api and handling. 2023-12-20 15:56:03 +01:00
benbierens 391a2653d9 Sets up getting of slot filled and freed events 2023-12-20 13:21:53 +01:00
benbierens 55be07d711 Implements getting slot host address 2023-12-20 11:34:23 +01:00
benbierens 75757e37fb Can fetch storage requests from chain 2023-12-20 10:55:29 +01:00
benbierens 2be31a4d3b Cleanup 2023-12-20 10:33:44 +01:00
benbierens 4b74a9d5fe working block time finder 2023-12-20 09:48:22 +01:00
benbierens 4f2539c59f Adds marketplace generated code 2023-12-19 15:43:26 +01:00
benbierens 7df1f3da7b block time finder 2023-12-19 15:30:46 +01:00
benbierens fb57998aa8 Merge branch 'master' into feature/bot-upgrade 2023-12-19 14:02:31 +01:00
Slava c4eceacafb Trigger Docker builds on folders content changes (#90) 2023-12-18 16:42:23 +02:00
Slava 974fc460c4 Remove preview from MintCommand explorer link (#89) 2023-12-18 16:38:16 +02:00
benbierens 87e84ae90d Adds method for checking of codex contracts are deployed 2023-12-18 13:32:58 +01:00
benbierens 2c026f99ca Merge branch 'feature/bot-upgrade' 2023-12-18 11:28:22 +01:00
benbierens 6298744918 Better logging 2023-12-18 11:27:28 +01:00
Slava 5342261db0 Update Report workflows (#88) 2023-12-18 11:30:15 +02:00
Slava fd540a53b1 Add Discordbot docker builds (#87)
* Add BiblioTech Docker builds

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

* Organize workflows

https://github.com/codex-storage/infra-codex/issues/113
2023-12-15 14:07:52 +02:00
benbierens 03283414cb Adds transaction ID to record of user activity 2023-12-15 11:09:15 +01:00
benbierens beaa67c280 Adds transaction links to mint output 2023-12-15 11:02:06 +01:00
benbierens d67ddab290 Checking for all unwanted log entries 2023-12-15 09:32:03 +01:00
benbierens b143136590 Merge branch 'feature/public-testnet-deploying' 2023-12-14 15:59:22 +01:00
benbierens 8fc8a3f7cf Adds check for block verification failures 2023-12-12 14:18:26 +01:00
benbierens 2b21722cf3 forgot to hook up getSPR command 2023-12-11 11:19:05 +01:00
benbierens c3fb7384e3 Sets up deploying discordbot with geth env vars 2023-12-11 11:09:29 +01:00
benbierens fde19383df Removes deployment.json from discord bot. 2023-12-11 11:01:12 +01:00
benbierens 777e414f0a Prevents duplicate deployments. 2023-12-11 09:59:09 +01:00
benbierens 6d7309cc9c Update bot for deployment without codex nodes. 2023-12-11 08:51:46 +01:00
benbierens ec03be6936 Fixes initialization of geth node 2023-12-11 08:38:31 +01:00
benbierens 5dc918287c Merge branch 'master' into feature/public-testnet-deploying 2023-12-11 08:30:25 +01:00
benbierens 55811b20da Fixes validation for not deploying codex nodes 2023-12-11 08:30:05 +01:00
benbierens 09554da362 Adds transfer speeds to status logs. 2023-12-06 10:50:02 +01:00
benbierens 074f5ebfae Sets up transferSpeed class 2023-12-06 09:59:45 +01:00
benbierens 260bc93414 Updates contracts image to latest 2023-12-06 09:34:22 +01:00
benbierens ad05dc07f0 wip report for november 2023-12-05 08:31:28 +01:00
Slava 12ea002660 Update Runners Jobs nodeSelector (#86)
https://github.com/codex-storage/infra-codex/issues/96
2023-11-27 19:22:28 +02:00
Slava 4c46a708ab Change affinity label (#85)
https://github.com/codex-storage/infra-codex/issues/100
2023-11-27 19:06:04 +02:00
Slava b57a727195 Adjust C-Tests for autoruns (#84) 2023-11-26 21:20:04 +02:00
Slava 00207de04d Auto cleanup old Continuous tests Jobs (#83) 2023-11-24 20:29:50 +02:00
benbierens 3761e236a3 Sets node critical priority for codex and geth nodes 2023-11-23 14:50:54 +01:00
benbierens ff52e8e841 Defines public IP fetching service 2023-11-23 14:15:37 +01:00
benbierens b61f5d835c Fixes request-storage endpoint 2023-11-23 14:10:59 +01:00
benbierens 1a86d3459d Fixes local files endpoint 2023-11-23 13:43:19 +01:00
benbierens 6bd7098cfe Fixes download endpoint. 2023-11-23 13:32:58 +01:00
Slava 06ff7c9760 Add CODEXDOCKERIMAGE input to C-Tests workflow (#82) 2023-11-22 21:04:59 +02:00
Slava 703f410161 Fix reports workflows scheduler (#81) 2023-11-22 12:38:19 +02:00
benbierens 46ab3b31ca Checking that we can bootstrap geth nodes together. 2023-11-21 10:33:11 +01:00
benbierens a84d6a3c22 Replaces debug/info fetching with list of known SPRs, settable via admin commands. 2023-11-21 09:38:58 +01:00
benbierens 54471d41d5 Allows for deployment without codex nodes 2023-11-21 09:03:52 +01:00
benbierens 8b224f6922 Updates public ip to point to service 2023-11-20 11:19:10 +01:00
benbierens 005c9b4259 Merge branch 'master' into feature/public-testnet-deploying 2023-11-20 11:17:17 +01:00
Slava 6f53e78e6a Run C-Tests reports by Scheduler (#80) 2023-11-19 21:13:40 +02:00
benbierens f4532f3801 Target duration now supports timespan string format. 2023-11-17 14:51:32 +01:00
benbierens b90c47a994 Switches to automatic public IP address for codex and geth containers. 2023-11-15 14:53:25 +01:00
benbierens db55792f42 Catching exceptions in task-factory. 2023-11-14 13:28:50 +01:00
benbierens 5996c0fa63 Adds involvedpods, error, and duration in seconds to status logs for continuous tests. 2023-11-14 12:56:47 +01:00
benbierens 485e3cf02e Merge branch 'master' into feature/public-testnet-deploying 2023-11-14 10:50:41 +01:00
benbierens ee319a6d0f Merge branch 'feature/scheduling-affinity' 2023-11-14 10:50:32 +01:00
benbierens b47b596062 Fetches used external ports in order to guarantee no collisions. 2023-11-14 10:49:14 +01:00
benbierens 7de0e5a1c4 Sets up tests-runners as avoided scheduling affinity. 2023-11-14 10:16:00 +01:00
Slava 7114fd1c00 Use dedicated node pool for tests runners (#79)
https://github.com/codex-storage/infra-codex/issues/88
2023-11-13 22:37:00 +02:00
benbierens 4192952a37 Prevents reuse of external port numbers 2023-11-13 16:05:41 +01:00
benbierens b82c74865e Adds numberSource for correct range for external ports 2023-11-13 15:27:52 +01:00
benbierens ca350604e3 Debugging nodeport 2023-11-13 15:11:49 +01:00
benbierens 5a7608460b retry by setting nodeport explicitly 2023-11-13 13:58:34 +01:00
benbierens 2565ced7fe Adds test report for october 2023-11-13 13:29:28 +01:00
benbierens a81f0b9145 Merge branch 'master' into feature/public-testnet-deploying 2023-11-13 13:15:23 +01:00
benbierens 1f841ca629 Removes modified/clean indicator 2023-11-13 13:05:44 +01:00
benbierens 537c37351a Merge branch 'master' of https://github.com/status-im/cs-codex-dist-tests 2023-11-13 11:57:48 +01:00
benbierens 0f9e01e393 Adds git revision information to logs for testing framework status 2023-11-13 11:56:02 +01:00
Slava 90118b3481 Use separate log folder per Continuous Tests run (#78) 2023-11-13 12:50:32 +02:00
benbierens 412a4d3c7a Sets exit code for continuous tests runner 2023-11-13 11:17:26 +01:00
benbierens 960bef455a Adds codex revision to metadata. 2023-11-13 09:37:45 +01:00
benbierens 344d3cd44f Merge branch 'master' into feature/continuous-test-status-logs
# Conflicts:
#	Tests/DistTestCore/Logs/StatusLog.cs
2023-11-13 09:33:24 +01:00
benbierens 6636ef3ac1 Moves routing table test into peerDiscovery tests. 2023-11-13 09:32:10 +01:00
Slava 496f8fa370 Add Run ID to the Tests Identification (#77) 2023-11-12 18:35:11 +02:00
Slava 25148db971 Show Tests Identification in runners workflows (#76) 2023-11-12 17:07:53 +02:00
benbierens 71ad471958 Cleanup routing table tests. Adds retry. 2023-11-12 11:24:58 +01:00
benbierens 96ff3c38bb Adds test for routing table checks. 2023-11-12 10:36:48 +01:00
benbierens ed56d9edcc Cleanup of kubernetesWorkflow assembly. 2023-11-12 10:07:23 +01:00
benbierens b78f527c39 Adds test-type to STATUS log jsons. 2023-11-10 15:28:53 +01:00
benbierens 096282ae1a Fixes debug/peer serialization. Adds retry for pod-finding. 2023-11-10 15:23:16 +01:00
benbierens 0be6df46bc Merge branch 'deployment-json-rework' into feature/continuous-test-status-logs 2023-11-10 09:49:35 +01:00
benbierens c5cc15daa0 Updates endpoint to match latest Codex 2023-11-10 08:20:08 +01:00
benbierens 0301c3b076 Adds status log entry for each test run in continuous tests. 2023-11-09 11:35:45 +01:00
benbierens 84382b4ab4 Removes server-admin requirement for admin commands 2023-11-09 11:14:17 +01:00
benbierens 26ec841db1 locks out default permissions for admin commands 2023-11-09 10:45:28 +01:00
benbierens 1ed93bbdde Adds required admin permission for admin commands. 2023-11-09 10:37:57 +01:00
benbierens ef83441b2f Correct chunking of long messages 2023-11-09 10:13:34 +01:00
benbierens 180bf3fd35 Fixes debugPeer response formatting 2023-11-08 13:33:48 +01:00
benbierens d1403c04c4 updates bot secret name 2023-11-08 11:59:31 +01:00
benbierens 8d12bc45a1 Loops in kubeconfig and namespace for discord bot. 2023-11-08 11:49:21 +01:00
benbierens 9f0f7c374a port update 2023-11-08 10:19:06 +01:00
benbierens b8ce4c49d6 Merge branch 'master' into deployment-json-rework
# Conflicts:
#	Tests/CodexContinuousTests/run.sh
#	Tools/CodexNetDeployer/deploy-continuous-testnet.sh
2023-11-08 09:29:22 +01:00
benbierens 90070de028 Enable debug logging from single environment variable or static field. 2023-11-08 09:24:39 +01:00
benbierens 073f0d86a1 Cleans up runnerLocationUtils. 2023-11-07 16:02:09 +01:00
benbierens ec6c987ef9 determine runner location with environment variables 2023-11-07 15:51:28 +01:00
benbierens 3f8699a59b Removes dependency on pod information for elastic search download 2023-11-07 14:33:45 +01:00
benbierens 2b463d060d Restores debug setting 2023-11-07 14:00:08 +01:00
benbierens 9d464708b5 Updates local address format 2023-11-07 13:49:28 +01:00
benbierens 3e49ce6ae9 Adds kube namespace to internal addresses 2023-11-07 13:13:06 +01:00
benbierens 6e60a8614c Removes runnerlocation field from StartResult. 2023-11-07 13:06:49 +01:00
benbierens eecdcf308d Rewrites runner location set 2023-11-07 12:48:18 +01:00
benbierens 3e500e8346 Unknown runner location option. 2023-11-07 12:42:01 +01:00
benbierens ea66b5b408 debugging address selection in cluster 2023-11-07 12:02:17 +01:00
benbierens 655f4895d0 Fixes serialization issue for running containers for discord bot and metrics container 2023-11-07 11:17:20 +01:00
Ben Bierens dae4aa1f64 Merge pull request #74 from codex-storage/feature/automate-continuous-tests
Automate Continuous Tests using GitHub Actions
2023-11-07 10:48:45 +01:00
benbierens 20f92633f7 updates metrics name in twoclient test 2023-11-07 10:42:31 +01:00
benbierens d0bed64c68 Disables resource limits 2023-11-07 09:25:51 +01:00
benbierens 1412dd84fe Get ports explicitly for internal or external endpoints 2023-11-06 16:52:55 +01:00
benbierens ad88560061 Creates internal service ports for external ports from container recipe. 2023-11-06 16:38:32 +01:00
benbierens 8d0b3feff7 logs runner location 2023-11-06 16:12:18 +01:00
benbierens 6672427565 Restores marketplace test 2023-11-06 16:10:19 +01:00
benbierens 4f9c0e0ce7 Restores metrics 2023-11-06 15:27:23 +01:00
benbierens dc9f3ab090 removes dependency on static pod name and address info 2023-11-06 14:33:47 +01:00
Veaceslav Doina ead1ff7ec6 Automate Continuous Tests using GitHub Actions (#69) 2023-11-06 15:09:51 +02:00
Veaceslav Doina 807e8b7b5f Fix TwoClientTest metric name
https://github.com/codex-storage/nim-codex/pull/603
2023-11-06 15:06:47 +02:00
benbierens db0a21bc60 Merge branch 'automated-teststarter' into deployment-json-rework 2023-11-03 14:57:32 +01:00
benbierens 19d466d5d6 better service names 2023-11-03 14:53:27 +01:00
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
GoodDaisy 65473d8c15 fix Continous typo 2023-10-26 20:27:44 +08: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
308 changed files with 12762 additions and 3168 deletions
-81
View File
@@ -1,81 +0,0 @@
name: Dist Tests
on:
# push:
# branches:
# - master
# tags:
# - 'v*.*.*'
# paths-ignore:
# - '**/*.md'
# - '.gitignore'
# - 'docker/**'
# - '!docker/job.yaml'
workflow_dispatch:
inputs:
branch:
description: Branch (master)
required: false
type: string
source:
description: Repository with tests (current)
required: false
type: string
nameprefix:
description: Runner prefix (codex-dist-tests)
required: false
type: string
namespace:
description: Runner namespace (default)
required: false
type: string
command:
description: Runner command (dotnet test Tests)
required: false
type: string
env:
BRANCH: ${{ github.ref_name }}
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
NAMEPREFIX: codex-dist-tests
NAMESPACE: default
COMMAND: dotnet test Tests/CodexTests
JOB_MANIFEST: docker/job.yaml
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.26.1
jobs:
run_tests:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Variables
run: |
[[ -n "${{ github.event.inputs.branch }}" ]] && echo "BRANCH=${{ github.event.inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
[[ -n "${{ github.event.inputs.source }}" ]] && echo "SOURCE=${{ github.event.inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
[[ -n "${{ github.event.inputs.nameprefix }}" ]] && echo "NAMEPREFIX=${{ github.event.inputs.nameprefix }}" >>"$GITHUB_ENV" || echo "NAMEPREFIX=${{ env.NAMEPREFIX }}" >>"$GITHUB_ENV"
[[ -n "${{ github.event.inputs.namespace }}" ]] && echo "NAMESPACE=${{ github.event.inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
[[ -n "${{ github.event.inputs.command }}" ]] && COMMAND="${{ github.event.inputs.command }}" || COMMAND="${{ env.COMMAND }}"
echo "COMMAND=$(jq -c 'split(" ")' <<< '"'$COMMAND'"')" >>"$GITHUB_ENV"
echo "RUNID=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
uses: azure/setup-kubectl@v3
with:
version: ${{ env.KUBE_VERSION }}
- name: Kubectl - Kubeconfig
run: |
mkdir -p "${HOME}"/.kube
echo "${{ env.KUBE_CONFIG }}" | base64 -d > "${HOME}"/.kube/config
- name: Kubectl - Create Job
run: |
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
+28
View File
@@ -0,0 +1,28 @@
name: Docker - Discord Bot
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/BiblioTech/**'
- '!Tools/BiblioTech/docker/docker-compose.yaml'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-discordbot.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/BiblioTech/docker/Dockerfile
docker_repo: codexstorage/codex-discordbot
secrets: inherit
+27
View File
@@ -0,0 +1,27 @@
name: Docker - KeyMaker
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/KeyMaker/**'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-KeyMaker.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/KeyMaker/docker/Dockerfile
docker_repo: codexstorage/codex-keymaker
secrets: inherit
+15 -14
View File
@@ -1,4 +1,4 @@
name: Docker - Reusable
name: Reusable - Docker
on:
@@ -66,26 +66,26 @@ jobs:
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REPO }}
- name: Docker - Set up Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Docker - Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Docker - Build and Push by digest
id: build
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ${{ env.DOCKER_FILE }}
@@ -101,10 +101,10 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Docker - Upload digest
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: digests
path: /tmp/digests/*
name: digests-${{ matrix.target.arch }}
path: /tmp/digests
if-no-files-found: error
retention-days: 1
@@ -139,17 +139,18 @@ jobs:
fi
- name: Docker - Download digests
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: digests
pattern: digests-*
merge-multiple: true
path: /tmp/digests
- name: Docker - Set up Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Docker - Meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ env.DOCKER_REPO }}
flavor: |
@@ -161,7 +162,7 @@ jobs:
type=sha,enable=${{ env.TAG_SHA }}
- name: Docker - Login to Docker Hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
+26
View File
@@ -0,0 +1,26 @@
name: Docker - Rewarder Bot
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/TestNetRewarder/**'
- '!Tools/TestNetRewarder/docker/docker-compose.yaml'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-rewarder.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
jobs:
build-and-push:
name: Build and Push
uses: ./.github/workflows/docker-reusable.yml
with:
docker_file: Tools/TestNetRewarder/docker/Dockerfile
docker_repo: codexstorage/codex-rewarderbot
secrets: inherit
@@ -1,4 +1,4 @@
name: Docker
name: Docker - Runner
on:
@@ -10,7 +10,7 @@ on:
paths:
- docker/Dockerfile
- docker/docker-entrypoint.sh
- .github/workflows/docker.yml
- .github/workflows/docker-runner.yml
- .github/workflows/docker-reusable.yml
workflow_dispatch:
@@ -0,0 +1,20 @@
name: Report - HoldMyBeerTest
on:
# schedule:
# - cron: '30 1 */2 * *'
workflow_dispatch:
jobs:
run_tests:
name: Call runner
uses: ./.github/workflows/run-continuous-tests.yaml
with:
source: ${{ format('{0}/{1}', github.server_url, github.repository) }}
branch: master
nameprefix: c-tests-report-holdmybeertest-48h
tests_target_duration: 48h
tests_filter: HoldMyBeerTest
tests_cleanup: true
secrets: inherit
+20
View File
@@ -0,0 +1,20 @@
name: Report - PeersTest
on:
# schedule:
# - cron: '30 1 */2 * *'
workflow_dispatch:
jobs:
run_tests:
name: Call runner
uses: ./.github/workflows/run-continuous-tests.yaml
with:
source: ${{ format('{0}/{1}', github.server_url, github.repository) }}
branch: master
nameprefix: c-tests-report-peerstest-48h
tests_target_duration: 48h
tests_filter: PeersTest
tests_cleanup: true
secrets: inherit
+170
View File
@@ -0,0 +1,170 @@
name: Run Continuous Tests
on:
workflow_dispatch:
inputs:
source:
description: Repository with tests (current)
required: false
type: string
branch:
description: Branch with tests (master)
required: false
type: string
codexdockerimage:
description: Codex Docker image (codexstorage/nim-codex:latest-dist-tests)
required: false
type: string
nameprefix:
description: Resources prefix (c-tests)
required: false
type: string
tests_target_duration:
description: Runner target duration (2d)
required: false
type: string
tests_filter:
description: Runner tests filter ("")
required: false
type: string
tests_cleanup:
description: Runner tests cleanup
required: false
type: boolean
default: true
workflow_call:
inputs:
source:
description: Repository with tests (current)
required: false
type: string
branch:
description: Branch with tests (master)
required: false
type: string
codexdockerimage:
description: Codex Docker image (codexstorage/nim-codex:latest-dist-tests)
required: false
type: string
nameprefix:
description: Resources prefix (c-tests)
required: false
type: string
tests_target_duration:
description: Runner target duration (2d)
required: false
type: string
tests_filter:
description: Runner tests filter ("")
required: false
type: string
tests_cleanup:
description: Runner tests cleanup
required: false
type: boolean
default: true
workflow_source:
description: Workflow source
required: false
type: string
default: ''
env:
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
BRANCH: ${{ github.ref_name }}
CODEXDOCKERIMAGE: codexstorage/nim-codex:latest-dist-tests
NAMEPREFIX: c-tests
NAMESPACE: default
TESTS_TARGET_DURATION: 2d
TESTS_FILTER: ""
TESTS_CLEANUP: true
JOB_MANIFEST: docker/continuous-tests-job.yaml
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.28.2
jobs:
run_tests:
name: Run Continuous Tests ${{ inputs.tests_filter }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
repository: ${{ inputs.workflow_source }}
- name: Variables
run: |
RUNID=$(date +%Y%m%d-%H%M%S)
echo "RUNID=${RUNID}" >> $GITHUB_ENV
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.codexdockerimage }}" ]] && echo "CODEXDOCKERIMAGE=${{ inputs.codexdockerimage }}" >>"$GITHUB_ENV" || echo "CODEXDOCKERIMAGE=${{ env.CODEXDOCKERIMAGE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.nameprefix }}" ]] && NAMEPREFIX="`awk '{ print tolower($0) }' <<< ${{ inputs.nameprefix }}`" || NAMEPREFIX="`awk '{ print tolower($0) }' <<< ${{ env.NAMEPREFIX }}`"
echo "NAMEPREFIX=${NAMEPREFIX}-${RUNID}" >>"$GITHUB_ENV"
echo "DEPLOYMENT_NAMESPACE=${NAMEPREFIX}-${RUNID}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.tests_target_duration }}" ]] && echo "TESTS_TARGET_DURATION=${{ inputs.tests_target_duration }}" >>"$GITHUB_ENV" || echo "TESTS_TARGET_DURATION=${{ env.TESTS_TARGET_DURATION }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.tests_filter }}" ]] && echo "TESTS_FILTER=${{ inputs.tests_filter }}" >>"$GITHUB_ENV" || echo "TESTS_FILTERS=${{ env.TESTS_FILTERS }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.tests_cleanup }}" ]] && echo "TESTS_CLEANUP=${{ inputs.tests_cleanup }}" >>"$GITHUB_ENV" || echo "TESTS_CLEANUP=${{ env.TESTS_CLEANUP }}" >>"$GITHUB_ENV"
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
uses: azure/setup-kubectl@v4
with:
version: ${{ env.KUBE_VERSION }}
- name: Kubectl - Kubeconfig
run: |
mkdir -p "${HOME}"/.kube
echo "${{ env.KUBE_CONFIG }}" | base64 -d > "${HOME}"/.kube/config
- name: Kubectl - Create Job
run: |
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
- name: Tests Identification
run: |
echo "----"
echo "Repository: ${{ env.SOURCE }}"
echo "Branch: ${{ env.BRANCH }}"
echo "Runner job: ${{ env.NAMEPREFIX }}"
echo "Runner pod: `kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -ojsonpath='{.items[0].metadata.name}'`"
echo "Runner namespace: ${{ env.NAMESPACE }}"
echo "----"
echo "Tests runid: ${{ env.RUNID }}"
echo "Tests codexdockerimage: ${{ env.CODEXDOCKERIMAGE }}"
echo "Tests namespace: ${{ env.DEPLOYMENT_NAMESPACE }}"
echo "Tests duration: ${{ env.TESTS_TARGET_DURATION }}"
echo "Tests filter: ${{ env.TESTS_FILTER }}"
echo "Tests cleanup: ${{ env.TESTS_CLEANUP }}"
echo "----"
- name: Show Runner logs
if: false
run: |
# Variables
# We need more than 300 seconds because Auto Scaler may take 3 minutes to tun a node
duration=600
namespace="${{ env.NAMESPACE }}"
pod=$(kubectl get pod --selector job-name=${{ env.NAMEPREFIX }} -o jsonpath="{.items[0].metadata.name}")
# Check Pod status
WAIT=120
SECONDS=0
sleep=1
while (( SECONDS < WAIT )); do
phase=$(kubectl get pod ${pod} -n ${namespace} -o jsonpath="{.status.phase}")
[[ "${phase}" == "Running" ]] && { echo "Pod $pod is in $phase state - Get the logs"; break; } || { echo "Pod $pod is in $phase state - Retry in $sleep seconds / $((WAIT - SECONDS))"; }
sleep $sleep
done
# Get logs
timeout $duration \
kubectl logs $pod \
-n $namespace \
-f \
--tail=-1 \
--timestamps || true
+83
View File
@@ -0,0 +1,83 @@
name: Run Dist Tests
on:
workflow_dispatch:
inputs:
source:
description: Repository with tests (current)
required: false
type: string
branch:
description: Branch with tests (master)
required: false
type: string
nameprefix:
description: Runner name prefix (d-tests-runner)
required: false
type: string
namespace:
description: Runner namespace (default)
required: false
type: string
command:
description: Command (dotnet test Tests/CodexTests)
required: false
type: string
env:
SOURCE: ${{ format('{0}/{1}', github.server_url, github.repository) }}
BRANCH: ${{ github.ref_name }}
NAMEPREFIX: d-tests-runner
NAMESPACE: default
COMMAND: dotnet test Tests/CodexTests
JOB_MANIFEST: docker/dist-tests-job.yaml
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG }}
KUBE_VERSION: v1.28.2
jobs:
run_tests:
name: Run Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Variables
run: |
echo "COMMAND=$(jq -c 'split(" ")' <<< '"'$COMMAND'"')" >>"$GITHUB_ENV"
echo "RUNID=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_ENV
echo "TESTID=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
[[ -n "${{ inputs.branch }}" ]] && echo "BRANCH=${{ inputs.branch }}" >>"$GITHUB_ENV" || echo "BRANCH=${{ env.BRANCH }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.source }}" ]] && echo "SOURCE=${{ inputs.source }}" >>"$GITHUB_ENV" || echo "SOURCE=${{ env.SOURCE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.nameprefix }}" ]] && echo "NAMEPREFIX=${{ inputs.nameprefix }}" >>"$GITHUB_ENV" || echo "NAMEPREFIX=${{ env.NAMEPREFIX }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.namespace }}" ]] && echo "NAMESPACE=${{ inputs.namespace }}" >>"$GITHUB_ENV" || echo "NAMESPACE=${{ env.NAMESPACE }}" >>"$GITHUB_ENV"
[[ -n "${{ inputs.command }}" ]] && COMMAND="${{ inputs.command }}" || COMMAND="${{ env.COMMAND }}"
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
uses: azure/setup-kubectl@v4
with:
version: ${{ env.KUBE_VERSION }}
- name: Kubectl - Kubeconfig
run: |
mkdir -p "${HOME}"/.kube
echo "${{ env.KUBE_CONFIG }}" | base64 -d > "${HOME}"/.kube/config
- name: Kubectl - Create Job
run: |
envsubst < ${{ env.JOB_MANIFEST }} | kubectl apply -f -
- name: Tests Identification
run: |
echo "----"
echo "Repository: ${{ env.SOURCE }}"
echo "Branch: ${{ env.BRANCH }}"
echo "Runner job: ${{ env.NAMEPREFIX }}-${{ env.RUNID }}"
echo "Runner namespace: ${{ env.NAMESPACE }}"
echo "----"
echo "Tests runid: ${{ env.RUNID }}"
echo "Tests command: `jq -r '. | join(" ")' <<< '${{ env.COMMAND }}'`"
echo "----"
+86 -14
View File
@@ -1,10 +1,26 @@
# Distributed System Tests for Nim-Codex
## Contributing plugins
The testing framework was created for testing Codex. However, it's been designed such that other distributed/containerized projects can 'easily' be added. In order to add your project to the framework you must:
The testing framework was created for testing Codex. However, it's been designed such that other containerized projects can 'easily' be added.
In this file, you'll see 'users' (in quote) mentioned once or twice. This refers to code/projects/tests which end up making use of your plugin. 'Users' come in many shapes and sizes and tend to have many differen use-cases in mind. Please consider this when reading this document and writing your plugin.
## Checklist
Your application must pass this checklist to be compatible with the framework:
- It runs in a docker container.
- It can be configured via environment variables. (You may need to create a docker image which contains a shell script, to pass some env-vars as CLI arguments to your application. Container command overrides do work, but are not equally reliable across container platforms. When in doubt: use env-var!)
- It has network interaction:
- It exposes one or more APIs via one or more ports, OR
- It makes calls to other services. (OR both.)
If your application's use-cases rely primarily on shell interaction, this framework might not be for you. The framework allows you to execute commands in containers AND read stdout/stderr responses. However, its focus during development has always been webservice API interactions.
## Steps
In order to add your project to the framework you must:
1. Create a library assembly in the project plugins folder.
1. It must contain a type that implements the `IProjectPlugin` interface from the `Core` assembly.
1. If your plugin wants to expose any specific methods or objects to the code using the framework (the tests and tools), it must implement extensions for the `CoreInterface` type.
1. If your plugin wants to expose any specific methods or objects to 'users', it must implement extensions for the `CoreInterface` type.
1. If your plugin wants to run containers of its own project, it must provide a recipe.
## Constructors & Tools
Your implementation of `IProjectPlugin` must have a public constructor with a single argument of type `IPluginTools`, for example:
@@ -20,19 +36,34 @@ Your implementation of `IProjectPlugin` must have a public constructor with a si
}
```
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients for containers. (Without having to figure out addresses manually.)
## Plugin Interfaces
The `IProjectPlugin` interface requires the implementation of two methods.
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version information at this time if applicable.
1. `Decommission` - Should your plugin have any active system resources, free them in this method.
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version and/or configuration information at this time if applicable.
1. `Decommission` - Should your plugin have any active system resources, free them in this method. Please note that resources managed by the framework (such as running containers and tracked data files) do *not* need to be manually disposed in this method. `Decommission` is to be used for resources not managed by the framework.
There are a few optional interfaces your plugin may choose to implement. The framework will automatically use these interfaces.
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string with will be prepended to all log statements made by your plugin.
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by code that uses your plugin.
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string which will be prepended to all log statements made by your plugin. A polite thing to do.
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by 'users' of your plugin. Often this data finds its way into log files and container-descriptors in order to help track versions/tests/deployments, etc.
## IPluginTools
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients to make calls to containers. (Figure out addresses and ports for containers is handled by the framework.)
It is possible and allowed for your plugin to depend on and use other plugins. (For example, maybe your project wants to interact with Ethereum and wants to use the GethPlugin to talk to a Geth node.) `IPluginTools` is *not* what is used for accessing functionality of other plugins. See 'Core Interface' section.
ILog GetLog();
IHttp CreateHttp(Action<HttpClient> onClientCreated);
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp();
IFileManager GetFileManager();
The plugin tools provide:
1. `Workflow` - This tool allows you to start and stop containers using "container recipes". (More on those below.) It also allows you to execute commands inside a container, access stdout/stderr, detect crashes, and access pod deployment information. The workflow tool also lets you inspect the locations available in the cluster, and decide where you want to run containers. (More on that below as well.)
1. `Log` - Good logging is priceless. Use this tool to get a log object handle, and write useful debug/info/error statements.
1. `Http` - This tool gives you a convenient way to access a standard dotnet HttpClient, and takes care of timeouts and retries (in accordance with the config). Additionally, it combos nicely with container objects created by `Workflow`, such that you never have to spend any time figuring out the addresses and ports of your containers.
1. `FileManager` - Lets you use tracked temporary files. Even if the 'user' tests/application start crashing, the framework will make sure these are cleaned up.
## Core Interface
Any functionality your plugin wants to expose to code which uses the framework will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
Any functionality your plugin wants to expose to 'users' will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
```C#
public static class CoreInterfaceExtensions
{
@@ -48,12 +79,14 @@ Any functionality your plugin wants to expose to code which uses the framework w
}
```
If your plugin wants to access the functionality exposed by other plugins, then you can pass the argument `CoreInterface ci` to your plugin code in order to do so. (For example, if you want to start a Geth node, the Geth plugin adds `IGethNode StartGethNode(this CoreInterface ci, Action<IGethSetup> setup)` to the core interface.) Don't forget you'll need to add a project reference to each plugin project you wish to use.
While technically you can build whatever you like on top of the `CoreInterface` and your own plugin types, I recommend that you follow the approach explained below.
## Deploying, Wrapping, and Starting
When building a plugin, it is important to make as few assumptions as possible about how it will be used by whoever is going to use the framework. For this reason, I recommend you expose three kinds of methods using your `CoreInterface` extensions:
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning containers as a result. If your project requires additional information, you can create a new class type to contain both it and the containers created.
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain.
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning container objects as a result. If your project requires additional information, you can create a new class type to contain both it and the container objects created.
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain. (For example, if my project has a REST API call that allows users to fetch some state information, the object returned by Wrap should have a convenient method to call that API and receive that state information.)
1. Start - This kind of method does both, simply calling a Deploy method first, then a Wrap method, and returns the result.
Here's an example:
@@ -69,8 +102,8 @@ public static class CoreInterfaceExtensions
public static IMyProjectNode WrapMyProjectContainer(this CoreInterface ci, RunningContainers container)
{
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` tool to create an HTTP client for the container, then wrap it in an object that
// represents the API of your project.
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` to create an HTTP client for the container, then wrap it in an object that
// represents the API of your project, in this case 'IMyProjectNode'.
}
public static IMyProjectNode StartMyProject(this CoreInterface ci, string someArgument)
@@ -82,5 +115,44 @@ public static class CoreInterfaceExtensions
}
```
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
Should your deploy methods not return framework-types like RunningContainers, please make sure that your custom times are serializable. (Decorate them with the `[Serializable]` attribute.) Tools have been built using this framework which rely on the ability to serialize and store deployment information for later use. Please don't break this possibility. (Consider using the `SerializeGate` type to help ensure compatibility.)
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinuousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
## Container Recipes
In order to run a container of your application, the framework needs to know how to create that container. Think of a container recipe as being similar to a docker-compose.yaml file: You specify the docker image, ports, environment variables, persistent volumes, and secrets. However, container recipes are code. This allows you to add conditional behaviour to how your container is constructed. For example: The 'user' of your plugin specifies in their call input that they want to run your application in a certain mode. This would cause your container recipe to set certain environment variables, which cause the application to behave in the requested way.
### Addresses and ports
In a docker-compose.yaml file, it is perfectly normal to specify which ports should be exposed on your container. However, in the framework there's more to consider. When your application container starts, who knows on what kind of machine it runs, and what other processes it's sharing space with? Well, Kubernetes knows. Therefore, it is recommended that container recipes *do not* specify exact port numbers. The framework allows container recipes to declare "a port" without specifying its port number. This allows the framework and Kubernetes to figure out which ports are available when it's time to deploy. In order to find out which port numbers were assigned post-deployment, you can look up the port by tag (which is just an identifying string). When you specify a port to be mapped in your container recipe, you must specify:
1. `Tag` - An identifier.
1. `Internal` or `External` - Whether this port should be accessible only inside the cluster (for other containers (k8s: "ClusterIP")) or outside the cluster as well (for external tools/applications (k8s: "NodePort")).
1. `Protocol` - TCP or UDP. Both protocols on the same port is not universally supported by all container engines, and is therefore not supported by the framework.
If your application wants to listen for incoming traffic from inside its container, be sure to bind it to address "0.0.0.0".
Reminder: If you don't want to worry about addresses, and internal or external ports, you don't have to! The container objects returned by the `workflow` plugin tool have a method called `GetAddress`. Given a port tag, it returns and address object. The `Http` plugin tool can use that address object to set up connections.
## Locations
The framework is designed to allow you to control instances of your application in multiple (physical) locations. It accomplishes this by using kubernetes, and the ability to deploy containers to specific hosts (nodes) inside a kubernetes cluster. Since Kubernetes allows you to build clusters cross-site, this framework in theory enables you to deploy and interact with containers running anywhere.
The `workflow` plugin tool provides you a list of all available locations in the cluster. When starting a container, you are able to pick one of those locations. If no location is selected, one will be chosen by kubernetes. Locations can be chosen explicitly by kubernetes node name, or, they can be picked from the array of available locations.
Example:
```C#
{
var location = Ci.GetKnownLocations().Get("kbnode_euwest_paris1");
var codex = Ci.StartCodexNode(s => s.At(location));
}
```
In this example, 'Ci' is an instance of the core interface. The CodexPlugin exposes a function 'StartCodexNode', which allows its user to specify a location. This location is then passed to the `workflow` tool when the Codex plugin starts its container.
The available locations array guarantees that each entry corresponds to a different kubernetes host.
```C#
{
var knownLocations = Ci.GetKnownLocations();
// I don't care where exactly, as long as they are different locations.
var codexAtZero = Ci.StartCodexNode(s => s.At(knownLocations.Get(0)));
var codexAtOne = Ci.StartCodexNode(s => s.At(knownLocations.Get(1)));
}
```
+1 -1
View File
@@ -21,7 +21,7 @@ Do you want to write some tests using this distributed test setup? Great! Here's
1. When using the auto-bootstrap, you have no control over the bootstrap node from your tests. You can't (for example) shut it down during the course of the test. If you need this level of control for your scenario, use the `CodexDistTest` instead.
1. If your test needs a long time to run, add the `[UseLongTimeouts]` function attribute. This will greatly increase maximum time-out values for operations like for example uploading and downloading files.
### Continuous tests
1. Add new code files to `Tests/CodexContinousTests/Tests`
1. Add new code files to `Tests/CodexContinuousTests/Tests`
1. Inherrit from `ContinuousTest`
1. Define one or more methods and decorate them with the `[TestMoment(...)]` attribute.
1. The TestMoment takes a number of seconds as argument. Each moment will be executed by the continuous test runner applying the given seconds as delay. (Non-cumulative. So two moments at T:10 will be executed one after another without delay, in this case the order of execution should not be depended upon.)
+33 -158
View File
@@ -4,9 +4,8 @@ namespace ArgsUniform
{
public class ArgsUniform<T>
{
private readonly Assigner<T> assigner;
private readonly Action printAppInfo;
private readonly object? defaultsProvider;
private readonly IEnv.IEnv env;
private readonly string[] args;
private const int cliStart = 8;
private const int shortStart = 38;
@@ -31,9 +30,9 @@ namespace ArgsUniform
public ArgsUniform(Action printAppInfo, object defaultsProvider, IEnv.IEnv env, params string[] args)
{
this.printAppInfo = printAppInfo;
this.defaultsProvider = defaultsProvider;
this.env = env;
this.args = args;
assigner = new Assigner<T>(env, args, defaultsProvider);
}
public T Parse(bool printResult = false)
@@ -42,7 +41,7 @@ namespace ArgsUniform
{
printAppInfo();
PrintHelp();
throw new Exception();
Environment.Exit(0);
}
var result = Activator.CreateInstance<T>();
@@ -53,18 +52,16 @@ namespace ArgsUniform
var attr = uniformProperty.GetCustomAttribute<UniformAttribute>();
if (attr != null)
{
if (!UniformAssign(result, attr, uniformProperty) && attr.Required)
if (!assigner.UniformAssign(result, attr, uniformProperty) && attr.Required)
{
{
missingRequired.Add(uniformProperty);
}
missingRequired.Add(uniformProperty);
}
}
}
if (missingRequired.Any())
{
PrintResults(result, uniformProperties);
PrintResults(printResult,result, uniformProperties);
Print("");
foreach (var missing in missingRequired)
{
@@ -75,37 +72,39 @@ namespace ArgsUniform
}
PrintHelp();
throw new ArgumentException("Unable to assemble all required arguments");
Environment.Exit(1);
}
if (printResult)
{
PrintResults(result, uniformProperties);
}
PrintResults(printResult, result, uniformProperties);
return result;
}
private void PrintResults(T result, PropertyInfo[] uniformProperties)
{
Print("");
foreach (var p in uniformProperties)
{
Print($"\t{p.Name} = {p.GetValue(result)}");
}
Print("");
}
public void PrintHelp()
{
Print("");
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description");
var attrs = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).Select(p => p.GetCustomAttribute<UniformAttribute>()).Where(a => a != null).ToArray();
foreach (var attr in attrs)
PrintAligned("CLI option:", "(short)", "Environment variable:", "Description", "(default)");
var props = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).ToArray();
foreach (var prop in props)
{
var a = attr!;
var optional = !a.Required ? " *" : "";
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional);
var a = prop.GetCustomAttribute<UniformAttribute>();
if (a != null)
{
var optional = !a.Required ? " (optional)" : "";
var def = assigner.DescribeDefaultFor(prop);
PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional, $"({def})");
}
}
Print("");
}
private void PrintResults(bool printResult, T result, PropertyInfo[] uniformProperties)
{
if (!printResult) return;
Print("");
foreach (var p in uniformProperties)
{
Print($"\t{p.Name} = {p.GetValue(result)}");
}
Print("");
}
@@ -115,7 +114,7 @@ namespace ArgsUniform
Console.WriteLine(msg);
}
private void PrintAligned(string cli, string s, string env, string desc)
private void PrintAligned(string cli, string s, string env, string desc, string def)
{
Console.CursorLeft = cliStart;
Console.Write(cli);
@@ -124,132 +123,8 @@ namespace ArgsUniform
Console.CursorLeft = envStart;
Console.Write(env);
Console.CursorLeft = descStart;
Console.Write(desc + Environment.NewLine);
}
private object GetDefaultValue(Type t)
{
if (t.IsValueType) return Activator.CreateInstance(t)!;
return null!;
}
private bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
return false;
}
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
{
var currentValue = uniformProperty.GetValue(result);
var isEmptryString = (currentValue as string) == string.Empty;
if (currentValue != GetDefaultValue(uniformProperty.PropertyType) && !isEmptryString) return true;
if (defaultsProvider == null) return false;
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
if (defaultProperty == null) return false;
var value = defaultProperty.GetValue(defaultsProvider);
if (value != null)
{
return Assign(result, uniformProperty, value);
}
return false;
}
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
if (!string.IsNullOrEmpty(e))
{
return Assign(result, uniformProperty, e);
}
return false;
}
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var fromArg = GetFromArgs(attr.Arg);
if (fromArg != null)
{
return Assign(result, uniformProperty, fromArg);
}
var fromShort = GetFromArgs(attr.ArgShort);
if (fromShort != null)
{
return Assign(result, uniformProperty, fromShort);
}
return false;
}
private bool Assign(T result, PropertyInfo uniformProperty, object value)
{
if (uniformProperty.PropertyType == value.GetType())
{
uniformProperty.SetValue(result, value);
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
{
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
throw new NotSupportedException();
}
}
}
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
{
uniformProperty.SetValue(result, e);
return true;
}
return false;
}
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
{
if (int.TryParse(value.ToString(), out int i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
{
uniformProperty.SetValue(result, true);
}
return true;
}
private string? GetFromArgs(string key)
{
var argKey = $"--{key}=";
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
if (arg != null)
{
return arg.Substring(argKey.Length);
}
return null;
Console.Write(desc + " ");
Console.Write(def + Environment.NewLine);
}
}
}
+186
View File
@@ -0,0 +1,186 @@
using System.Globalization;
using System.Numerics;
using System.Reflection;
namespace ArgsUniform
{
public class Assigner<T>
{
private readonly IEnv.IEnv env;
private readonly string[] args;
private readonly object? defaultsProvider;
public Assigner(IEnv.IEnv env, string[] args, object? defaultsProvider)
{
this.env = env;
this.args = args;
this.defaultsProvider = defaultsProvider;
}
public bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true;
if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true;
if (AssignFromDefaultsIfAble(result, uniformProperty)) return true;
return false;
}
public string DescribeDefaultFor(PropertyInfo property)
{
var obj = Activator.CreateInstance<T>();
var defaultValue = GetDefaultValue(obj, property);
if (defaultValue == null) return "";
if (defaultValue is string str)
{
return "\"" + str + "\"";
}
return defaultValue.ToString() ?? string.Empty;
}
private object? GetDefaultValue(T result, PropertyInfo uniformProperty)
{
// Get value from object's static initializer if it's there.
var currentValue = uniformProperty.GetValue(result);
if (currentValue != null) return currentValue;
// Get value from defaults-provider object if it's there.
if (defaultsProvider == null) return null;
var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name);
if (defaultProperty == null) return null;
return defaultProperty.GetValue(defaultsProvider);
}
private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty)
{
var defaultValue = GetDefaultValue(result, uniformProperty);
var isEmptryString = (defaultValue as string) == string.Empty;
if (defaultValue != null && defaultValue != GetDefaultValueForType(uniformProperty.PropertyType) && !isEmptryString)
{
return Assign(result, uniformProperty, defaultValue);
}
return false;
}
private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty);
if (!string.IsNullOrEmpty(e))
{
return Assign(result, uniformProperty, e);
}
return false;
}
private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty)
{
var fromArg = GetFromArgs(attr.Arg);
if (fromArg != null)
{
return Assign(result, uniformProperty, fromArg);
}
var fromShort = GetFromArgs(attr.ArgShort);
if (fromShort != null)
{
return Assign(result, uniformProperty, fromShort);
}
return false;
}
private bool Assign(T result, PropertyInfo uniformProperty, object value)
{
if (uniformProperty.PropertyType == value.GetType())
{
uniformProperty.SetValue(result, value);
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
{
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
return true;
}
else
{
if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value);
if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(ulong)) return AssignUlong(result, uniformProperty, value);
if (uniformProperty.PropertyType == typeof(BigInteger)) return AssignBigInt(result, uniformProperty, value);
throw new NotSupportedException(
$"Unsupported property type '${uniformProperty.PropertyType}' " +
$"for property '${uniformProperty.Name}'.");
}
}
}
private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (Enum.TryParse(uniformProperty.PropertyType, s, out var e))
{
uniformProperty.SetValue(result, e);
return true;
}
return false;
}
private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value)
{
if (int.TryParse(value.ToString(), CultureInfo.InvariantCulture, out int i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private bool AssignUlong(T? result, PropertyInfo uniformProperty, object value)
{
if (ulong.TryParse(value.ToString(), CultureInfo.InvariantCulture, out ulong i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private bool AssignBigInt(T result, PropertyInfo uniformProperty, object value)
{
if (BigInteger.TryParse(value.ToString(), CultureInfo.InvariantCulture, out BigInteger i))
{
uniformProperty.SetValue(result, i);
return true;
}
return false;
}
private static bool AssignBool(T result, PropertyInfo uniformProperty, object value)
{
var s = value.ToString();
if (s == "1" || (s != null && s.ToLowerInvariant() == "true"))
{
uniformProperty.SetValue(result, true);
}
return true;
}
private string? GetFromArgs(string key)
{
var argKey = $"--{key}=";
var arg = args.FirstOrDefault(a => a.StartsWith(argKey));
if (arg != null)
{
return arg.Substring(argKey.Length);
}
return null;
}
private static object GetDefaultValueForType(Type t)
{
if (t.IsValueType) return Activator.CreateInstance(t)!;
return null!;
}
}
}
+5 -4
View File
@@ -1,4 +1,5 @@
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
namespace Core
{
@@ -29,11 +30,11 @@ namespace Core
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);
var msg = $"Downloading container log for '{container.Name}'";
entryPoint.Tools.GetLog().Log(msg);
var logHandler = new WriteToFileLogHandler(entryPoint.Tools.GetLog(), msg);
workflow.DownloadContainerLog(container, logHandler, tailLines);
return logHandler.DownloadLog();
return new DownloadedLog(logHandler);
}
public string ExecuteContainerCommand(IHasContainer containerSource, string command, params string[] args)
+26 -8
View File
@@ -1,10 +1,12 @@
using Logging;
using KubernetesWorkflow;
using Logging;
namespace Core
{
public interface IDownloadedLog
{
bool DoesLogContain(string expectedString);
void IterateLines(Action<string> action);
string[] GetLinesContaining(string expectedString);
string[] FindLinesThatContain(params string[] tags);
void DeleteFile();
}
@@ -13,12 +15,12 @@ namespace Core
{
private readonly LogFile logFile;
internal DownloadedLog(LogFile logFile)
internal DownloadedLog(WriteToFileLogHandler logHandler)
{
this.logFile = logFile;
logFile = logHandler.LogFile;
}
public bool DoesLogContain(string expectedString)
public void IterateLines(Action<string> action)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
@@ -26,12 +28,28 @@ namespace Core
var line = streamReader.ReadLine();
while (line != null)
{
if (line.Contains(expectedString)) return true;
action(line);
line = streamReader.ReadLine();
}
}
public string[] GetLinesContaining(string expectedString)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
var lines = new List<string>();
var line = streamReader.ReadLine();
while (line != null)
{
if (line.Contains(expectedString))
{
lines.Add(line);
}
line = streamReader.ReadLine();
}
//Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}");
return false;
return lines.ToArray(); ;
}
public string[] FindLinesThatContain(params string[] tags)
+193
View File
@@ -0,0 +1,193 @@
using Logging;
using Newtonsoft.Json;
using Serialization = Newtonsoft.Json.Serialization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Utils;
namespace Core
{
public interface IEndpoint
{
string HttpGetString(string route);
T HttpGetJson<T>(string route);
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
string HttpPostJson<TRequest>(string route, TRequest body);
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
T Deserialize<T>(string json);
}
internal class Endpoint : IEndpoint
{
private readonly ILog log;
private readonly IHttp http;
private readonly Address address;
private readonly string baseUrl;
private readonly string? logAlias;
public Endpoint(ILog log, IHttp http, Address address, string baseUrl, string? logAlias)
{
this.log = log;
this.http = http;
this.address = address;
this.baseUrl = baseUrl;
this.logAlias = logAlias;
}
public string HttpGetString(string route)
{
return http.OnClient(client =>
{
return GetString(client, route);
}, $"HTTP-GET:{route}");
}
public T HttpGetJson<T>(string route)
{
return http.OnClient(client =>
{
var json = GetString(client, route);
return Deserialize<T>(json);
}, $"HTTP-GET:{route}");
}
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{
return http.OnClient(client =>
{
var response = PostJson(client, route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(json);
}
Log(GetUrl() + route, json);
return Deserialize<TResponse>(json);
}, $"HTTP-POST-JSON: {route}");
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
return http.OnClient(client =>
{
var response = PostJson(client, route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}, $"HTTP-POST-JSON: {route}");
}
public TResponse HttpPostString<TResponse>(string route, string body)
{
return http.OnClient(client =>
{
var response = PostJsonString(client, route, body);
if (response == null) throw new Exception("Received no response.");
var result = Deserialize<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
}, $"HTTO-POST-JSON: {route}");
}
public string HttpPostStream(string route, Stream stream)
{
return http.OnClient(client =>
{
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STREAM: {route}");
}
public Stream HttpGetStream(string route)
{
return http.OnClient(client =>
{
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url));
}, $"HTTP-GET-STREAM: {route}");
}
public T Deserialize<T>(string json)
{
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate (object? sender, Serialization.ErrorEventArgs args)
{
if (args.CurrentObject == args.ErrorContext.OriginalObject)
{
errors.Add($"""
Member: '{args.ErrorContext.Member?.ToString() ?? "<null>"}'
Path: {args.ErrorContext.Path}
Error: {args.ErrorContext.Error.Message}
""");
args.ErrorContext.Handled = true;
}
}
});
if (errors.Count > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
}
else if (deserialized == null)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
}
return deserialized;
}
private string GetString(HttpClient client, string route)
{
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private HttpResponseMessage PostJson<TRequest>(HttpClient client, string route, TRequest body)
{
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}
private string PostJsonString(HttpClient client, string route, string body)
{
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
if (logAlias != null)
{
log.Debug($"({logAlias})({url}) = '{message}'", 3);
}
else
{
log.Debug($"({url}) = '{message}'", 3);
}
}
}
}
+7 -3
View File
@@ -38,10 +38,14 @@ namespace Core
return new CoreInterface(this);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
/// <summary>
/// Deletes kubernetes and tracked file resources.
/// when `waitTillDone` is true, this function will block until resources are deleted.
/// </summary>
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
{
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles);
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
}
internal T GetPlugin<T>() where T : IProjectPlugin
+35 -165
View File
@@ -1,202 +1,72 @@
using Logging;
using Newtonsoft.Json;
using Serialization = Newtonsoft.Json.Serialization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Utils;
namespace Core
{
public interface IHttp
{
string HttpGetString(string route);
T HttpGetJson<T>(string route);
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
string HttpPostJson<TRequest>(string route, TRequest body);
string HttpPostString(string route, string body);
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
T TryJsonDeserialize<T>(string json);
T OnClient<T>(Func<HttpClient, T> action);
T OnClient<T>(Func<HttpClient, T> action, string description);
T OnClient<T>(Func<HttpClient, T> action, Retry retry);
IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null);
}
internal class Http : IHttp
{
private static readonly object httpLock = new object();
private readonly ILog log;
private readonly ITimeSet timeSet;
private readonly Address address;
private readonly string baseUrl;
private readonly Action<HttpClient> onClientCreated;
private readonly string? logAlias;
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, string? logAlias = null)
: this(log, timeSet, address, baseUrl, DoNothing, logAlias)
internal Http(ILog log, ITimeSet timeSet)
: this(log, timeSet, DoNothing)
{
}
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
internal Http(ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
{
this.log = log;
this.timeSet = timeSet;
this.address = address;
this.baseUrl = baseUrl;
this.onClientCreated = onClientCreated;
this.logAlias = logAlias;
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
}
public string HttpGetString(string route)
public T OnClient<T>(Func<HttpClient, T> action)
{
return Retry(() =>
return OnClient(action, GetDescription());
}
public T OnClient<T>(Func<HttpClient, T> action, string description)
{
var retry = new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), f => { });
return OnClient(action, retry);
}
public T OnClient<T>(Func<HttpClient, T> action, Retry retry)
{
var client = GetClient();
return LockRetry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str; ;
}, $"HTTP-GET:{route}");
return action(client);
}, retry);
}
public T HttpGetJson<T>(string route)
public IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null)
{
var json = HttpGetString(route);
return TryJsonDeserialize<T>(json);
return new Endpoint(log, this, address, baseUrl, logAlias);
}
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
private string GetDescription()
{
var response = PostJson(route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
return DebugStack.GetCallerName(skipFrames: 2);
}
private T LockRetry<T>(Func<T> operation, Retry retry)
{
lock (httpLock)
{
throw new HttpRequestException(json);
return retry.Run(operation);
}
Log(GetUrl() + route, json);
return TryJsonDeserialize<TResponse>(json);
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
var response = PostJson(route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}
public string HttpPostString(string route, string body)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STRING: {route}");
}
public TResponse HttpPostString<TResponse>(string route, string body)
{
var response = HttpPostString(route, body);
if (response == null) throw new Exception("Received no response.");
var result = JsonConvert.DeserializeObject<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
}
public string HttpPostStream(string route, Stream stream)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STREAM: {route}");
}
public Stream HttpGetStream(string route)
{
return Retry(() =>
{
var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url));
}, $"HTTP-GET-STREAM: {route}");
}
public T TryJsonDeserialize<T>(string json)
{
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate(object? sender, Serialization.ErrorEventArgs args)
{
if (args.CurrentObject == args.ErrorContext.OriginalObject)
{
errors.Add($"""
Member: '{args.ErrorContext.Member?.ToString() ?? "<null>"}'
Path: {args.ErrorContext.Path}
Error: {args.ErrorContext.Error.Message}
""");
args.ErrorContext.Handled = true;
}
}
});
if (errors.Count() > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
}
else if (deserialized == null)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
}
return deserialized;
}
private HttpResponseMessage PostJson<TRequest>(string route, TRequest body)
{
return Retry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}, $"HTTP-POST-JSON: {route}");
}
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
if (logAlias != null)
{
log.Debug($"({logAlias})({url}) = '{message}'", 3);
}
else
{
log.Debug($"({url}) = '{message}'", 3);
}
}
private T Retry<T>(Func<T> operation, string description)
{
return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description);
}
private HttpClient GetClient()
-28
View File
@@ -1,28 +0,0 @@
using KubernetesWorkflow;
using Logging;
namespace Core
{
internal class LogDownloadHandler : LogHandler, ILogHandler
{
private readonly LogFile log;
internal LogDownloadHandler(string description, LogFile log)
{
this.log = log;
log.Write($"{description} -->> {log.FullFilename}");
log.WriteRaw(description);
}
internal IDownloadedLog DownloadLog()
{
return new DownloadedLog(log);
}
protected override void ProcessLine(string line)
{
log.WriteRaw(line);
}
}
}
+2 -2
View File
@@ -34,12 +34,12 @@
return metadata;
}
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles)
internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
{
foreach (var pair in pairs)
{
pair.Plugin.Decommission();
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles);
pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone);
}
}
+24 -18
View File
@@ -1,13 +1,18 @@
using FileUtils;
using KubernetesWorkflow;
using Logging;
using Utils;
namespace Core
{
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool
{
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles);
ITimeSet TimeSet { get; }
/// <summary>
/// Deletes kubernetes and tracked file resources.
/// when `waitTillDone` is true, this function will block until resources are deleted.
/// </summary>
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone);
}
public interface IWorkflowTool
@@ -22,9 +27,9 @@ namespace Core
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);
IHttp CreateHttp(Action<HttpClient> onClientCreated);
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp();
}
public interface IFileTool
@@ -34,37 +39,38 @@ namespace Core
internal class PluginTools : IPluginTools
{
private readonly ITimeSet timeSet;
private readonly WorkflowCreator workflowCreator;
private readonly IFileManager fileManager;
private ILog log;
private readonly LogPrefixer log;
internal PluginTools(ILog log, WorkflowCreator workflowCreator, string fileManagerRootFolder, ITimeSet timeSet)
{
this.log = log;
this.log = new LogPrefixer(log);
this.workflowCreator = workflowCreator;
this.timeSet = timeSet;
TimeSet = timeSet;
fileManager = new FileManager(log, fileManagerRootFolder);
}
public ITimeSet TimeSet { get; }
public void ApplyLogPrefix(string prefix)
{
log = new LogPrefixer(log, prefix);
log.Prefix = prefix;
}
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
public IHttp CreateHttp(Action<HttpClient> onClientCreated)
{
return CreateHttp(address, baseUrl, onClientCreated, timeSet, logAlias);
return CreateHttp(onClientCreated, TimeSet);
}
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet ts, string? logAlias = null)
public IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet ts)
{
return new Http(log, ts, address, baseUrl, onClientCreated, logAlias);
return new Http(log, ts, onClientCreated);
}
public IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null)
public IHttp CreateHttp()
{
return new Http(log, timeSet, address, baseUrl, logAlias);
return new Http(log, TimeSet);
}
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
@@ -72,9 +78,9 @@ namespace Core
return workflowCreator.CreateWorkflow(namespaceOverride);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone)
{
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace();
if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace(waitTillDone);
if (deleteTrackedFiles) fileManager.DeleteAllFiles();
}
+33 -12
View File
@@ -2,10 +2,31 @@
{
public interface ITimeSet
{
/// <summary>
/// Timeout for a single HTTP call.
/// </summary>
TimeSpan HttpCallTimeout();
int HttpMaxNumberOfRetries();
/// <summary>
/// Maximum total time to attempt to make a successful HTTP call to a service.
/// When HTTP calls time out during this timespan, retries will be made.
/// </summary>
TimeSpan HttpRetryTimeout();
/// <summary>
/// After a failed HTTP call, wait this long before trying again.
/// </summary>
TimeSpan HttpCallRetryDelay();
TimeSpan WaitForK8sServiceDelay();
/// <summary>
/// After a failed K8s operation, wait this long before trying again.
/// </summary>
TimeSpan K8sOperationRetryDelay();
/// <summary>
/// Maximum total time to attempt to perform a successful k8s operation.
/// If k8s operations fail during this timespan, retries will be made.
/// </summary>
TimeSpan K8sOperationTimeout();
}
@@ -16,9 +37,9 @@
return TimeSpan.FromMinutes(3);
}
public int HttpMaxNumberOfRetries()
public TimeSpan HttpRetryTimeout()
{
return 3;
return TimeSpan.FromMinutes(10);
}
public TimeSpan HttpCallRetryDelay()
@@ -26,7 +47,7 @@
return TimeSpan.FromSeconds(1);
}
public TimeSpan WaitForK8sServiceDelay()
public TimeSpan K8sOperationRetryDelay()
{
return TimeSpan.FromSeconds(10);
}
@@ -41,27 +62,27 @@
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromHours(2);
return TimeSpan.FromMinutes(30);
}
public int HttpMaxNumberOfRetries()
public TimeSpan HttpRetryTimeout()
{
return 1;
return TimeSpan.FromHours(2.2);
}
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(2);
return TimeSpan.FromSeconds(20);
}
public TimeSpan WaitForK8sServiceDelay()
public TimeSpan K8sOperationRetryDelay()
{
return TimeSpan.FromSeconds(10);
return TimeSpan.FromSeconds(30);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromMinutes(15);
return TimeSpan.FromHours(1);
}
}
}
+21
View File
@@ -0,0 +1,21 @@
using Utils;
namespace DiscordRewards
{
public class CheckConfig
{
public CheckType Type { get; set; }
public ulong MinNumberOfHosts { get; set; }
public ByteSize MinSlotSize { get; set; } = 0.Bytes();
public TimeSpan MinDuration { get; set; } = TimeSpan.Zero;
}
public enum CheckType
{
Uninitialized,
HostFilledSlot,
HostFinishedSlot,
ClientPostedContract,
ClientStartedContract,
}
}
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,31 @@
namespace DiscordRewards
{
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || Averages.Any() || EventsOverview.Any();
}
}
public class RewardUsersCommand
{
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
public class MarketAverage
{
public int NumberOfFinished { get; set; }
public int TimeRangeSeconds { get; set; }
public float Price { get; set; }
public float Size { get; set; }
public float Duration { get; set; }
public float Collateral { get; set; }
public float ProofProbability { get; set; }
}
}
+18
View File
@@ -0,0 +1,18 @@
namespace DiscordRewards
{
public class RewardConfig
{
public const string UsernameTag = "<USER>";
public RewardConfig(ulong roleId, string message, CheckConfig checkConfig)
{
RoleId = roleId;
Message = message;
CheckConfig = checkConfig;
}
public ulong RoleId { get; }
public string Message { get; }
public CheckConfig CheckConfig { get; }
}
}
+53
View File
@@ -0,0 +1,53 @@
using Utils;
namespace DiscordRewards
{
public class RewardRepo
{
private static string Tag => RewardConfig.UsernameTag;
public RewardConfig[] Rewards { get; } = new RewardConfig[]
{
// Filled any slot
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
{
Type = CheckType.HostFilledSlot
}),
// Finished any slot
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
{
Type = CheckType.HostFinishedSlot
}),
// Finished a sizable slot
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.HostFinishedSlot,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
}),
// Posted any contract
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
{
Type = CheckType.ClientPostedContract
}),
// Started any contract
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.ClientStartedContract
}),
// Started a sizable contract
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.ClientStartedContract,
MinNumberOfHosts = 4,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
})
};
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ namespace FileUtils
{
var sw = Stopwatch.Begin(log);
var result = GenerateRandomFile(size, label);
sw.End($"Generated file '{result.Describe()}'.");
sw.End($"Generated file {result.Describe()}.");
return result;
}
+6 -1
View File
@@ -37,6 +37,11 @@ namespace FileUtils
return $"'{Filename}'{sizePostfix}";
}
public ByteSize GetFilesize()
{
return new ByteSize(GetFileSize());
}
private void AssertEqual(TrackedFile? actual)
{
if (actual == null) FrameworkAssert.Fail("TestFile is null.");
@@ -60,7 +65,7 @@ namespace FileUtils
if (readExpected == 0 && readActual == 0)
{
log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'.");
log.Log($"OK: {Describe()} is equal to {actual.Describe()}.");
return;
}
+39
View File
@@ -0,0 +1,39 @@
using CodexContractsPlugin;
using GethPlugin;
using Logging;
namespace GethConnector
{
public class GethConnector
{
public IGethNode GethNode { get; }
public ICodexContracts CodexContracts { get; }
public static GethConnector? Initialize(ILog log)
{
if (!string.IsNullOrEmpty(GethInput.LoadError))
{
var msg = "Geth input incorrect: " + GethInput.LoadError;
log.Error(msg);
return null;
}
var contractsDeployment = new CodexContractsDeployment(
marketplaceAddress: GethInput.MarketplaceAddress,
abi: GethInput.ABI,
tokenAddress: GethInput.TokenAddress
);
var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment);
return new GethConnector(gethNode, contracts);
}
private GethConnector(IGethNode gethNode, ICodexContracts codexContracts)
{
GethNode = gethNode;
CodexContracts = codexContracts;
}
}
}
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
</ItemGroup>
</Project>
+52
View File
@@ -0,0 +1,52 @@
namespace GethConnector
{
public static class GethInput
{
private const string GethHostVar = "GETH_HOST";
private const string GethPortVar = "GETH_HTTP_PORT";
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
private const string AbiVar = "CODEXCONTRACTS_ABI";
static GethInput()
{
var error = new List<string>();
var gethHost = GetEnvVar(error, GethHostVar);
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
var privateKey = GetEnvVar(error, GethPrivKeyVar);
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
var tokenAddress = GetEnvVar(error, TokenAddressVar);
var abi = GetEnvVar(error, AbiVar);
if (error.Any())
{
LoadError = string.Join(", ", error);
}
else
{
GethHost = gethHost!;
GethPort = gethPort;
PrivateKey = privateKey!;
MarketplaceAddress = marketplaceAddress!;
TokenAddress = tokenAddress!;
ABI = abi!;
}
}
public static string GethHost { get; } = string.Empty;
public static int GethPort { get; }
public static string PrivateKey { get; } = string.Empty;
public static string MarketplaceAddress { get; } = string.Empty;
public static string TokenAddress { get; } = string.Empty;
public static string ABI { get; } = string.Empty;
public static string LoadError { get; } = string.Empty;
private static string? GetEnvVar(List<string> error, string name)
{
var result = Environment.GetEnvironmentVariable(name);
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
return result;
}
}
}
@@ -1,41 +0,0 @@
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;
}
}
}
@@ -6,17 +6,17 @@ namespace KubernetesWorkflow
{
private readonly K8sClient client;
private readonly string k8sNamespace;
private readonly RunningPod pod;
private readonly string podName;
private readonly string containerName;
private readonly string command;
private readonly string[] arguments;
private readonly List<string> lines = new List<string>();
public CommandRunner(K8sClient client, string k8sNamespace, RunningPod pod, string containerName, string command, string[] arguments)
public CommandRunner(K8sClient client, string k8sNamespace, string podName, string containerName, string command, string[] arguments)
{
this.client = client;
this.k8sNamespace = k8sNamespace;
this.pod = pod;
this.podName = podName;
this.containerName = containerName;
this.command = command;
this.arguments = arguments;
@@ -27,7 +27,7 @@ namespace KubernetesWorkflow
var input = new[] { command }.Concat(arguments).ToArray();
Time.Wait(client.Run(c => c.NamespacedPodExecAsync(
pod.PodInfo.Name, k8sNamespace, containerName, input, false, Callback, new CancellationToken())));
podName, k8sNamespace, containerName, input, false, Callback, new CancellationToken())));
}
public string GetStdOut()
+22 -19
View File
@@ -7,27 +7,29 @@ namespace KubernetesWorkflow
{
private readonly ILog log;
private readonly KubernetesClientConfiguration config;
private readonly string containerName;
private readonly string podName;
private readonly string recipeName;
private readonly string k8sNamespace;
private readonly RunningContainer container;
private ILogHandler? logHandler;
private CancellationTokenSource cts;
private Task? worker;
private Exception? workerException;
public CrashWatcher(ILog log, KubernetesClientConfiguration config, string k8sNamespace, RunningContainer container)
public CrashWatcher(ILog log, KubernetesClientConfiguration config, string containerName, string podName, string recipeName, string k8sNamespace)
{
this.log = log;
this.config = config;
this.containerName = containerName;
this.podName = podName;
this.recipeName = recipeName;
this.k8sNamespace = k8sNamespace;
this.container = container;
cts = new CancellationTokenSource();
}
public void Start(ILogHandler logHandler)
public void Start()
{
if (worker != null) throw new InvalidOperationException();
this.logHandler = logHandler;
cts = new CancellationTokenSource();
worker = Task.Run(Worker);
}
@@ -46,7 +48,9 @@ namespace KubernetesWorkflow
public bool HasContainerCrashed()
{
using var client = new Kubernetes(config);
return HasContainerBeenRestarted(client, container.Pod.PodInfo.Name);
var result = HasContainerBeenRestarted(client);
if (result) DownloadCrashedContainerLogs(client);
return result;
}
private void Worker()
@@ -66,30 +70,29 @@ namespace KubernetesWorkflow
using var client = new Kubernetes(config);
while (!token.IsCancellationRequested)
{
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(1));
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
var pod = container.Pod;
var recipe = container.Recipe;
var podName = pod.PodInfo.Name;
if (HasContainerBeenRestarted(client, podName))
if (HasContainerBeenRestarted(client))
{
DownloadCrashedContainerLogs(client, podName, recipe);
DownloadCrashedContainerLogs(client);
return;
}
}
}
private bool HasContainerBeenRestarted(Kubernetes client, string podName)
private bool HasContainerBeenRestarted(Kubernetes client)
{
var podInfo = client.ReadNamespacedPod(podName, k8sNamespace);
return podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
if (result) log.Log("Pod crash detected for " + containerName);
return result;
}
private void DownloadCrashedContainerLogs(Kubernetes client, string podName, ContainerRecipe recipe)
private void DownloadCrashedContainerLogs(Kubernetes client)
{
log.Log("Pod crash detected for " + container.Name);
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipe.Name, previous: true);
logHandler!.Log(stream);
using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true);
var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName);
handler.Log(stream);
}
}
}
+1 -12
View File
@@ -16,6 +16,7 @@ namespace KubernetesWorkflow
{
var config = GetConfig();
UpdateHostAddress(config);
config.SkipTlsVerify = true; // Required for operation on Wings cluster.
return config;
}
@@ -54,16 +55,4 @@ namespace KubernetesWorkflow
}
}
}
public class K8sNodeLabel
{
public K8sNodeLabel(string key, string value)
{
Key = key;
Value = value;
}
public string Key { get; }
public string Value { get; }
}
}
+386 -136
View File
@@ -1,5 +1,7 @@
using k8s;
using k8s.Models;
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
using Logging;
using Utils;
@@ -9,15 +11,14 @@ namespace KubernetesWorkflow
{
private readonly ILog log;
private readonly K8sCluster cluster;
private readonly KnownK8sPods knownPods;
private readonly WorkflowNumberSource workflowNumberSource;
private readonly K8sClient client;
public const string PodLabelKey = "pod-uuid";
public K8sController(ILog log, K8sCluster cluster, KnownK8sPods knownPods, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
public K8sController(ILog log, K8sCluster cluster, WorkflowNumberSource workflowNumberSource, string k8sNamespace)
{
this.log = log;
this.cluster = cluster;
this.knownPods = knownPods;
this.workflowNumberSource = workflowNumberSource;
client = new K8sClient(cluster.GetK8sClientConfig());
@@ -29,40 +30,59 @@ namespace KubernetesWorkflow
client.Dispose();
}
public RunningPod BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
public StartResult BringOnline(ContainerRecipe[] containerRecipes, ILocation location)
{
log.Debug();
EnsureNamespace();
var deploymentName = CreateDeployment(containerRecipes, location);
var (serviceName, servicePortsMap) = CreateService(containerRecipes);
var podInfo = FetchNewPod();
var podLabel = K8sNameUtils.Format(Guid.NewGuid().ToString());
var deployment = CreateDeployment(containerRecipes, location, podLabel);
var internalService = CreateInternalService(containerRecipes);
var externalService = CreateExternalService(containerRecipes);
return new RunningPod(cluster, podInfo, deploymentName, serviceName, servicePortsMap.ToArray());
return new StartResult(cluster, containerRecipes, deployment, internalService, externalService);
}
public void Stop(RunningPod pod)
public void WaitUntilOnline(RunningContainer container)
{
log.Debug();
if (!string.IsNullOrEmpty(pod.ServiceName)) DeleteService(pod.ServiceName);
DeleteDeployment(pod.DeploymentName);
WaitUntilDeploymentOffline(pod.DeploymentName);
WaitUntilPodOffline(pod.PodInfo.Name);
WaitUntilDeploymentOnline(container);
}
public void DownloadPodLog(RunningPod pod, ContainerRecipe recipe, ILogHandler logHandler, int? tailLines)
public PodInfo GetPodInfo(RunningDeployment deployment)
{
var pod = GetPodForDeployment(deployment);
return CreatePodInfo(pod);
}
public void Stop(StartResult startResult, bool waitTillStopped)
{
log.Debug();
using var stream = client.Run(c => c.ReadNamespacedPodLog(pod.PodInfo.Name, K8sNamespace, recipe.Name, tailLines: tailLines));
if (startResult.InternalService != null) DeleteService(startResult.InternalService);
if (startResult.ExternalService != null) DeleteService(startResult.ExternalService);
DeleteDeployment(startResult.Deployment);
if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
}
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines, bool? previous)
{
log.Debug();
var podName = GetPodName(container);
var recipeName = container.Recipe.Name;
using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines, previous: previous));
logHandler.Log(stream);
}
public string ExecuteCommand(RunningPod pod, string containerName, string command, params string[] args)
public string ExecuteCommand(RunningContainer container, string command, params string[] args)
{
var containerName = container.Recipe.Name;
var cmdAndArgs = $"{containerName}: {command} ({string.Join(",", args)})";
log.Debug(cmdAndArgs);
var runner = new CommandRunner(client, K8sNamespace, pod, containerName, command, args);
var podName = GetPodName(container);
var runner = new CommandRunner(client, K8sNamespace, podName, containerName, command, args);
runner.Run();
var result = runner.GetStdOut();
@@ -70,7 +90,32 @@ namespace KubernetesWorkflow
return result;
}
public void DeleteAllNamespacesStartingWith(string prefix)
public int[] GetUsedExternalPorts()
{
return client.Run(c =>
{
var result = new List<int>();
var services = c.ListServiceForAllNamespaces();
var nodePorts = services.Items.Where(s => s.Spec.Type == "NodePort").ToArray();
if (!nodePorts.Any()) return result.ToArray();
foreach (var service in nodePorts)
{
foreach (var port in service.Spec.Ports)
{
if (port.NodePort.HasValue)
{
result.Add(port.NodePort.Value);
}
}
}
return result.ToArray();
});
}
public void DeleteAllNamespacesStartingWith(string prefix, bool wait)
{
log.Debug();
@@ -79,30 +124,28 @@ namespace KubernetesWorkflow
foreach (var ns in namespaces)
{
DeleteNamespace(ns);
}
foreach (var ns in namespaces)
{
WaitUntilNamespaceDeleted(ns);
DeleteNamespace(ns, wait);
}
}
public void DeleteNamespace()
public void DeleteNamespace(bool wait)
{
log.Debug();
if (IsNamespaceOnline(K8sNamespace))
{
client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0));
if (wait) WaitUntilNamespaceDeleted(K8sNamespace);
}
WaitUntilNamespaceDeleted();
}
public void DeleteNamespace(string ns)
public void DeleteNamespace(string ns, bool wait)
{
log.Debug();
if (IsNamespaceOnline(ns))
{
client.Run(c => c.DeleteNamespace(ns, null, null, gracePeriodSeconds: 0));
if (wait) WaitUntilNamespaceDeleted(ns);
}
}
@@ -304,7 +347,7 @@ namespace KubernetesWorkflow
#region Deployment management
private string CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location)
private RunningDeployment CreateDeployment(ContainerRecipe[] containerRecipes, ILocation location, string podLabel)
{
var deploymentSpec = new V1Deployment
{
@@ -321,11 +364,13 @@ namespace KubernetesWorkflow
{
Metadata = new V1ObjectMeta
{
Labels = GetSelector(containerRecipes),
Labels = GetSelector(containerRecipes, podLabel),
Annotations = GetAnnotations(containerRecipes)
},
Spec = new V1PodSpec
{
PriorityClassName = GetPriorityClassName(containerRecipes),
Affinity = CreatePodAffinity(containerRecipes),
NodeSelector = CreateNodeSelector(location),
Containers = CreateDeploymentContainers(containerRecipes),
Volumes = CreateVolumes(containerRecipes)
@@ -335,15 +380,15 @@ namespace KubernetesWorkflow
};
client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sNamespace));
WaitUntilDeploymentOnline(deploymentSpec.Metadata.Name);
return deploymentSpec.Metadata.Name;
var name = deploymentSpec.Metadata.Name;
return new RunningDeployment(name, podLabel);
}
private void DeleteDeployment(string deploymentName)
private void DeleteDeployment(RunningDeployment deployment)
{
client.Run(c => c.DeleteNamespacedDeployment(deploymentName, K8sNamespace));
WaitUntilDeploymentOffline(deploymentName);
client.Run(c => c.DeleteNamespacedDeployment(deployment.Name, K8sNamespace));
WaitUntilDeploymentOffline(deployment.Name);
}
private IDictionary<string, string> CreateNodeSelector(ILocation location)
@@ -357,17 +402,69 @@ namespace KubernetesWorkflow
};
}
private V1Affinity? CreatePodAffinity(ContainerRecipe[] recipes)
{
var notIns = recipes
.Select(r => r.SchedulingAffinity.NotIn)
.Where(n => !string.IsNullOrEmpty(n))
.Distinct()
.ToList();
if (!notIns.Any()) return null;
return new V1Affinity
{
NodeAffinity = new V1NodeAffinity
{
RequiredDuringSchedulingIgnoredDuringExecution = new V1NodeSelector
{
NodeSelectorTerms = new List<V1NodeSelectorTerm>
{
new V1NodeSelectorTerm
{
MatchExpressions = new List<V1NodeSelectorRequirement>
{
new V1NodeSelectorRequirement
{
Key = "allow-tests-pods",
OperatorProperty = "NotIn",
Values = notIns
}
}
}
}
}
}
};
}
private K8sNodeLabel? GetNodeLabelForLocation(ILocation location)
{
var l = (Location)location;
return l.NodeLabel;
}
private string GetPriorityClassName(ContainerRecipe[] containerRecipes)
{
if (containerRecipes.Any(c => c.SetCriticalPriority))
{
return "system-node-critical";
}
return null!;
}
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes)
{
return containerRecipes.First().PodLabels.GetLabels();
}
private IDictionary<string, string> GetSelector(ContainerRecipe[] containerRecipes, string podLabel)
{
var labels = containerRecipes.First().PodLabels.Clone();
labels.Add(PodLabelKey, podLabel);
return labels.GetLabels();
}
private IDictionary<string, string> GetRunnerNamespaceSelector()
{
return new Dictionary<string, string> { { "kubernetes.io/metadata.name", "default" } };
@@ -409,10 +506,17 @@ namespace KubernetesWorkflow
Ports = CreateContainerPorts(recipe),
Env = CreateEnv(recipe),
VolumeMounts = CreateContainerVolumeMounts(recipe),
Resources = CreateResourceLimits(recipe)
Resources = CreateResourceLimits(recipe),
Command = CreateCommandList(recipe)
};
}
private IList<string> CreateCommandList(ContainerRecipe recipe)
{
if (recipe.CommandOverride == null || !recipe.CommandOverride.Command.Any()) return null!;
return recipe.CommandOverride.Command.ToList();
}
private V1ResourceRequirements CreateResourceLimits(ContainerRecipe recipe)
{
return new V1ResourceRequirements
@@ -431,7 +535,7 @@ namespace KubernetesWorkflow
}
if (set.Memory.SizeInBytes != 0)
{
result.Add("memory", new ResourceQuantity(set.Memory.ToSuffixNotation()));
result.Add("memory", new ResourceQuantity(set.Memory.SizeInBytes.ToString()));
}
return result;
}
@@ -446,7 +550,8 @@ namespace KubernetesWorkflow
return new V1VolumeMount
{
Name = v.VolumeName,
MountPath = v.MountPath
MountPath = v.MountPath,
SubPath = v.SubPath,
};
}
@@ -462,28 +567,28 @@ namespace KubernetesWorkflow
private V1Volume CreateVolume(VolumeMount v)
{
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
CreatePersistentVolumeClaimIfNeeded(v);
if (!string.IsNullOrEmpty(v.HostPath))
{
ApiVersion = "v1",
Metadata = new V1ObjectMeta
return new V1Volume
{
Name = v.VolumeName
},
Spec = new V1PersistentVolumeClaimSpec
{
AccessModes = new List<string>
Name = v.VolumeName,
HostPath = new V1HostPathVolumeSource
{
"ReadWriteOnce"
},
Resources = new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>
{
{"storage", new ResourceQuantity(v.ResourceQuantity) }
}
Path = v.HostPath
}
}
}, K8sNamespace));
};
}
if (!string.IsNullOrEmpty(v.Secret))
{
return new V1Volume
{
Name = v.VolumeName,
Secret = CreateVolumeSecret(v)
};
}
return new V1Volume
{
@@ -495,6 +600,50 @@ namespace KubernetesWorkflow
};
}
private void CreatePersistentVolumeClaimIfNeeded(VolumeMount v)
{
var pvcs = client.Run(c => c.ListNamespacedPersistentVolumeClaim(K8sNamespace));
if (pvcs != null && pvcs.Items.Any(i => i.Name() == v.VolumeName)) return;
client.Run(c => c.CreateNamespacedPersistentVolumeClaim(new V1PersistentVolumeClaim
{
ApiVersion = "v1",
Metadata = new V1ObjectMeta
{
Name = v.VolumeName,
},
Spec = new V1PersistentVolumeClaimSpec
{
AccessModes = new List<string>
{
"ReadWriteOnce"
},
Resources = CreateVolumeResourceRequirements(v),
},
}, K8sNamespace));
}
private V1SecretVolumeSource CreateVolumeSecret(VolumeMount v)
{
if (string.IsNullOrWhiteSpace(v.Secret)) return null!;
return new V1SecretVolumeSource
{
SecretName = v.Secret
};
}
private V1ResourceRequirements CreateVolumeResourceRequirements(VolumeMount v)
{
if (v.ResourceQuantity == null) return null!;
return new V1ResourceRequirements
{
Requests = new Dictionary<string, ResourceQuantity>()
{
{"storage", new ResourceQuantity(v.ResourceQuantity) }
}
};
}
private List<V1EnvVar> CreateEnv(ContainerRecipe recipe)
{
return recipe.EnvVars.Select(CreateEnvVar).ToList();
@@ -511,157 +660,256 @@ namespace KubernetesWorkflow
private List<V1ContainerPort> CreateContainerPorts(ContainerRecipe recipe)
{
var exposedPorts = recipe.ExposedPorts.Select(p => CreateContainerPort(recipe, p));
var internalPorts = recipe.InternalPorts.Select(p => CreateContainerPort(recipe, p));
var exposedPorts = recipe.ExposedPorts.SelectMany(p => CreateContainerPort(recipe, p));
var internalPorts = recipe.InternalPorts.SelectMany(p => CreateContainerPort(recipe, p));
return exposedPorts.Concat(internalPorts).ToList();
}
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port)
private List<V1ContainerPort> CreateContainerPort(ContainerRecipe recipe, Port port)
{
var result = new List<V1ContainerPort>();
if (port.IsTcp()) CreateTcpContainerPort(result, recipe, port);
if (port.IsUdp()) CreateUdpContainerPort(result, recipe, port);
return result;
}
private void CreateUdpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
{
result.Add(CreateContainerPort(recipe, port, "UDP"));
}
private void CreateTcpContainerPort(List<V1ContainerPort> result, ContainerRecipe recipe, Port port)
{
result.Add(CreateContainerPort(recipe, port, "TCP"));
}
private V1ContainerPort CreateContainerPort(ContainerRecipe recipe, Port port, string protocol)
{
return new V1ContainerPort
{
Name = GetNameForPort(recipe, port),
ContainerPort = port.Number
ContainerPort = port.Number,
Protocol = protocol
};
}
private string GetNameForPort(ContainerRecipe recipe, Port port)
{
return $"p{workflowNumberSource.WorkflowNumber}-{recipe.Number}-{port.Number}";
var inputs = new[]
{
$"p{workflowNumberSource.WorkflowNumber}",
recipe.Number.ToString(),
port.Number.ToString(),
port.Protocol.ToString().ToLowerInvariant()
};
return K8sNameUtils.FormatPortName(string.Join(",", inputs));
}
private string GetPodName(RunningContainer container)
{
return GetPodForDeployment(container.RunningPod.StartResult.Deployment).Metadata.Name;
}
private V1Pod GetPodForDeployment(RunningDeployment deployment)
{
return Time.Retry(() => GetPodForDeplomentInternal(deployment),
// We will wait up to 1 minute, k8s might be moving pods around.
maxTimeout: TimeSpan.FromMinutes(1),
retryTime: TimeSpan.FromSeconds(10),
description: "Find pod by label for deployment.");
}
private V1Pod GetPodForDeplomentInternal(RunningDeployment deployment)
{
var allPods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
var pods = allPods.Items.Where(p => p.GetLabel(PodLabelKey) == deployment.PodLabel).ToArray();
if (pods.Length != 1)
{
var allLabels = allPods.Items.Select(p =>
{
var labels = string.Join(",", p.Labels().Select(l => $"{l.Key}={l.Value}"));
return $"pod:'{p.Name()}' has labels: [{labels}]";
});
throw new Exception($"Expected to find 1 pod by podLabel '{deployment.PodLabel}'. Found: {pods.Length}. " +
$"Total number of pods: {allPods.Items.Count}. Their labels: {string.Join(Environment.NewLine, allLabels)}");
}
return pods[0];
}
#endregion
#region Service management
private (string, List<ContainerRecipePortMapEntry>) CreateService(ContainerRecipe[] containerRecipes)
private RunningService? CreateInternalService(ContainerRecipe[] recipes)
{
var result = new List<ContainerRecipePortMapEntry>();
return CreateService(recipes, r => r.InternalPorts.Concat(r.ExposedPorts).ToArray(), "ClusterIP", "int", false);
}
var ports = CreateServicePorts(containerRecipes);
private RunningService? CreateExternalService(ContainerRecipe[] recipes)
{
return CreateService(recipes, r => r.ExposedPorts, "NodePort", "ext", true);
}
if (!ports.Any())
{
// None of these container-recipes wish to expose anything via a service port.
// So, we don't have to create a service.
return (string.Empty, result);
}
private RunningService? CreateService(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, string serviceType, string namePostfix, bool isNodePort)
{
var ports = CreateServicePorts(recipes, portSelector, isNodePort);
if (!ports.Any()) return null;
var serviceSpec = new V1Service
{
ApiVersion = "v1",
Metadata = CreateServiceMetadata(),
Metadata = CreateServiceMetadata(recipes, namePostfix),
Spec = new V1ServiceSpec
{
Type = "NodePort",
Selector = GetSelector(containerRecipes),
Ports = ports
Type = serviceType,
Selector = GetSelector(recipes),
Ports = ports,
}
};
client.Run(c => c.CreateNamespacedService(serviceSpec, K8sNamespace));
ReadBackServiceAndMapPorts(serviceSpec, containerRecipes, result);
var result = ReadBackServiceAndMapPorts(serviceSpec, recipes);
var name = serviceSpec.Metadata.Name;
return (serviceSpec.Metadata.Name, result);
return new RunningService(name, result);
}
private void ReadBackServiceAndMapPorts(V1Service serviceSpec, ContainerRecipe[] containerRecipes, List<ContainerRecipePortMapEntry> result)
private List<ContainerRecipePortMapEntry> ReadBackServiceAndMapPorts(V1Service serviceSpec, ContainerRecipe[] containerRecipes)
{
// For each container-recipe, we need to figure out which service-ports it was assigned by K8s.
var result = new List<ContainerRecipePortMapEntry>();
// For each container-recipe-port, we need to figure out which service-ports it was assigned by K8s.
var readback = client.Run(c => c.ReadNamespacedService(serviceSpec.Metadata.Name, K8sNamespace));
foreach (var r in containerRecipes)
{
foreach (var port in r.ExposedPorts)
var recipePorts = r.ExposedPorts.Concat(r.InternalPorts).ToArray();
foreach (var port in recipePorts)
{
var portName = GetNameForPort(r, port);
var matchingServicePorts = readback.Spec.Ports.Where(p => p.Name == portName);
if (matchingServicePorts.Any())
{
// These service ports belongs to this recipe.
var optionals = matchingServicePorts.Select(p => MapNodePortIfAble(p, port.Tag));
var ports = optionals.Where(p => p != null).Select(p => p!).ToArray();
var ports = matchingServicePorts.Select(p => MapPortIfAble(p, port.Tag, port.Protocol)).ToArray();
if (ports.Any())
{
result.Add(new ContainerRecipePortMapEntry(r.Number, ports));
log.Debug($"Service Readback: {portName} found: {string.Join(",", ports.Select(p => p.ToString()))}");
}
}
}
return result;
}
private Port? MapNodePortIfAble(V1ServicePort p, string tag)
private Port MapPortIfAble(V1ServicePort p, string tag, PortProtocol protocol)
{
if (p.NodePort == null) return null;
return new Port(p.NodePort.Value, tag);
if (p.NodePort != null) return new Port(p.NodePort.Value, tag, protocol);
if (p.Port > 0) return new Port(p.Port, tag, protocol);
throw new Exception("Unable to map port.");
}
private void DeleteService(string serviceName)
private void DeleteService(RunningService service)
{
client.Run(c => c.DeleteNamespacedService(serviceName, K8sNamespace));
client.Run(c => c.DeleteNamespacedService(service.Name, K8sNamespace));
}
private V1ObjectMeta CreateServiceMetadata()
private V1ObjectMeta CreateServiceMetadata(ContainerRecipe[] containerRecipes, string namePostfix)
{
var recipeName = containerRecipes.First().Name;
var name = K8sNameUtils.Format($"{recipeName}-{workflowNumberSource.WorkflowNumber}-{namePostfix}");
log.Debug("Creating service: " + name);
return new V1ObjectMeta
{
Name = "service-" + workflowNumberSource.WorkflowNumber,
NamespaceProperty = K8sNamespace
Name = name,
NamespaceProperty = K8sNamespace,
};
}
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes)
private List<V1ServicePort> CreateServicePorts(ContainerRecipe[] recipes, Func<ContainerRecipe, Port[]> portSelector, bool isNodePort)
{
var result = new List<V1ServicePort>();
foreach (var recipe in recipes)
{
result.AddRange(CreateServicePorts(recipe));
var ports = portSelector(recipe);
foreach (var port in ports)
{
result.AddRange(CreateServicePorts(recipe, port, isNodePort));
}
}
return result;
}
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe)
private List<V1ServicePort> CreateServicePorts(ContainerRecipe recipe, Port recipePort, bool isNodePort)
{
var result = new List<V1ServicePort>();
foreach (var port in recipe.ExposedPorts)
{
result.Add(new V1ServicePort
{
Name = GetNameForPort(recipe, port),
Protocol = "TCP",
Port = port.Number,
TargetPort = GetNameForPort(recipe, port),
});
}
if (recipePort.IsTcp()) CreateServicePort(result, recipe, recipePort, "TCP", isNodePort);
if (recipePort.IsUdp()) CreateServicePort(result, recipe, recipePort, "UDP", isNodePort);
return result;
}
private void CreateServicePort(List<V1ServicePort> result, ContainerRecipe recipe, Port port, string protocol, bool isNodePort)
{
var p = new V1ServicePort
{
Name = GetNameForPort(recipe, port),
Protocol = protocol,
Port = port.Number,
TargetPort = GetNameForPort(recipe, port)
};
if (isNodePort) p.NodePort = port.Number;
result.Add(p);
}
#endregion
#region Waiting
private void WaitUntilNamespaceCreated()
{
WaitUntil(() => IsNamespaceOnline(K8sNamespace));
WaitUntil(() => IsNamespaceOnline(K8sNamespace), nameof(WaitUntilNamespaceCreated));
}
private void WaitUntilNamespaceDeleted()
private void WaitUntilNamespaceDeleted(string @namespace)
{
WaitUntil(() => !IsNamespaceOnline(K8sNamespace));
WaitUntil(() => !IsNamespaceOnline(@namespace), nameof(WaitUntilNamespaceDeleted));
}
private void WaitUntilNamespaceDeleted(string name)
{
WaitUntil(() => !IsNamespaceOnline(name));
}
private void WaitUntilDeploymentOnline(string deploymentName)
private void WaitUntilDeploymentOnline(RunningContainer container)
{
WaitUntil(() =>
{
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace));
CheckForCrash(container);
var deployment = client.Run(c => c.ReadNamespacedDeployment(container.Recipe.Name, K8sNamespace));
return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0;
});
}, nameof(WaitUntilDeploymentOnline));
}
private void CheckForCrash(RunningContainer container)
{
var deploymentName = container.Recipe.Name;
var podName = GetPodName(container);
var podInfo = client.Run(c => c.ReadNamespacedPod(podName, K8sNamespace));
if (podInfo == null) return;
if (podInfo.Status == null) return;
if (podInfo.Status.ContainerStatuses == null) return;
var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0);
if (result)
{
var msg = $"Pod crash detected for deployment {deploymentName} (pod:{podName})";
log.Error(msg);
DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true);
throw new Exception(msg);
}
}
private void WaitUntilDeploymentOffline(string deploymentName)
@@ -671,29 +919,28 @@ namespace KubernetesWorkflow
var deployments = client.Run(c => c.ListNamespacedDeployment(K8sNamespace));
var deployment = deployments.Items.SingleOrDefault(d => d.Metadata.Name == deploymentName);
return deployment == null || deployment.Status.AvailableReplicas == 0;
});
}, nameof(WaitUntilDeploymentOffline));
}
private void WaitUntilPodOffline(string podName)
private void WaitUntilPodsForDeploymentAreOffline(RunningDeployment deployment)
{
WaitUntil(() =>
{
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace)).Items;
var pod = pods.SingleOrDefault(p => p.Metadata.Name == podName);
return pod == null;
});
var pods = FindPodsByLabel(deployment.PodLabel);
return !pods.Any();
}, nameof(WaitUntilPodsForDeploymentAreOffline));
}
private void WaitUntil(Func<bool> predicate)
private void WaitUntil(Func<bool> predicate, string msg)
{
var sw = Stopwatch.Begin(log, true);
try
{
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay());
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay(), msg);
}
finally
{
sw.End("", 1);
sw.End(msg, 1);
}
}
@@ -701,25 +948,28 @@ namespace KubernetesWorkflow
public CrashWatcher CreateCrashWatcher(RunningContainer container)
{
return new CrashWatcher(log, cluster.GetK8sClientConfig(), K8sNamespace, container);
var containerName = container.Name;
var podName = GetPodName(container);
var recipeName = container.Recipe.Name;
return new CrashWatcher(log, cluster.GetK8sClientConfig(), containerName, podName, recipeName, K8sNamespace);
}
private PodInfo FetchNewPod()
private V1Pod[] FindPodsByLabel(string podLabel)
{
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace)).Items;
var pods = client.Run(c => c.ListNamespacedPod(K8sNamespace));
return pods.Items.Where(p => p.GetLabel(PodLabelKey) == podLabel).ToArray();
}
var newPods = pods.Where(p => !knownPods.Contains(p.Name())).ToArray();
if (newPods.Length != 1) throw new InvalidOperationException("Expected only 1 pod to be created. Test infra failure.");
var newPod = newPods.Single();
var name = newPod.Name();
var ip = newPod.Status.PodIP;
var k8sNodeName = newPod.Spec.NodeName;
private PodInfo CreatePodInfo(V1Pod pod)
{
var name = pod.Name();
var ip = pod.Status.PodIP;
var k8sNodeName = pod.Spec.NodeName;
if (string.IsNullOrEmpty(name)) throw new InvalidOperationException("Invalid pod name received. Test infra failure.");
if (string.IsNullOrEmpty(ip)) throw new InvalidOperationException("Invalid pod IP received. Test infra failure.");
knownPods.Add(name);
return new PodInfo(name, ip, k8sNodeName);
}
}
+8 -5
View File
@@ -1,19 +1,22 @@
namespace KubernetesWorkflow
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
namespace KubernetesWorkflow
{
public interface IK8sHooks
{
void OnContainersStarted(RunningContainers runningContainers);
void OnContainersStopped(RunningContainers runningContainers);
void OnContainersStarted(RunningPod runningPod);
void OnContainersStopped(RunningPod runningPod);
void OnContainerRecipeCreated(ContainerRecipe recipe);
}
public class DoNothingK8sHooks : IK8sHooks
{
public void OnContainersStarted(RunningContainers runningContainers)
public void OnContainersStarted(RunningPod runningPod)
{
}
public void OnContainersStopped(RunningContainers runningContainers)
public void OnContainersStopped(RunningPod runningPod)
{
}
+12 -1
View File
@@ -3,8 +3,19 @@
public static class K8sNameUtils
{
public static string Format(string s)
{
return Format(s, 62);
}
public static string FormatPortName(string s)
{
return Format(s, 15);
}
private static string Format(string s, int maxLength)
{
var result = s.ToLowerInvariant()
.Replace("_", "-")
.Replace(" ", "-")
.Replace(":", "-")
.Replace("/", "-")
@@ -14,7 +25,7 @@
.Replace(",", "-");
result = result.Trim('-');
if (result.Length > 62) result = result.Substring(0, 62);
if (result.Length > maxLength) result = result.Substring(0, maxLength);
return result;
}
@@ -1,17 +0,0 @@
namespace KubernetesWorkflow
{
public class KnownK8sPods
{
private readonly List<string> knownActivePodNames = new List<string>();
public bool Contains(string name)
{
return knownActivePodNames.Contains(name);
}
public void Add(string name)
{
knownActivePodNames.Add(name);
}
}
}
+3 -1
View File
@@ -1,4 +1,6 @@
namespace KubernetesWorkflow
using KubernetesWorkflow.Types;
namespace KubernetesWorkflow
{
public interface ILocation
{
@@ -1,4 +1,5 @@
using Logging;
using KubernetesWorkflow.Types;
using Logging;
namespace KubernetesWorkflow
{
@@ -0,0 +1,46 @@
using Logging;
namespace KubernetesWorkflow
{
public interface ILogHandler
{
void Log(Stream log);
}
public abstract class LogHandler : ILogHandler
{
public void Log(Stream log)
{
using var reader = new StreamReader(log);
var line = reader.ReadLine();
while (line != null)
{
ProcessLine(line);
line = reader.ReadLine();
}
}
protected abstract void ProcessLine(string line);
}
public class WriteToFileLogHandler : LogHandler, ILogHandler
{
public WriteToFileLogHandler(ILog sourceLog, string description)
{
LogFile = sourceLog.CreateSubfile();
var msg = $"{description} -->> {LogFile.FullFilename}";
sourceLog.Log(msg);
LogFile.Write(msg);
LogFile.WriteRaw(description);
}
public LogFile LogFile { get; }
protected override void ProcessLine(string line)
{
LogFile.WriteRaw(line);
}
}
}
@@ -0,0 +1,7 @@
namespace KubernetesWorkflow
{
public static class PublicIpService
{
public static string Address { get; } = "ip.codex.storage";
}
}
@@ -0,0 +1,12 @@
namespace KubernetesWorkflow.Recipe
{
public class CommandOverride
{
public CommandOverride(params string[] command)
{
Command = command;
}
public string[] Command { get; }
}
}
@@ -1,6 +1,6 @@
using Newtonsoft.Json.Linq;
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class ContainerAdditionals
{
@@ -20,7 +20,7 @@ namespace KubernetesWorkflow
{
var typeName = GetTypeName(typeof(T));
var userData = Additionals.SingleOrDefault(a => a.Type == typeName);
if (userData == null) return default(T);
if (userData == null) return default;
var jobject = (JObject)userData.UserData;
return jobject.ToObject<T>();
}
@@ -1,13 +1,16 @@
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class ContainerRecipe
{
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
{
Number = number;
NameOverride = nameOverride;
Image = image;
Resources = resources;
SchedulingAffinity = schedulingAffinity;
CommandOverride = commandOverride;
SetCriticalPriority = setCriticalPriority;
ExposedPorts = exposedPorts;
InternalPorts = internalPorts;
EnvVars = envVars;
@@ -32,6 +35,9 @@
public int Number { get; }
public string? NameOverride { get; }
public ContainerResources Resources { get; }
public SchedulingAffinity SchedulingAffinity { get; }
public CommandOverride CommandOverride { get; }
public bool SetCriticalPriority { get; }
public string Image { get; }
public Port[] ExposedPorts { get; }
public Port[] InternalPorts { get; }
@@ -53,28 +59,52 @@
$"internalPorts: {string.Join(",", InternalPorts.Select(p => p.Number))}, " +
$"envVars: {string.Join(",", EnvVars.Select(v => v.ToString()))}, " +
$"limits: {Resources}, " +
$"affinity: {SchedulingAffinity}, " +
$"volumes: {string.Join(",", Volumes.Select(v => $"'{v.MountPath}'"))}";
}
}
public class Port
{
public Port(int number, string tag)
public Port(int number, string tag, PortProtocol protocol)
{
Number = number;
Tag = tag;
Protocol = protocol;
if (string.IsNullOrWhiteSpace(Tag))
{
throw new Exception("A unique port tag is required");
}
}
public int Number { get; }
public string Tag { get; }
public PortProtocol Protocol { get; }
public bool IsTcp()
{
return Protocol == PortProtocol.TCP;
}
public bool IsUdp()
{
return Protocol == PortProtocol.UDP;
}
public override string ToString()
{
if (string.IsNullOrEmpty(Tag)) return $"untagged-port={Number}";
return $"{Tag}={Number}";
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)
@@ -94,15 +124,21 @@
public class VolumeMount
{
public VolumeMount(string volumeName, string mountPath, string resourceQuantity)
public VolumeMount(string volumeName, string mountPath, string? subPath = null, string? resourceQuantity = null, string? secret = null, string? hostPath = null)
{
VolumeName = volumeName;
MountPath = mountPath;
SubPath = subPath;
ResourceQuantity = resourceQuantity;
Secret = secret;
HostPath = hostPath;
}
public string VolumeName { get; }
public string MountPath { get; }
public string ResourceQuantity { get; }
public string? SubPath { get; }
public string? ResourceQuantity { get; }
public string? Secret { get; }
public string? HostPath { get; }
}
}
@@ -1,6 +1,6 @@
using Utils;
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public abstract class ContainerRecipeFactory
{
@@ -13,6 +13,9 @@ namespace KubernetesWorkflow
private readonly List<object> additionals = new List<object>();
private RecipeComponentFactory factory = null!;
private ContainerResources resources = new ContainerResources();
private SchedulingAffinity schedulingAffinity = new SchedulingAffinity();
private CommandOverride commandOverride = new CommandOverride();
private bool setCriticalPriority;
public ContainerRecipe CreateRecipe(int index, int containerNumber, RecipeComponentFactory factory, StartupConfig config)
{
@@ -22,7 +25,7 @@ namespace KubernetesWorkflow
Initialize(config);
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources,
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
exposedPorts.ToArray(),
internalPorts.ToArray(),
envVars.ToArray(),
@@ -40,6 +43,9 @@ namespace KubernetesWorkflow
additionals.Clear();
this.factory = null!;
resources = new ContainerResources();
schedulingAffinity = new SchedulingAffinity();
commandOverride = new CommandOverride();
setCriticalPriority = false;
return recipe;
}
@@ -50,31 +56,31 @@ namespace KubernetesWorkflow
protected int Index { get; private set; } = 0;
protected abstract void Initialize(StartupConfig config);
protected Port AddExposedPort(string tag)
protected Port AddExposedPort(string tag, PortProtocol protocol = PortProtocol.TCP)
{
return AddExposedPort(factory.CreatePort(tag));
return AddExposedPort(factory.CreateExternalPort(tag, protocol));
}
protected Port AddExposedPort(int number, string tag)
protected Port AddExposedPort(int number, string tag, PortProtocol protocol = PortProtocol.TCP)
{
return AddExposedPort(factory.CreatePort(number, tag));
return AddExposedPort(factory.CreateExternalPort(number, tag, protocol));
}
protected Port AddInternalPort(string tag = "")
protected Port AddInternalPort(string tag = "", PortProtocol protocol = PortProtocol.TCP)
{
var p = factory.CreatePort(tag);
var p = factory.CreateInternalPort(tag, protocol);
internalPorts.Add(p);
return p;
}
protected void AddExposedPortAndVar(string name, string tag)
protected void AddExposedPortAndVar(string name, string tag, PortProtocol protocol = PortProtocol.TCP)
{
AddEnvVar(name, AddExposedPort(tag));
AddEnvVar(name, AddExposedPort(tag, protocol));
}
protected void AddInternalPortAndVar(string name, string tag = "")
protected void AddInternalPortAndVar(string name, string tag = "", PortProtocol protocol = PortProtocol.TCP)
{
AddEnvVar(name, AddInternalPort(tag));
AddEnvVar(name, AddInternalPort(tag, protocol));
}
protected void AddEnvVar(string name, string value)
@@ -97,12 +103,18 @@ namespace KubernetesWorkflow
podAnnotations.Add(name, value);
}
protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null)
{
var size = 10.MB().SizeInBytes.ToString();
volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath));
}
protected void AddVolume(string mountPath, ByteSize volumeSize)
{
volumeMounts.Add(new VolumeMount(
$"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}",
mountPath,
volumeSize.ToSuffixNotation()));
resourceQuantity: volumeSize.SizeInBytes.ToString()));
}
protected void Additional(object userData)
@@ -115,11 +127,28 @@ namespace KubernetesWorkflow
SetResourcesRequest(new ContainerResourceSet(milliCPUs, memory));
}
protected void SetResourceLimits(int milliCPUs, ByteSize memory)
protected void SetSchedulingAffinity(string notIn)
{
SetResourceLimits(new ContainerResourceSet(milliCPUs, memory));
schedulingAffinity = new SchedulingAffinity(notIn);
}
protected void OverrideCommand(params string[] command)
{
commandOverride = new CommandOverride(command);
}
protected void SetSystemCriticalPriority()
{
setCriticalPriority = true;
}
// Disabled following a possible bug in the k8s cluster that will throttle containers much more than is
// called for if they have resource limits defined.
//protected void SetResourceLimits(int milliCPUs, ByteSize memory)
//{
// SetResourceLimits(new ContainerResourceSet(milliCPUs, memory));
//}
protected void SetResourcesRequest(ContainerResourceSet requests)
{
resources.Requests = requests;
@@ -1,6 +1,6 @@
using Utils;
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class ContainerResources
{
@@ -1,4 +1,4 @@
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class PodAnnotations
{
@@ -1,4 +1,4 @@
namespace KubernetesWorkflow
namespace KubernetesWorkflow.Recipe
{
public class PodLabels
{
@@ -0,0 +1,50 @@
using System.Globalization;
using Utils;
namespace KubernetesWorkflow.Recipe
{
public class RecipeComponentFactory
{
private readonly NumberSource internalNumberSource = new NumberSource(8080);
private static readonly NumberSource externalNumberSource = new NumberSource(30000);
private static int[] usedExternalPorts = Array.Empty<int>();
public void Update(K8sController controller)
{
usedExternalPorts = controller.GetUsedExternalPorts();
}
public Port CreateInternalPort(string tag, PortProtocol protocol)
{
return new Port(internalNumberSource.GetNextNumber(), tag, protocol);
}
public Port CreateExternalPort(int number, string tag, PortProtocol protocol)
{
if (usedExternalPorts.Contains(number)) throw new Exception($"External port number {number} is already in use by the cluster.");
return new Port(number, tag, protocol);
}
public Port CreateExternalPort(string tag, PortProtocol protocol)
{
while (true)
{
var number = externalNumberSource.GetNextNumber();
if (!usedExternalPorts.Contains(number))
{
return new Port(number, tag, protocol);
}
}
}
public EnvVar CreateEnvVar(string name, int value)
{
return CreateEnvVar(name, value.ToString(CultureInfo.InvariantCulture));
}
public EnvVar CreateEnvVar(string name, string value)
{
return new EnvVar(name, value);
}
}
}
@@ -0,0 +1,18 @@
namespace KubernetesWorkflow.Recipe
{
public class SchedulingAffinity
{
public SchedulingAffinity(string? notIn = null)
{
NotIn = notIn;
}
public string? NotIn { get; }
public override string ToString()
{
if (string.IsNullOrEmpty(NotIn)) return "none";
return "notIn:" + NotIn;
}
}
}
@@ -1,30 +0,0 @@
using System.Globalization;
using Utils;
namespace KubernetesWorkflow
{
public class RecipeComponentFactory
{
private NumberSource portNumberSource = new NumberSource(8080);
public Port CreatePort(int number, string tag)
{
return new Port(number, tag);
}
public Port CreatePort(string tag)
{
return new Port(portNumberSource.GetNextNumber(), tag);
}
public EnvVar CreateEnvVar(string name, int value)
{
return CreateEnvVar(name, value.ToString(CultureInfo.InvariantCulture));
}
public EnvVar CreateEnvVar(string name, string value)
{
return new EnvVar(name, value);
}
}
}
@@ -1,63 +1,38 @@
using System.Net.NetworkInformation;
using Utils;
namespace KubernetesWorkflow
namespace KubernetesWorkflow
{
internal enum RunnerLocation
{
Unknown,
ExternalToCluster,
InternalToCluster,
}
internal static class RunnerLocationUtils
{
private static RunnerLocation? knownLocation = null;
private static RunnerLocation location = RunnerLocation.Unknown;
internal static RunnerLocation DetermineRunnerLocation(RunningContainer container)
internal static RunnerLocation GetRunnerLocation()
{
if (knownLocation != null) return knownLocation.Value;
knownLocation = PingForLocation(container);
return knownLocation.Value;
DetermineRunnerLocation();
if (location == RunnerLocation.Unknown) throw new Exception("Runner location is unknown.");
return location;
}
private static RunnerLocation PingForLocation(RunningContainer container)
private static void DetermineRunnerLocation()
{
if (PingHost(container.Pod.PodInfo.Ip))
if (location != RunnerLocation.Unknown) return;
var port = Environment.GetEnvironmentVariable("KUBERNETES_PORT");
var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST");
if (string.IsNullOrEmpty(port) || string.IsNullOrEmpty(host))
{
return RunnerLocation.InternalToCluster;
location = RunnerLocation.ExternalToCluster;
}
foreach (var port in container.ContainerPorts)
else
{
if (PingHost(Format(port.ExternalAddress)))
{
return RunnerLocation.ExternalToCluster;
}
location = RunnerLocation.InternalToCluster;
}
throw new Exception("Unable to determine location relative to kubernetes cluster.");
}
private static string Format(Address host)
{
return host.Host
.Replace("http://", "")
.Replace("https://", "");
}
private static bool PingHost(string host)
{
try
{
using var pinger = new Ping();
PingReply reply = pinger.Send(host);
return reply.Status == IPStatus.Success;
}
catch (PingException)
{
}
return false;
}
}
}
@@ -1,79 +0,0 @@
using Newtonsoft.Json;
using Utils;
namespace KubernetesWorkflow
{
public class RunningContainers
{
public RunningContainers(StartupConfig startupConfig, RunningPod runningPod, RunningContainer[] containers)
{
StartupConfig = startupConfig;
RunningPod = runningPod;
Containers = containers;
}
public StartupConfig StartupConfig { get; }
public RunningPod RunningPod { get; }
public RunningContainer[] Containers { get; }
public string Describe()
{
return string.Join(",", Containers.Select(c => c.Name));
}
}
public class RunningContainer
{
public RunningContainer(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts, string name, ContainerPort[] containerPorts)
{
Pod = pod;
Recipe = recipe;
ServicePorts = servicePorts;
Name = name;
ContainerPorts = containerPorts;
}
public string Name { get; }
public RunningPod Pod { get; }
public ContainerRecipe Recipe { get; }
public Port[] ServicePorts { get; }
public ContainerPort[] ContainerPorts { get; }
public Address GetAddress(string portTag)
{
var containerPort = ContainerPorts.Single(c => c.Port.Tag == portTag);
if (RunnerLocationUtils.DetermineRunnerLocation(this) == RunnerLocation.InternalToCluster)
{
return containerPort.InternalAddress;
}
return containerPort.ExternalAddress;
}
}
public class ContainerPort
{
public ContainerPort(Port port, Address externalAddress, Address internalAddress)
{
Port = port;
ExternalAddress = externalAddress;
InternalAddress = internalAddress;
}
public Port Port { get; }
public Address ExternalAddress { get; }
public Address InternalAddress { get; }
}
public static class RunningContainersExtensions
{
public static RunningContainer[] Containers(this RunningContainers[] runningContainers)
{
return runningContainers.SelectMany(c => c.Containers).ToArray();
}
public static string Describe(this RunningContainers[] runningContainers)
{
return string.Join(",", runningContainers.Select(c => c.Describe()));
}
}
}
+49 -37
View File
@@ -1,54 +1,66 @@
namespace KubernetesWorkflow
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
namespace KubernetesWorkflow
{
public class RunningPod
public class StartResult
{
public RunningPod(K8sCluster cluster, PodInfo podInfo, string deploymentName, string serviceName, ContainerRecipePortMapEntry[] portMapEntries)
public StartResult(
K8sCluster cluster,
ContainerRecipe[] containerRecipes,
RunningDeployment deployment,
RunningService? internalService,
RunningService? externalService)
{
Cluster = cluster;
PodInfo = podInfo;
DeploymentName = deploymentName;
ServiceName = serviceName;
PortMapEntries = portMapEntries;
ContainerRecipes = containerRecipes;
Deployment = deployment;
InternalService = internalService;
ExternalService = externalService;
}
public K8sCluster Cluster { get; }
public PodInfo PodInfo { get; }
public ContainerRecipePortMapEntry[] PortMapEntries { get; }
public string DeploymentName { get; }
public string ServiceName { get; }
public ContainerRecipe[] ContainerRecipes { get; }
public RunningDeployment Deployment { get; }
public RunningService? InternalService { get; }
public RunningService? ExternalService { get; }
public Port[] GetServicePortsForContainerRecipe(ContainerRecipe containerRecipe)
public Port GetInternalServicePorts(ContainerRecipe recipe, string tag)
{
return PortMapEntries
.Where(p => p.ContainerNumber == containerRecipe.Number)
.SelectMany(p => p.Ports)
.ToArray();
}
}
if (InternalService != null)
{
var p = InternalService.GetServicePortForRecipeAndTag(recipe, tag);
if (p != null) return p;
}
public class ContainerRecipePortMapEntry
{
public ContainerRecipePortMapEntry(int containerNumber, Port[] ports)
{
ContainerNumber = containerNumber;
Ports = ports;
throw new Exception($"Unable to find internal port by tag '{tag}' for recipe '{recipe.Name}'.");
}
public int ContainerNumber { get; }
public Port[] Ports { get; }
}
public class PodInfo
{
public PodInfo(string name, string ip, string k8sNodeName)
public Port GetExternalServicePorts(ContainerRecipe recipe, string tag)
{
Name = name;
Ip = ip;
K8SNodeName = k8sNodeName;
if (ExternalService != null)
{
var p = ExternalService.GetServicePortForRecipeAndTag(recipe, tag);
if (p != null) return p;
}
throw new Exception($"Unable to find external port by tag '{tag}' for recipe '{recipe.Name}'.");
}
public string Name { get; }
public string Ip { get; }
public string K8SNodeName { get; }
public Port[] GetServicePortsForContainer(ContainerRecipe recipe)
{
if (InternalService != null)
{
var p = InternalService.GetServicePortsForRecipe(recipe);
if (p.Any()) return p;
}
if (ExternalService != null)
{
var p = ExternalService.GetServicePortsForRecipe(recipe);
if (p.Any()) return p;
}
return Array.Empty<Port>();
}
}
}
+115 -74
View File
@@ -1,4 +1,7 @@
using Logging;
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace KubernetesWorkflow
@@ -6,14 +9,16 @@ namespace KubernetesWorkflow
public interface IStartupWorkflow
{
IKnownLocations GetAvailableLocations();
RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
PodInfo GetPodInfo(RunningContainer container);
PodInfo GetPodInfo(RunningPod pod);
CrashWatcher CreateCrashWatcher(RunningContainer container);
void Stop(RunningContainers runningContainers);
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
void Stop(RunningPod pod, bool waitTillStopped);
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null);
string ExecuteCommand(RunningContainer container, string command, params string[] args);
void DeleteNamespace();
void DeleteNamespacesStartingWith(string namespacePrefix);
void DeleteNamespace(bool wait);
void DeleteNamespacesStartingWith(string namespacePrefix, bool wait);
}
public class StartupWorkflow : IStartupWorkflow
@@ -21,17 +26,15 @@ namespace KubernetesWorkflow
private readonly ILog log;
private readonly WorkflowNumberSource numberSource;
private readonly K8sCluster cluster;
private readonly KnownK8sPods knownK8SPods;
private readonly string k8sNamespace;
private readonly RecipeComponentFactory componentFactory = new RecipeComponentFactory();
private readonly LocationProvider locationProvider;
internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, KnownK8sPods knownK8SPods, string k8sNamespace)
internal StartupWorkflow(ILog log, WorkflowNumberSource numberSource, K8sCluster cluster, string k8sNamespace)
{
this.log = log;
this.numberSource = numberSource;
this.cluster = cluster;
this.knownK8SPods = knownK8SPods;
this.k8sNamespace = k8sNamespace;
locationProvider = new LocationProvider(log, K8s);
@@ -42,44 +45,72 @@ namespace KubernetesWorkflow
return locationProvider.GetAvailableLocations();
}
public RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
public FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
{
return Start(numberOfContainers, KnownLocations.UnspecifiedLocation, recipeFactory, startupConfig);
}
public RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
public FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
{
return K8s(controller =>
{
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
var runningPod = controller.BringOnline(recipes, location);
var containers = CreateContainers(runningPod, recipes, startupConfig);
componentFactory.Update(controller);
var rc = new RunningContainers(startupConfig, runningPod, containers);
var recipes = CreateRecipes(numberOfContainers, recipeFactory, startupConfig);
var startResult = controller.BringOnline(recipes, location);
var containers = CreateContainers(startResult, recipes, startupConfig);
var rc = new RunningPod(startupConfig, startResult, containers);
cluster.Configuration.Hooks.OnContainersStarted(rc);
return rc;
if (startResult.ExternalService != null)
{
componentFactory.Update(controller);
}
return new FutureContainers(rc, this);
});
}
public void WaitUntilOnline(RunningPod rc)
{
K8s(controller =>
{
foreach (var c in rc.Containers)
{
controller.WaitUntilOnline(c);
}
});
}
public PodInfo GetPodInfo(RunningContainer container)
{
return K8s(c => c.GetPodInfo(container.RunningPod.StartResult.Deployment));
}
public PodInfo GetPodInfo(RunningPod pod)
{
return K8s(c => c.GetPodInfo(pod.StartResult.Deployment));
}
public CrashWatcher CreateCrashWatcher(RunningContainer container)
{
return K8s(c => c.CreateCrashWatcher(container));
}
public void Stop(RunningContainers runningContainers)
public void Stop(RunningPod runningPod, bool waitTillStopped)
{
K8s(controller =>
{
controller.Stop(runningContainers.RunningPod);
cluster.Configuration.Hooks.OnContainersStopped(runningContainers);
controller.Stop(runningPod.StartResult, waitTillStopped);
cluster.Configuration.Hooks.OnContainersStopped(runningPod);
});
}
public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null)
public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null)
{
K8s(controller =>
{
controller.DownloadPodLog(container.Pod, container.Recipe, logHandler, tailLines);
controller.DownloadPodLog(container, logHandler, tailLines, previous);
});
}
@@ -87,38 +118,36 @@ namespace KubernetesWorkflow
{
return K8s(controller =>
{
return controller.ExecuteCommand(container.Pod, container.Recipe.Name, command, args);
return controller.ExecuteCommand(container, command, args);
});
}
public void DeleteNamespace()
public void DeleteNamespace(bool wait)
{
K8s(controller =>
{
controller.DeleteNamespace();
controller.DeleteNamespace(wait);
});
}
public void DeleteNamespacesStartingWith(string namespacePrefix)
public void DeleteNamespacesStartingWith(string namespacePrefix, bool wait)
{
K8s(controller =>
{
controller.DeleteAllNamespacesStartingWith(namespacePrefix);
controller.DeleteAllNamespacesStartingWith(namespacePrefix, wait);
});
}
private RunningContainer[] CreateContainers(RunningPod runningPod, ContainerRecipe[] recipes, StartupConfig startupConfig)
private RunningContainer[] CreateContainers(StartResult startResult, ContainerRecipe[] recipes, StartupConfig startupConfig)
{
log.Debug();
return recipes.Select(r =>
{
var servicePorts = runningPod.GetServicePortsForContainerRecipe(r);
log.Debug($"{r} -> service ports: {string.Join(",", servicePorts.Select(p => p.Number))}");
var name = GetContainerName(r, startupConfig);
var addresses = CreateContainerAddresses(startResult, r);
log.Debug($"{r}={name} -> container addresses: {string.Join(Environment.NewLine, addresses.Select(a => a.ToString()))}");
return new RunningContainer(runningPod, r, servicePorts, name,
CreateContainerPorts(runningPod, r, servicePorts));
return new RunningContainer(name, r, addresses);
}).ToArray();
}
@@ -136,37 +165,40 @@ namespace KubernetesWorkflow
}
}
private ContainerPort[] CreateContainerPorts(RunningPod pod, ContainerRecipe recipe, Port[] servicePorts)
private ContainerAddress[] CreateContainerAddresses(StartResult startResult, ContainerRecipe recipe)
{
var result = new List<ContainerPort>();
var result = new List<ContainerAddress>();
foreach (var exposedPort in recipe.ExposedPorts)
{
result.Add(new ContainerPort(
exposedPort,
GetContainerExternalAddress(pod, servicePorts, exposedPort),
GetContainerInternalAddress(exposedPort)));
result.Add(new ContainerAddress(exposedPort.Tag, GetContainerExternalAddress(startResult, recipe, exposedPort.Tag), false));
result.Add(new ContainerAddress(exposedPort.Tag, GetContainerInternalAddress(startResult, recipe, exposedPort.Tag), true));
}
foreach (var internalPort in recipe.InternalPorts)
{
result.Add(new ContainerAddress(internalPort.Tag, GetContainerInternalAddress(startResult, recipe, internalPort.Tag), true));
}
return result.ToArray();
}
private static Address GetContainerExternalAddress(RunningPod pod, Port[] servicePorts, Port exposedPort)
private static Address GetContainerExternalAddress(StartResult startResult, ContainerRecipe recipe, string tag)
{
var servicePort = servicePorts.Single(p => p.Tag == exposedPort.Tag);
var port = startResult.GetExternalServicePorts(recipe, tag);
return new Address(
pod.Cluster.HostAddress,
servicePort.Number);
startResult.Cluster.HostAddress,
port.Number);
}
private Address GetContainerInternalAddress(Port exposedPort)
private Address GetContainerInternalAddress(StartResult startResult, ContainerRecipe recipe, string tag)
{
var serviceName = "service-" + numberSource.WorkflowNumber;
var port = exposedPort.Number;
var namespaceName = startResult.Cluster.Configuration.KubernetesNamespace;
var serviceName = startResult.InternalService!.Name;
var port = startResult.GetInternalServicePorts(recipe, tag);
return new Address(
$"http://{serviceName}.{k8sNamespace}.svc.cluster.local",
port);
$"http://{serviceName}.{namespaceName}.svc.cluster.local",
port.Number);
}
private ContainerRecipe[] CreateRecipes(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
@@ -176,6 +208,8 @@ namespace KubernetesWorkflow
for (var i = 0; i < numberOfContainers; i++)
{
var recipe = recipeFactory.CreateRecipe(i, numberSource.GetContainerNumber(), componentFactory, startupConfig);
CheckPorts(recipe);
if (cluster.Configuration.AddAppPodLabel) recipe.PodLabels.Add("app", recipeFactory.AppName);
cluster.Configuration.Hooks.OnContainerRecipeCreated(recipe);
result.Add(recipe);
@@ -184,40 +218,47 @@ namespace KubernetesWorkflow
return result.ToArray();
}
private void CheckPorts(ContainerRecipe recipe)
{
var allTags =
recipe.ExposedPorts.Concat(recipe.InternalPorts)
.Select(p => K8sNameUtils.Format(p.Tag)).ToArray();
if (allTags.Length != allTags.Distinct().Count())
{
throw new Exception("Duplicate port tags found in recipe for " + recipe.Name);
}
}
private void K8s(Action<K8sController> action)
{
var controller = new K8sController(log, cluster, knownK8SPods, numberSource, k8sNamespace);
action(controller);
controller.Dispose();
try
{
var controller = new K8sController(log, cluster, numberSource, k8sNamespace);
action(controller);
controller.Dispose();
}
catch (k8s.Autorest.HttpOperationException ex)
{
log.Error(JsonConvert.SerializeObject(ex.Response));
throw;
}
}
private T K8s<T>(Func<K8sController, T> action)
{
var controller = new K8sController(log, cluster, knownK8SPods, numberSource, k8sNamespace);
var result = action(controller);
controller.Dispose();
return result;
}
}
public interface ILogHandler
{
void Log(Stream log);
}
public abstract class LogHandler : ILogHandler
{
public void Log(Stream log)
{
using var reader = new StreamReader(log);
var line = reader.ReadLine();
while (line != null)
try
{
ProcessLine(line);
line = reader.ReadLine();
var controller = new K8sController(log, cluster, numberSource, k8sNamespace);
var result = action(controller);
controller.Dispose();
return result;
}
catch (k8s.Autorest.HttpOperationException ex)
{
log.Error(JsonConvert.SerializeObject(ex.Response));
throw;
}
}
protected abstract void ProcessLine(string line);
}
}
@@ -0,0 +1,24 @@
using Utils;
namespace KubernetesWorkflow.Types
{
public class ContainerAddress
{
public ContainerAddress(string portTag, Address address, bool isInteral)
{
PortTag = portTag;
Address = address;
IsInteral = isInteral;
}
public string PortTag { get; }
public Address Address { get; }
public bool IsInteral { get; }
public override string ToString()
{
var indicator = IsInteral ? "int" : "ext";
return $"{indicator} {PortTag} -> '{Address}'";
}
}
}
@@ -0,0 +1,16 @@
using KubernetesWorkflow.Recipe;
namespace KubernetesWorkflow.Types
{
public class ContainerRecipePortMapEntry
{
public ContainerRecipePortMapEntry(int recipeNumber, Port[] ports)
{
RecipeNumber = recipeNumber;
Ports = ports;
}
public int RecipeNumber { get; }
public Port[] Ports { get; }
}
}
@@ -0,0 +1,20 @@
namespace KubernetesWorkflow.Types
{
public class FutureContainers
{
private readonly RunningPod runningPod;
private readonly StartupWorkflow workflow;
public FutureContainers(RunningPod runningPod, StartupWorkflow workflow)
{
this.runningPod = runningPod;
this.workflow = workflow;
}
public RunningPod WaitForOnline()
{
workflow.WaitUntilOnline(runningPod);
return runningPod;
}
}
}
@@ -0,0 +1,14 @@
namespace KubernetesWorkflow.Types
{
public class K8sNodeLabel
{
public K8sNodeLabel(string key, string value)
{
Key = key;
Value = value;
}
public string Key { get; }
public string Value { get; }
}
}
@@ -0,0 +1,16 @@
namespace KubernetesWorkflow.Types
{
public class PodInfo
{
public PodInfo(string name, string ip, string k8sNodeName)
{
Name = name;
Ip = ip;
K8SNodeName = k8sNodeName;
}
public string Name { get; }
public string Ip { get; }
public string K8SNodeName { get; }
}
}
@@ -0,0 +1,54 @@
using KubernetesWorkflow.Recipe;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace KubernetesWorkflow.Types
{
public class RunningContainer
{
public RunningContainer(string name, ContainerRecipe recipe, ContainerAddress[] addresses)
{
Name = name;
Recipe = recipe;
Addresses = addresses;
}
public string Name { get; }
public ContainerRecipe Recipe { get; }
public ContainerAddress[] Addresses { get; }
[JsonIgnore]
public RunningPod RunningPod { get; internal set; } = null!;
public Address GetAddress(ILog log, string portTag)
{
var addresses = Addresses.Where(a => a.PortTag == portTag).ToArray();
if (!addresses.Any()) throw new Exception("No addresses found for portTag: " + portTag);
var select = SelectAddress(addresses);
log.Debug($"Container '{Name}' selected for tag '{portTag}' address: '{select}'");
return select.Address;
}
public Address GetInternalAddress(string portTag)
{
var containerAddress = Addresses.Single(a => a.PortTag == portTag && a.IsInteral);
return containerAddress.Address;
}
private ContainerAddress SelectAddress(ContainerAddress[] addresses)
{
var location = RunnerLocationUtils.GetRunnerLocation();
if (location == RunnerLocation.InternalToCluster)
{
return addresses.Single(a => a.IsInteral);
}
if (location == RunnerLocation.ExternalToCluster)
{
return addresses.Single(a => !a.IsInteral);
}
throw new Exception("Running location not known.");
}
}
}
@@ -0,0 +1,14 @@
namespace KubernetesWorkflow.Types
{
public class RunningDeployment
{
public RunningDeployment(string name, string podLabel)
{
Name = name;
PodLabel = podLabel;
}
public string Name { get; }
public string PodLabel { get; }
}
}
@@ -0,0 +1,39 @@
using Newtonsoft.Json;
namespace KubernetesWorkflow.Types
{
public class RunningPod
{
public RunningPod(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
{
StartupConfig = startupConfig;
StartResult = startResult;
Containers = containers;
foreach (var c in containers) c.RunningPod = this;
}
public StartupConfig StartupConfig { get; }
public StartResult StartResult { get; }
public RunningContainer[] Containers { get; }
[JsonIgnore]
public string Name
{
get { return $"'{string.Join("&", Containers.Select(c => c.Name).ToArray())}'"; }
}
public string Describe()
{
return string.Join(",", Containers.Select(c => c.Name));
}
}
public static class RunningContainersExtensions
{
public static string Describe(this RunningPod[] runningContainers)
{
return string.Join(",", runningContainers.Select(c => c.Describe()));
}
}
}
@@ -0,0 +1,29 @@
using KubernetesWorkflow.Recipe;
namespace KubernetesWorkflow.Types
{
public class RunningService
{
public RunningService(string name, List<ContainerRecipePortMapEntry> result)
{
Name = name;
Result = result;
}
public string Name { get; }
public List<ContainerRecipePortMapEntry> Result { get; }
public Port? GetServicePortForRecipeAndTag(ContainerRecipe recipe, string tag)
{
return GetServicePortsForRecipe(recipe).SingleOrDefault(p => p.Tag == tag);
}
public Port[] GetServicePortsForRecipe(ContainerRecipe recipe)
{
return Result
.Where(p => p.RecipeNumber == recipe.Number)
.SelectMany(p => p.Ports)
.ToArray();
}
}
}
@@ -7,7 +7,6 @@ namespace KubernetesWorkflow
{
private readonly NumberSource numberSource = new NumberSource(0);
private readonly NumberSource containerNumberSource = new NumberSource(0);
private readonly KnownK8sPods knownPods = new KnownK8sPods();
private readonly K8sCluster cluster;
private readonly ILog log;
private readonly Configuration configuration;
@@ -26,7 +25,7 @@ namespace KubernetesWorkflow
var workflowNumberSource = new WorkflowNumberSource(numberSource.GetNextNumber(),
containerNumberSource);
return new StartupWorkflow(log, workflowNumberSource, cluster, knownPods, GetNamespace(namespaceOverride));
return new StartupWorkflow(log, workflowNumberSource, cluster, GetNamespace(namespaceOverride));
}
private string GetNamespace(string? namespaceOverride)
+15 -8
View File
@@ -13,16 +13,21 @@ namespace Logging
public abstract class BaseLog : ILog
{
public static bool EnableDebugLogging { get; set; } = false;
private readonly NumberSource subfileNumberSource = new NumberSource(0);
private readonly bool debug;
private readonly List<BaseLogStringReplacement> replacements = new List<BaseLogStringReplacement>();
private LogFile? logFile;
protected BaseLog(bool debug)
public BaseLog()
{
this.debug = debug;
IsDebug =
EnableDebugLogging ||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("LOGDEBUG")) ||
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("DEBUGLOG"));
}
protected bool IsDebug { get; private set; }
protected abstract string GetFullName();
public LogFile LogFile
@@ -39,19 +44,20 @@ namespace Logging
LogFile.Write(ApplyReplacements(message));
}
public virtual void Debug(string message = "", int skipFrames = 0)
public void Debug(string message = "", int skipFrames = 0)
{
if (debug)
if (IsDebug)
{
var callerName = DebugStack.GetCallerName(skipFrames);
// We don't use Log because in the debug output we should not have any replacements.
LogFile.Write($"(debug)({callerName}) {message}");
Log($"(debug)({callerName}) {message}");
}
}
public virtual void Error(string message)
{
Log($"[ERROR] {message}");
var msg = $"[ERROR] {message}";
Console.WriteLine(msg);
Log(msg);
}
public virtual void AddStringReplace(string from, string to)
@@ -73,6 +79,7 @@ namespace Logging
private string ApplyReplacements(string str)
{
if (IsDebug) return str;
foreach (var replacement in replacements)
{
str = replacement.Apply(str);
-4
View File
@@ -2,10 +2,6 @@
{
public class ConsoleLog : BaseLog
{
public ConsoleLog() : base(false)
{
}
protected override string GetFullName()
{
return "CONSOLE";
+17
View File
@@ -0,0 +1,17 @@
namespace Logging
{
public class FileLog : BaseLog
{
public FileLog(string fullFilename)
{
FullFilename = fullFilename;
}
public string FullFilename { get; }
protected override string GetFullName()
{
return FullFilename;
}
}
}
+1 -3
View File
@@ -2,13 +2,11 @@
{
public class LogConfig
{
public LogConfig(string logRoot, bool debugEnabled)
public LogConfig(string logRoot)
{
LogRoot = logRoot;
DebugEnabled = debugEnabled;
}
public string LogRoot { get; }
public bool DebugEnabled { get; }
}
}
+12 -5
View File
@@ -3,14 +3,21 @@
public class LogPrefixer : ILog
{
private readonly ILog backingLog;
private readonly string prefix;
public LogPrefixer(ILog backingLog)
{
this.backingLog = backingLog;
}
public LogPrefixer(ILog backingLog, string prefix)
{
this.backingLog = backingLog;
this.prefix = prefix;
Prefix = prefix;
}
public string Prefix { get; set; } = string.Empty;
public LogFile CreateSubfile(string ext = "log")
{
return backingLog.CreateSubfile(ext);
@@ -18,17 +25,17 @@
public void Debug(string message = "", int skipFrames = 0)
{
backingLog.Debug(prefix + message, skipFrames);
backingLog.Debug(Prefix + message, skipFrames);
}
public void Error(string message)
{
backingLog.Error(prefix + message);
backingLog.Error(Prefix + message);
}
public void Log(string message)
{
backingLog.Log(prefix + message);
backingLog.Log(Prefix + message);
}
public void AddStringReplace(string from, string to)
+2 -8
View File
@@ -2,10 +2,6 @@
{
public class NullLog : BaseLog
{
public NullLog() : base(false)
{
}
public string FullFilename { get; set; } = "NULL";
protected override string GetFullName()
@@ -15,15 +11,13 @@
public override void Log(string message)
{
}
public override void Debug(string message = "", int skipFrames = 0)
{
if (IsDebug) base.Log(message);
}
public override void Error(string message)
{
Console.WriteLine("Error: " + message);
base.Error(message);
}
public override void AddStringReplace(string from, string to)
+17 -5
View File
@@ -16,19 +16,19 @@ namespace Logging
this.debug = debug;
}
public static void Measure(ILog log, string name, Action action, bool debug = false)
public static TimeSpan Measure(ILog log, string name, Action action, bool debug = false)
{
var sw = Begin(log, name, debug);
action();
sw.End();
return sw.End();
}
public static T Measure<T>(ILog log, string name, Func<T> action, bool debug = false)
public static StopwatchResult<T> Measure<T>(ILog log, string name, Func<T> action, bool debug = false)
{
var sw = Begin(log, name, debug);
var result = action();
sw.End();
return result;
var duration = sw.End();
return new StopwatchResult<T>(result, duration);
}
public static Stopwatch Begin(ILog log)
@@ -68,4 +68,16 @@ namespace Logging
return duration;
}
}
public class StopwatchResult<T>
{
public StopwatchResult(T value, TimeSpan duration)
{
Value = value;
Duration = duration;
}
public T Value { get; }
public TimeSpan Duration { get; }
}
}
@@ -0,0 +1,41 @@
namespace NethereumWorkflow.BlockUtils
{
public class BlockCache
{
public delegate void CacheClearedEvent();
private const int MaxEntries = 1024 * 1024 * 5;
private readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
public event CacheClearedEvent? OnCacheCleared;
public BlockTimeEntry Add(ulong number, DateTime dateTime)
{
return Add(new BlockTimeEntry(number, dateTime));
}
public BlockTimeEntry Add(BlockTimeEntry entry)
{
if (!entries.ContainsKey(entry.BlockNumber))
{
if (entries.Count > MaxEntries)
{
entries.Clear();
var e = OnCacheCleared;
if (e != null) e();
}
entries.Add(entry.BlockNumber, entry);
}
return entries[entry.BlockNumber];
}
public BlockTimeEntry? Get(ulong number)
{
if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null;
return value;
}
public int Size { get { return entries.Count; } }
}
}
@@ -0,0 +1,19 @@
namespace NethereumWorkflow.BlockUtils
{
public class BlockTimeEntry
{
public BlockTimeEntry(ulong blockNumber, DateTime utc)
{
BlockNumber = blockNumber;
Utc = utc;
}
public ulong BlockNumber { get; }
public DateTime Utc { get; }
public override string ToString()
{
return $"[{BlockNumber}] @ {Utc.ToString("o")}";
}
}
}
@@ -0,0 +1,112 @@
using Logging;
namespace NethereumWorkflow.BlockUtils
{
public class BlockTimeFinder
{
private readonly BlockCache cache;
private readonly BlockchainBounds bounds;
private readonly IWeb3Blocks web3;
private readonly ILog log;
public BlockTimeFinder(BlockCache cache, IWeb3Blocks web3, ILog log)
{
this.web3 = web3;
this.log = log;
this.cache = cache;
bounds = new BlockchainBounds(cache, web3);
}
public BlockTimeEntry Get(ulong blockNumber)
{
bounds.Initialize();
var b = cache.Get(blockNumber);
if (b != null) return b;
return GetBlock(blockNumber);
}
public ulong? GetHighestBlockNumberBefore(DateTime moment)
{
bounds.Initialize();
if (moment <= bounds.Genesis.Utc) return null;
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
return Log(() => Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector));
}
public ulong? GetLowestBlockNumberAfter(DateTime moment)
{
bounds.Initialize();
if (moment >= bounds.Current.Utc) return null;
if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
return Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ;
}
private ulong Log(Func<ulong> operation)
{
var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder), true);
var result = operation();
sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})");
return result;
}
private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func<DateTime, BlockTimeEntry, bool> isWhatIwant)
{
var middle = GetMiddle(lower, upper);
if (middle.BlockNumber == lower.BlockNumber)
{
if (isWhatIwant(target, upper)) return upper.BlockNumber;
}
if (isWhatIwant(target, middle))
{
return middle.BlockNumber;
}
if (middle.Utc > target)
{
return Search(lower, middle, target, isWhatIwant);
}
else
{
return Search(middle, upper, target, isWhatIwant);
}
}
private BlockTimeEntry GetMiddle(BlockTimeEntry lower, BlockTimeEntry upper)
{
ulong range = upper.BlockNumber - lower.BlockNumber;
ulong number = lower.BlockNumber + range / 2;
return GetBlock(number);
}
private bool HighestBeforeSelector(DateTime target, BlockTimeEntry entry)
{
var next = GetBlock(entry.BlockNumber + 1);
return
entry.Utc <= target &&
next.Utc > target;
}
private bool LowestAfterSelector(DateTime target, BlockTimeEntry entry)
{
var previous = GetBlock(entry.BlockNumber - 1);
return
entry.Utc >= target &&
previous.Utc < target;
}
private BlockTimeEntry GetBlock(ulong number)
{
if (number < bounds.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis.");
if (number > bounds.Current.BlockNumber) throw new Exception("Can't fetch block after current.");
var dateTime = web3.GetTimestampForBlock(number);
if (dateTime == null) throw new Exception("Failed to get dateTime for block that should exist.");
return cache.Add(number, dateTime.Value);
}
}
}
@@ -0,0 +1,106 @@
namespace NethereumWorkflow.BlockUtils
{
public class BlockchainBounds
{
private readonly BlockCache cache;
private readonly IWeb3Blocks web3;
public BlockTimeEntry Genesis { get; private set; } = null!;
public BlockTimeEntry Current { get; private set; } = null!;
public BlockchainBounds(BlockCache cache, IWeb3Blocks web3)
{
this.cache = cache;
this.web3 = web3;
cache.OnCacheCleared += Initialize;
}
public void Initialize()
{
AddCurrentBlock();
LookForGenesisBlock();
if (Current.BlockNumber == Genesis.BlockNumber)
{
throw new Exception("Unsupported condition: Current block is genesis block.");
}
}
private void LookForGenesisBlock()
{
if (Genesis != null)
{
cache.Add(Genesis);
return;
}
var blockTime = web3.GetTimestampForBlock(0);
if (blockTime != null)
{
AddGenesisBlock(0, blockTime.Value);
return;
}
LookForGenesisBlock(0, Current);
}
private void LookForGenesisBlock(ulong lower, BlockTimeEntry upper)
{
if (Genesis != null) return;
var range = upper.BlockNumber - lower;
if (range == 1)
{
var lowTime = web3.GetTimestampForBlock(lower);
if (lowTime != null)
{
AddGenesisBlock(lower, lowTime.Value);
}
else
{
AddGenesisBlock(upper);
}
return;
}
var current = lower + range / 2;
var blockTime = web3.GetTimestampForBlock(current);
if (blockTime != null)
{
var newUpper = cache.Add(current, blockTime.Value);
LookForGenesisBlock(lower, newUpper);
}
else
{
LookForGenesisBlock(current, upper);
}
}
private void AddCurrentBlock()
{
var currentBlockNumber = web3.GetCurrentBlockNumber();
var blockTime = web3.GetTimestampForBlock(currentBlockNumber);
if (blockTime == null) throw new Exception("Unable to get dateTime for current block.");
AddCurrentBlock(currentBlockNumber, blockTime.Value);
}
private void AddCurrentBlock(ulong currentBlockNumber, DateTime dateTime)
{
Current = new BlockTimeEntry(currentBlockNumber, dateTime);
cache.Add(Current);
}
private void AddGenesisBlock(ulong number, DateTime dateTime)
{
AddGenesisBlock(new BlockTimeEntry(number, dateTime));
}
private void AddGenesisBlock(BlockTimeEntry entry)
{
Genesis = entry;
cache.Add(Genesis);
}
}
}
@@ -1,13 +1,18 @@
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace NethereumWorkflow
{
public class NethereumInteraction
{
// BlockCache is a static instance: It stays alive for the duration of the application runtime.
private readonly static BlockCache blockCache = new BlockCache();
private readonly ILog log;
private readonly Web3 web3;
@@ -17,34 +22,47 @@ namespace NethereumWorkflow
this.web3 = web3;
}
public void SendEth(string toAddress, decimal ethAmount)
public string SendEth(string toAddress, decimal ethAmount)
{
log.Debug();
var receipt = Time.Wait(web3.Eth.GetEtherTransferService().TransferEtherAndWaitForReceiptAsync(toAddress, ethAmount));
if (!receipt.Succeeded()) throw new Exception("Unable to send Eth");
return receipt.TransactionHash;
}
public decimal GetEthBalance()
{
log.Debug();
return GetEthBalance(web3.TransactionManager.Account.Address);
}
public decimal GetEthBalance(string address)
{
log.Debug();
var balance = Time.Wait(web3.Eth.GetBalance.SendRequestAsync(address));
return Web3.Convert.FromWei(balance.Value);
}
public TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
log.Debug(typeof(TFunction).ToString());
var handler = web3.Eth.GetContractQueryHandler<TFunction>();
return Time.Wait(handler.QueryAsync<TResult>(contractAddress, function));
}
public void SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
public string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
log.Debug();
var handler = web3.Eth.GetContractTransactionHandler<TFunction>();
var receipt = Time.Wait(handler.SendRequestAndWaitForReceiptAsync(contractAddress, function));
if (!receipt.Succeeded()) throw new Exception("Unable to perform contract transaction.");
return receipt.TransactionHash;
}
public Transaction GetTransaction(string transactionHash)
{
log.Debug();
return Time.Wait(web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash));
}
public decimal? GetSyncedBlockNumber()
@@ -70,5 +88,46 @@ namespace NethereumWorkflow
return false;
}
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, BlockInterval blockRange) where TEvent : IEventDTO, new()
{
return GetEvents<TEvent>(address, blockRange.From, blockRange.To);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
{
var eventHandler = web3.Eth.GetEvent<TEvent>(address);
var from = new BlockParameter(fromBlockNumber);
var to = new BlockParameter(toBlockNumber);
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
}
public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange)
{
var wrapper = new Web3Wrapper(web3, log);
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
if (fromBlock == null || toBlock == null)
{
throw new Exception("Failed to convert time range to block range.");
}
return new BlockInterval(
timeRange: timeRange,
from: fromBlock.Value,
to: toBlock.Value
);
}
public BlockTimeEntry GetBlockForNumber(ulong number)
{
var wrapper = new Web3Wrapper(web3, log);
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
return blockTimeFinder.Get(number);
}
}
}
@@ -20,6 +20,7 @@ namespace NethereumWorkflow
public NethereumInteraction CreateWorkflow()
{
log.Debug("Starting interaction to " + ip + ":" + port);
return new NethereumInteraction(log, CreateWeb3());
}
@@ -0,0 +1,46 @@
using Logging;
using Nethereum.RPC.Eth.DTOs;
using Nethereum.Web3;
using Utils;
namespace NethereumWorkflow
{
public interface IWeb3Blocks
{
ulong GetCurrentBlockNumber();
DateTime? GetTimestampForBlock(ulong blockNumber);
}
public class Web3Wrapper : IWeb3Blocks
{
private readonly Web3 web3;
private readonly ILog log;
public Web3Wrapper(Web3 web3, ILog log)
{
this.web3 = web3;
this.log = log;
}
public ulong GetCurrentBlockNumber()
{
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
return Convert.ToUInt64(number.ToDecimal());
}
public DateTime? GetTimestampForBlock(ulong blockNumber)
{
try
{
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
if (block == null) return null;
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
}
catch (Exception ex)
{
log.Error("Exception while getting timestamp for block: " + ex);
return null;
}
}
}
}
+5
View File
@@ -15,5 +15,10 @@
{
return $"{Host}:{Port}";
}
public bool IsValid()
{
return !string.IsNullOrEmpty(Host) && Port > 0;
}
}
}
+30
View File
@@ -0,0 +1,30 @@
namespace Utils
{
public class BlockInterval
{
public BlockInterval(TimeRange timeRange, ulong from, ulong to)
{
if (from < to)
{
From = from;
To = to;
}
else
{
From = to;
To = from;
}
TimeRange = timeRange;
}
public ulong From { get; }
public ulong To { get; }
public TimeRange TimeRange { get; }
public ulong NumberOfBlocks => To - From;
public override string ToString()
{
return $"[{From} - {To}]";
}
}
}
+15 -1
View File
@@ -2,6 +2,9 @@
{
public class ByteSize
{
public static readonly ByteSize Zero = new ByteSize(0);
public const double DefaultSecondsPerMB = 10.0;
public ByteSize(long sizeInBytes)
{
if (sizeInBytes < 0) throw new ArgumentException("Cannot create ByteSize object with size less than 0. Was: " + sizeInBytes);
@@ -10,7 +13,6 @@
public long SizeInBytes { get; }
public const double DefaultSecondsPerMB = 10.0;
public long ToMB()
{
@@ -46,6 +48,18 @@
}
}
public class BytesPerSecond : ByteSize
{
public BytesPerSecond(long sizeInBytes) : base(sizeInBytes)
{
}
public override string ToString()
{
return base.ToString() + "/s";
}
}
public static class ByteSizeIntExtensions
{
private const long Kilo = 1024;
+4 -2
View File
@@ -1,4 +1,6 @@
namespace Utils
using System.Globalization;
namespace Utils
{
public static class Formatter
{
@@ -10,7 +12,7 @@
var sizeOrder = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
var digit = Math.Round(bytes / Math.Pow(1024, sizeOrder), 1);
return digit.ToString() + sizeSuffixes[sizeOrder];
return digit.ToString(CultureInfo.InvariantCulture) + sizeSuffixes[sizeOrder];
}
}
}
+7 -2
View File
@@ -2,6 +2,7 @@
{
public class NumberSource
{
private readonly object @lock = new object();
private int number;
public NumberSource(int start)
@@ -11,8 +12,12 @@
public int GetNextNumber()
{
var n = number;
number++;
var n = -1;
lock (@lock)
{
n = number;
number++;
}
return n;
}
}
+131
View File
@@ -0,0 +1,131 @@
namespace Utils
{
public class Retry
{
private readonly string description;
private readonly TimeSpan maxTimeout;
private readonly TimeSpan sleepAfterFail;
private readonly Action<Failure> onFail;
public Retry(string description, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail)
{
this.description = description;
this.maxTimeout = maxTimeout;
this.sleepAfterFail = sleepAfterFail;
this.onFail = onFail;
}
public void Run(Action task)
{
var run = new RetryRun(description, task, maxTimeout, sleepAfterFail, onFail);
run.Run();
}
public T Run<T>(Func<T> task)
{
T? result = default;
var run = new RetryRun(description, () =>
{
result = task();
}, maxTimeout, sleepAfterFail, onFail);
run.Run();
return result!;
}
private class RetryRun
{
private readonly string description;
private readonly Action task;
private readonly TimeSpan maxTimeout;
private readonly TimeSpan sleepAfterFail;
private readonly Action<Failure> onFail;
private readonly DateTime start = DateTime.UtcNow;
private readonly List<Failure> failures = new List<Failure>();
private int tryNumber;
private DateTime tryStart;
public RetryRun(string description, Action task, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> onFail)
{
this.description = description;
this.task = task;
this.maxTimeout = maxTimeout;
this.sleepAfterFail = sleepAfterFail;
this.onFail = onFail;
tryNumber = 0;
tryStart = DateTime.UtcNow;
}
public void Run()
{
while (true)
{
CheckMaximums();
tryNumber++;
tryStart = DateTime.UtcNow;
try
{
task();
return;
}
catch (Exception ex)
{
var failure = CaptureFailure(ex);
onFail(failure);
Time.Sleep(sleepAfterFail);
}
}
}
private Failure CaptureFailure(Exception ex)
{
var f = new Failure(ex, DateTime.UtcNow - tryStart, tryNumber);
failures.Add(f);
return f;
}
private void CheckMaximums()
{
if (Duration() > maxTimeout) Fail();
}
private void Fail()
{
throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport}",
new AggregateException(failures.Select(f => f.Exception)));
}
private string GetFailureReport()
{
return Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => f.Describe()));
}
private TimeSpan Duration()
{
return DateTime.UtcNow - start;
}
}
}
public class Failure
{
public Failure(Exception exception, TimeSpan duration, int tryNumber)
{
Exception = exception;
Duration = duration;
TryNumber = tryNumber;
}
public Exception Exception { get; }
public TimeSpan Duration { get; }
public int TryNumber { get; }
public string Describe()
{
return $"Try {TryNumber} failed after {Time.FormatDuration(Duration)} with exception '{Exception}'";
}
}
}
@@ -1,15 +1,27 @@
namespace ContinuousTests
namespace Utils
{
public class TaskFactory
{
private readonly object taskLock = new();
private readonly List<Task> activeTasks = new List<Task>();
public void Run(Action action)
public void Run(Action action, string name)
{
lock (taskLock)
{
activeTasks.Add(Task.Run(action).ContinueWith(CleanupTask, null));
activeTasks.Add(Task.Run(() => CatchException(action, name)).ContinueWith(CleanupTask, null));
}
}
private void CatchException(Action action, string name)
{
try
{
action();
}
catch (Exception ex)
{
Console.WriteLine($"Exception in task '{name}': " + ex);
}
}
+68 -58
View File
@@ -13,6 +13,17 @@
return task.Result;
}
public static void Wait(Task task)
{
task.Wait();
}
public static string FormatDuration(TimeSpan? d)
{
if (d == null) return "[NULL]";
return FormatDuration(d.Value);
}
public static string FormatDuration(TimeSpan d)
{
var result = "";
@@ -23,100 +34,99 @@
return result;
}
public static void WaitUntil(Func<bool> predicate)
public static TimeSpan ParseTimespan(string span)
{
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1));
span = span.Replace(" ", "").Replace(",", "");
var result = TimeSpan.Zero;
var number = "";
foreach (var c in span)
{
if (char.IsNumber(c)) number += c;
else
{
var value = Convert.ToInt32(number);
number = "";
if (c == 'd') result += TimeSpan.FromDays(value);
else if (c == 'h') result += TimeSpan.FromHours(value);
else if (c == 'm') result += TimeSpan.FromMinutes(value);
else if (c == 's') result += TimeSpan.FromSeconds(value);
else throw new Exception("Unknown time modifier: " + c);
}
}
if (!string.IsNullOrEmpty(number))
{
var value = Convert.ToInt32(number);
result += TimeSpan.FromSeconds(value);
}
return result;
}
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay)
public static void WaitUntil(Func<bool> predicate, string msg)
{
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1), msg);
}
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay, string msg)
{
var start = DateTime.UtcNow;
var tries = 1;
var state = predicate();
while (!state)
{
if (DateTime.UtcNow - start > timeout)
var duration = DateTime.UtcNow - start;
if (duration > timeout)
{
throw new TimeoutException("Operation timed out.");
throw new TimeoutException($"Operation timed out after {tries} tries over (total) {FormatDuration(duration)}. '{msg}'");
}
Sleep(retryDelay);
state = predicate();
tries++;
}
}
public static void Retry(Action action, string description)
{
Retry(action, 1, description);
Retry(action, TimeSpan.FromSeconds(30), description);
}
public static T Retry<T>(Func<T> action, string description)
{
return Retry(action, 1, description);
return Retry(action, TimeSpan.FromSeconds(30), description);
}
public static void Retry(Action action, int maxRetries, string description)
public static void Retry(Action action, TimeSpan maxTimeout, string description)
{
Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description);
}
public static T Retry<T>(Func<T> action, int maxRetries, string description)
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, string description)
{
return Retry(action, maxRetries, TimeSpan.FromSeconds(1), description);
return Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description);
}
public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description)
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var retries = 0;
var exceptions = new List<Exception>();
while (true)
{
if (retries > maxRetries)
{
var duration = DateTime.UtcNow - start;
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
}
try
{
action();
return;
}
catch (Exception ex)
{
exceptions.Add(ex);
retries++;
}
Sleep(retryTime);
}
Retry(action, maxTimeout, retryTime, description, f => { });
}
public static T Retry<T>(Func<T> action, int maxRetries, TimeSpan retryTime, string description)
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var retries = 0;
var exceptions = new List<Exception>();
while (true)
{
if (retries > maxRetries)
{
var duration = DateTime.UtcNow - start;
throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions));
}
return Retry(action, maxTimeout, retryTime, description, f => { });
}
try
{
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
retries++;
}
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
{
var r = new Retry(description, maxTimeout, retryTime, onFail);
r.Run(action);
}
Sleep(retryTime);
}
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
{
var r = new Retry(description, maxTimeout, retryTime, onFail);
return r.Run(action);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace Utils
{
public class TimeRange
{
public TimeRange(DateTime from, DateTime to)
{
if (from < to)
{
From = from;
To = to;
}
else
{
From = to;
To = from;
}
Duration = To - From;
}
public DateTime From { get; }
public DateTime To { get; }
public TimeSpan Duration { get; }
}
}
@@ -0,0 +1,68 @@
using CodexContractsPlugin.Marketplace;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public class ChainEvents
{
private ChainEvents(
BlockInterval blockInterval,
Request[] requests,
RequestFulfilledEventDTO[] fulfilled,
RequestCancelledEventDTO[] cancelled,
SlotFilledEventDTO[] slotFilled,
SlotFreedEventDTO[] slotFreed
)
{
BlockInterval = blockInterval;
Requests = requests;
Fulfilled = fulfilled;
Cancelled = cancelled;
SlotFilled = slotFilled;
SlotFreed = slotFreed;
}
public BlockInterval BlockInterval { get; }
public Request[] Requests { get; }
public RequestFulfilledEventDTO[] Fulfilled { get; }
public RequestCancelledEventDTO[] Cancelled { get; }
public SlotFilledEventDTO[] SlotFilled { get; }
public SlotFreedEventDTO[] SlotFreed { get; }
public IHasBlock[] All
{
get
{
var all = new List<IHasBlock>();
all.AddRange(Requests);
all.AddRange(Fulfilled);
all.AddRange(Cancelled);
all.AddRange(SlotFilled);
all.AddRange(SlotFreed);
return all.ToArray();
}
}
public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval)
{
return FromContractEvents(contracts.GetEvents(blockInterval));
}
public static ChainEvents FromTimeRange(ICodexContracts contracts, TimeRange timeRange)
{
return FromContractEvents(contracts.GetEvents(timeRange));
}
public static ChainEvents FromContractEvents(ICodexContractsEvents events)
{
return new ChainEvents(
events.BlockInterval,
events.GetStorageRequests(),
events.GetRequestFulfilledEvents(),
events.GetRequestCancelledEvents(),
events.GetSlotFilledEvents(),
events.GetSlotFreedEvents()
);
}
}
}
@@ -0,0 +1,156 @@
using CodexContractsPlugin.Marketplace;
using Logging;
using System.Numerics;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateChangeHandler
{
void OnNewRequest(IChainStateRequest request);
void OnRequestFinished(IChainStateRequest request);
void OnRequestFulfilled(IChainStateRequest request);
void OnRequestCancelled(IChainStateRequest request);
void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex);
void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex);
}
public class ChainState
{
private readonly List<ChainStateRequest> requests = new List<ChainStateRequest>();
private readonly ILog log;
private readonly ICodexContracts contracts;
private readonly IChainStateChangeHandler handler;
public ChainState(ILog log, ICodexContracts contracts, IChainStateChangeHandler changeHandler, DateTime startUtc)
{
this.log = new LogPrefixer(log, "(ChainState) ");
this.contracts = contracts;
handler = changeHandler;
StartUtc = startUtc;
TotalSpan = new TimeRange(startUtc, startUtc);
}
public TimeRange TotalSpan { get; private set; }
public IChainStateRequest[] Requests => requests.ToArray();
public DateTime StartUtc { get; }
public void Update()
{
Update(DateTime.UtcNow);
}
public void Update(DateTime toUtc)
{
var span = new TimeRange(TotalSpan.To, toUtc);
var events = ChainEvents.FromTimeRange(contracts, span);
Apply(events);
TotalSpan = new TimeRange(TotalSpan.From, span.To);
}
private void Apply(ChainEvents events)
{
if (events.BlockInterval.TimeRange.From < TotalSpan.From)
throw new Exception("Attempt to update ChainState with set of events from before its current record.");
log.Log($"ChainState updating: {events.BlockInterval}");
// Run through each block and apply the events to the state in order.
var span = events.BlockInterval.TimeRange.Duration;
var numBlocks = events.BlockInterval.NumberOfBlocks;
var spanPerBlock = span / numBlocks;
var eventUtc = events.BlockInterval.TimeRange.From;
for (var b = events.BlockInterval.From; b <= events.BlockInterval.To; b++)
{
var blockEvents = events.All.Where(e => e.Block.BlockNumber == b).ToArray();
ApplyEvents(b, blockEvents, eventUtc);
eventUtc += spanPerBlock;
}
}
private void ApplyEvents(ulong blockNumber, IHasBlock[] blockEvents, DateTime eventsUtc)
{
foreach (var e in blockEvents)
{
dynamic d = e;
ApplyEvent(d);
}
ApplyTimeImplicitEvents(blockNumber, eventsUtc);
}
private void ApplyEvent(Request request)
{
if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId)))
throw new Exception("Received NewRequest event for id that already exists.");
var newRequest = new ChainStateRequest(log, request, RequestState.New);
requests.Add(newRequest);
handler.OnNewRequest(newRequest);
}
private void ApplyEvent(RequestFulfilledEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.UpdateState(request.Block.BlockNumber, RequestState.Started);
handler.OnRequestFulfilled(r);
}
private void ApplyEvent(RequestCancelledEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.UpdateState(request.Block.BlockNumber, RequestState.Cancelled);
handler.OnRequestCancelled(r);
}
private void ApplyEvent(SlotFilledEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.Hosts.Add(request.Host, (int)request.SlotIndex);
r.Log($"[{request.Block.BlockNumber}] SlotFilled (host:'{request.Host}', slotIndex:{request.SlotIndex})");
handler.OnSlotFilled(r, request.SlotIndex);
}
private void ApplyEvent(SlotFreedEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.Hosts.RemoveHost((int)request.SlotIndex);
r.Log($"[{request.Block.BlockNumber}] SlotFreed (slotIndex:{request.SlotIndex})");
handler.OnSlotFreed(r, request.SlotIndex);
}
private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc)
{
foreach (var r in requests)
{
if (r.State == RequestState.Started
&& r.FinishedUtc < eventsUtc)
{
r.UpdateState(blockNumber, RequestState.Finished);
handler.OnRequestFinished(r);
}
}
}
private ChainStateRequest? FindRequest(byte[] requestId)
{
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId));
if (r == null) log.Log("Unable to find request by ID!");
return r;
}
private bool Equal(byte[] a, byte[] b)
{
return a.SequenceEqual(b);
}
}
}
@@ -0,0 +1,80 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateRequest
{
Request Request { get; }
RequestState State { get; }
DateTime ExpiryUtc { get; }
DateTime FinishedUtc { get; }
EthAddress Client { get; }
RequestHosts Hosts { get; }
}
public class ChainStateRequest : IChainStateRequest
{
private readonly ILog log;
public ChainStateRequest(ILog log, Request request, RequestState state)
{
this.log = log;
Request = request;
State = state;
ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
Log($"[{request.Block.BlockNumber}] Created as {State}.");
Client = new EthAddress(request.Client);
Hosts = new RequestHosts();
}
public Request Request { get; }
public RequestState State { get; private set; }
public DateTime ExpiryUtc { get; }
public DateTime FinishedUtc { get; }
public EthAddress Client { get; }
public RequestHosts Hosts { get; }
public void UpdateState(ulong blockNumber, RequestState newState)
{
Log($"[{blockNumber}] Transit: {State} -> {newState}");
State = newState;
}
public void Log(string msg)
{
log.Log($"Request '{Request.Id}': {msg}");
}
}
public class RequestHosts
{
private readonly Dictionary<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
public void Add(EthAddress host, int index)
{
hosts.Add(index, host);
}
public void RemoveHost(int index)
{
hosts.Remove(index);
}
public EthAddress? GetHost(int index)
{
if (!hosts.ContainsKey(index)) return null;
return hosts[index];
}
public EthAddress[] GetHosts()
{
return hosts.Values.ToArray();
}
}
}
@@ -0,0 +1,31 @@
using System.Numerics;
namespace CodexContractsPlugin.ChainMonitor
{
public class DoNothingChainEventHandler : IChainStateChangeHandler
{
public void OnNewRequest(IChainStateRequest request)
{
}
public void OnRequestCancelled(IChainStateRequest request)
{
}
public void OnRequestFinished(IChainStateRequest request)
{
}
public void OnRequestFulfilled(IChainStateRequest request)
{
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
{
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)
{
}
}
}
@@ -1,5 +1,12 @@
using GethPlugin;
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.ABI;
using Nethereum.Util;
using NethereumWorkflow;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Utils;
namespace CodexContractsPlugin
{
@@ -7,45 +14,109 @@ namespace CodexContractsPlugin
{
CodexContractsDeployment Deployment { get; }
void MintTestTokens(IGethNode gethNode, IHasEthAddress owner, TestToken testTokens);
void MintTestTokens(IGethNode gethNode, EthAddress ethAddress, TestToken testTokens);
TestToken GetTestTokenBalance(IGethNode gethNode, IHasEthAddress owner);
TestToken GetTestTokenBalance(IGethNode gethNode, EthAddress ethAddress);
bool IsDeployed();
string MintTestTokens(IHasEthAddress owner, TestToken testTokens);
string MintTestTokens(EthAddress ethAddress, TestToken testTokens);
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
ICodexContractsEvents GetEvents(TimeRange timeRange);
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
}
[JsonConverter(typeof(StringEnumConverter))]
public enum RequestState
{
New,
Started,
Cancelled,
Finished,
Failed
}
public class CodexContractsAccess : ICodexContracts
{
private readonly ILog log;
private readonly IGethNode gethNode;
public CodexContractsAccess(ILog log, CodexContractsDeployment deployment)
public CodexContractsAccess(ILog log, IGethNode gethNode, CodexContractsDeployment deployment)
{
this.log = log;
this.gethNode = gethNode;
Deployment = deployment;
}
public CodexContractsDeployment Deployment { get; }
public void MintTestTokens(IGethNode gethNode, IHasEthAddress owner, TestToken testTokens)
public bool IsDeployed()
{
MintTestTokens(gethNode, owner.EthAddress, testTokens);
return !string.IsNullOrEmpty(StartInteraction().GetTokenName(Deployment.TokenAddress));
}
public void MintTestTokens(IGethNode gethNode, EthAddress ethAddress, TestToken testTokens)
public string MintTestTokens(IHasEthAddress owner, TestToken testTokens)
{
var interaction = new ContractInteractions(log, gethNode);
interaction.MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
return MintTestTokens(owner.EthAddress, testTokens);
}
public TestToken GetTestTokenBalance(IGethNode gethNode, IHasEthAddress owner)
public string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
{
return GetTestTokenBalance(gethNode, owner.EthAddress);
return StartInteraction().MintTestTokens(ethAddress, testTokens.TstWei, Deployment.TokenAddress);
}
public TestToken GetTestTokenBalance(IGethNode gethNode, EthAddress ethAddress)
public TestToken GetTestTokenBalance(IHasEthAddress owner)
{
var interaction = new ContractInteractions(log, gethNode);
var balance = interaction.GetBalance(Deployment.TokenAddress, ethAddress.Address);
return balance.TestTokens();
return GetTestTokenBalance(owner.EthAddress);
}
public TestToken GetTestTokenBalance(EthAddress ethAddress)
{
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
return balance.TstWei();
}
public ICodexContractsEvents GetEvents(TimeRange timeRange)
{
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
}
public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
{
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
{
var encoder = new ABIEncode();
var encoded = encoder.GetABIEncoded(
new ABIValue("bytes32", storageRequest.RequestId),
new ABIValue("uint256", slotIndex.ToBig())
);
var hashed = Sha3Keccack.Current.CalculateHash(encoded);
var func = new GetHostFunction
{
SlotId = hashed
};
var address = gethNode.Call<GetHostFunction, string>(Deployment.MarketplaceAddress, func);
if (string.IsNullOrEmpty(address)) return null;
return new EthAddress(address);
}
public RequestState GetRequestState(Request request)
{
var func = new RequestStateFunction
{
RequestId = request.RequestId
};
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private ContractInteractions StartInteraction()
{
return new ContractInteractions(log, gethNode);
}
}
}
@@ -1,10 +1,13 @@
using KubernetesWorkflow;
using GethPlugin;
using KubernetesWorkflow;
using KubernetesWorkflow.Recipe;
using Logging;
namespace CodexContractsPlugin
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-1854dfb-dist-tests";
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests";
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
@@ -16,10 +19,11 @@ namespace CodexContractsPlugin
{
var config = startupConfig.Get<CodexContractsContainerConfig>();
var ip = config.GethNode.StartResult.Container.Pod.PodInfo.Ip;
var port = config.GethNode.StartResult.HttpPort.Number;
var address = config.GethNode.StartResult.Container.GetAddress(new NullLog(), GethContainerRecipe.HttpPortTag);
AddEnvVar("DISTTEST_NETWORK_URL", $"http://{ip}:{port}");
SetSchedulingAffinity(notIn: "false");
AddEnvVar("DISTTEST_NETWORK_URL", address.ToString());
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
AddEnvVar("KEEP_ALIVE", "1");
}
@@ -0,0 +1,108 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.Hex.HexTypes;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace CodexContractsPlugin
{
public interface ICodexContractsEvents
{
BlockInterval BlockInterval { get; }
Request[] GetStorageRequests();
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
RequestCancelledEventDTO[] GetRequestCancelledEvents();
SlotFilledEventDTO[] GetSlotFilledEvents();
SlotFreedEventDTO[] GetSlotFreedEvents();
}
public class CodexContractsEvents : ICodexContractsEvents
{
private readonly ILog log;
private readonly IGethNode gethNode;
private readonly CodexContractsDeployment deployment;
public CodexContractsEvents(ILog log, IGethNode gethNode, CodexContractsDeployment deployment, BlockInterval blockInterval)
{
this.log = log;
this.gethNode = gethNode;
this.deployment = deployment;
BlockInterval = blockInterval;
}
public BlockInterval BlockInterval { get; }
public Request[] GetStorageRequests()
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
var i = new ContractInteractions(log, gethNode);
return events
.Select(e =>
{
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents()
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents()
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents()
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
}
}
@@ -35,10 +35,10 @@ namespace CodexContractsPlugin
return starter.Deploy(ci, gethNode);
}
public ICodexContracts WrapDeploy(CodexContractsDeployment deployment)
public ICodexContracts WrapDeploy(IGethNode gethNode, CodexContractsDeployment deployment)
{
deployment = SerializeGate.Gate(deployment);
return starter.Wrap(deployment);
return starter.Wrap(gethNode, deployment);
}
}
}

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