Compare commits

...
Sign in to create a new pull request.

383 commits

Author SHA1 Message Date
c6c6358426 Fix warnings.
Some checks failed
ci/woodpecker/push/build Pipeline was successful
CodeQL / Analyze (push) Has been cancelled
Java CI with Gradle / build (push) Has been cancelled
Deploy Jekyll site to Pages / build (push) Has been cancelled
Deploy Jekyll site to Pages / deploy (push) Has been cancelled
2025-08-11 20:32:08 +02:00
470c266157 Build with woodpecker (#1)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Java CI with Gradle / build (push) Waiting to run
Deploy Jekyll site to Pages / build (push) Waiting to run
Deploy Jekyll site to Pages / deploy (push) Blocked by required conditions
ci/woodpecker/push/build Pipeline was successful
Reviewed-on: #1
Co-authored-by: Michael N. Lipp <mnl@mnl.de>
Co-committed-by: Michael N. Lipp <mnl@mnl.de>
2025-08-11 12:50:28 +02:00
7b8df80828 Use display manager for login.
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Java CI with Gradle / build (push) Waiting to run
Deploy Jekyll site to Pages / build (push) Waiting to run
Deploy Jekyll site to Pages / deploy (push) Blocked by required conditions
2025-08-01 17:41:03 +02:00
fccf2a6b65 Fix branch evaluation.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-07-26 22:43:45 +02:00
00e9affee4 Fix evaluation of template source. 2025-07-26 15:18:22 +02:00
fa84110e1d Handle configuration value properly. 2025-07-14 17:34:09 +02:00
76b579c404 Add key, allowing vue to optimize.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-05-04 11:36:55 +02:00
a5433c869b Upgrade webconsole base library. 2025-05-03 22:29:42 +02:00
10f3028f06 Increase concurrency and avoid race condition. 2025-04-30 16:27:15 +02:00
b7fad4614d Improve debug messages.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-04-29 14:02:12 +02:00
7d298ce24b Clarify intend. 2025-04-14 21:39:35 +02:00
6ef4c2aaa2 Fix return value. 2025-04-14 12:08:48 +02:00
5bcf0ba051 Add umami to javadoc. 2025-04-13 17:01:38 +02:00
d67f374de7 Try umami. 2025-04-13 16:48:42 +02:00
2b3420c801 Update component picture.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-31 12:21:47 +02:00
bd54d293eb Update picture. 2025-03-30 21:58:22 +02:00
cb2ae7c33e Update. 2025-03-30 21:32:55 +02:00
85a9b41046 Update picture. 2025-03-30 21:12:18 +02:00
fb976802cf Minor edit. 2025-03-30 13:05:33 +02:00
af112bb66b Editorial changes. 2025-03-30 12:37:49 +02:00
592b30f6c5 Update state diagram. 2025-03-30 12:17:14 +02:00
c716e32534 Make tests work again. 2025-03-30 11:42:03 +02:00
c79d678a2a More consistent logging. 2025-03-29 18:38:09 +01:00
f30ea79abb Minor editorial changes. 2025-03-29 17:41:01 +01:00
d7d5c860a2 Merge branch 'wip/optimize' 2025-03-29 15:10:32 +01:00
991763f228 Optimize state change handling.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-29 15:09:38 +01:00
db49f5ba2f Restart non-deleted pods. 2025-03-28 21:28:55 +01:00
2e70ef2b98 Merge branch 'feature/pod-restart' 2025-03-28 18:03:54 +01:00
e8097d87d9 Let the operator manage pod restarts. 2025-03-28 18:03:09 +01:00
7a70d73330 Merge branch 'feature/pools'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-24 15:05:31 +01:00
3143a1be93 Remove no longer valid optimization.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-24 15:04:39 +01:00
4bcbafb4f1 Improve label. 2025-03-22 11:25:02 +01:00
331b6d8d61 Minor edit. 2025-03-21 09:18:38 +01:00
725fb663c8 Merge branch 'feature/pools'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-20 18:47:46 +01:00
d9799df861 Delete assignments when pool is deleted. 2025-03-20 18:46:45 +01:00
fe1d56517b Reorganize handlers. 2025-03-20 18:29:45 +01:00
359b1fdb84 Clarify pipeline usage. 2025-03-20 18:02:14 +01:00
16a15bc9ad Document memory allocation. 2025-03-20 09:33:10 +01:00
7644e65ab0 Merge branch 'wip/optimize' 2025-03-19 22:59:14 +01:00
dbc89e6e09 Avoid unnecessary processing. 2025-03-19 22:57:58 +01:00
9baf9b7673 Reset runner info when pod is deleted. 2025-03-18 21:48:10 +01:00
3686629a28 Fix race condition. 2025-03-18 16:44:15 +01:00
5991fe0d2d Merge branch 'wip/optimize' 2025-03-17 16:50:29 +01:00
3b0a4c8a23 Rate limit for RAM size updates. 2025-03-17 16:49:10 +01:00
5ca45d7620 Minor edit.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-16 23:12:22 +01:00
efd489b22f Allow VM operator to watch pods. 2025-03-16 23:03:19 +01:00
9644e5fd83 Improve debug message. 2025-03-16 23:02:59 +01:00
fe18bb3cdf Consolidate debug messages. 2025-03-16 23:02:44 +01:00
9a557f5019 Merge branch 'wip/optimize' 2025-03-16 17:08:40 +01:00
fd0f4f8eb2 Fetch display secret only when needed. 2025-03-16 17:01:36 +01:00
9bd17e8899 Update. 2025-03-16 15:44:27 +01:00
227c097c01 Actively add pod info, don't run queries. 2025-03-16 15:13:16 +01:00
ce4d0bfb72 More features, more resources. 2025-03-16 14:53:01 +01:00
017607f2e2 Prune not required data before transfer. 2025-03-15 15:21:44 +01:00
fcdb537f35 Merge branch 'fix/condition-update' 2025-03-15 12:35:23 +01:00
5309460fbf Prevent update of lastTransitionTime if we have no transition. 2025-03-15 12:33:20 +01:00
dc228295d1 Update. 2025-03-15 11:25:13 +01:00
1b1e5ffb8c Reduce default logging. 2025-03-14 21:16:01 +01:00
a3d6db3178 Merge branch 'feature/vm-info'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-14 18:39:34 +01:00
197c21bc32 More consistent logging. 2025-03-14 18:37:43 +01:00
8bc413b2da Update templates when testing. 2025-03-14 18:37:05 +01:00
ba582d877e Display runner version in GUI. 2025-03-14 17:52:25 +01:00
3a4404b758 Add runner version to status. 2025-03-14 16:57:58 +01:00
f0ebea5353 Use two digit numbers for VMs. 2025-03-14 12:33:59 +01:00
084bdd1f46 Merge branch 'feature/auto-login' 2025-03-14 11:22:53 +01:00
5947bd3684 Avoid unnecessary config map changes. 2025-03-14 11:21:29 +01:00
c02b3d99cb Restore reasonable default. 2025-03-13 18:42:52 +01:00
407aa4b4d1 Merge branch 'fix/runner-poweroff' 2025-03-13 18:40:55 +01:00
3df01fcad0 Add debug messages. 2025-03-13 18:19:09 +01:00
2d16cbc352 Add powerdown through guest agent (more reliable). 2025-03-13 17:45:28 +01:00
2a3774ae24 Minor edits. 2025-03-13 15:46:11 +01:00
d637cb2c72 Move constants to separate class. 2025-03-13 10:34:36 +01:00
f493a2c582 Use process exit as termination confirmation. 2025-03-13 10:25:32 +01:00
72e1b8a580 Merge branch 'fix/runner-poweroff' 2025-03-12 21:50:56 +01:00
ecb43db83e Test configuration. 2025-03-12 21:48:18 +01:00
2a33f468f2 Track connection closing. 2025-03-12 21:46:46 +01:00
36877666f3 No QMP poweroff if QMP not available. 2025-03-12 20:59:36 +01:00
fae75dafa9 Merge branch 'fix/race-condition' 2025-03-12 17:45:07 +01:00
46f079504c Update. 2025-03-12 17:40:58 +01:00
5d0c6c6423 Fix startup. 2025-03-12 17:22:46 +01:00
19968ab73e Move code to agent. 2025-03-12 15:04:03 +01:00
68a688c4ce Intermediate state. 2025-03-12 11:56:32 +01:00
44868464b9 Update title. 2025-03-09 17:07:08 +01:00
61286a528c Add hint. 2025-03-09 16:43:19 +01:00
ce1a9afec7 Fix layout. 2025-03-09 15:52:35 +01:00
591278a07f Edit README.md 2025-03-08 05:37:40 +00:00
29bc6f539c Add hint regarding requirements. 2025-03-07 20:52:10 +01:00
2aa4116e95 Merge branch 'main' into feature/pools 2025-03-07 13:27:35 +01:00
7104984ac7 Consistent spelling for QEMU.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2025-03-07 13:26:31 +01:00
7f7306fc4a Merge branch 'feature/pools' 2025-03-06 23:32:05 +01:00
7130c128bb Add another environment setting. 2025-03-06 23:30:57 +01:00
3557e5fc27 Merge branch 'feature/pools' 2025-03-06 14:44:41 +01:00
f3907ffae9 Fix some markdown style issues. 2025-03-06 14:40:50 +01:00
9459c367ac PMD is wrong. 2025-03-06 13:03:04 +01:00
c5a00acf3d "Fix" PMD warning. 2025-03-06 13:02:53 +01:00
1a8412d767 Fix deepCopy for arrays.
Isn't used in project.
2025-03-06 12:59:06 +01:00
21d2fe2dbd Merge branch 'main' into feature/auto-login 2025-03-06 11:46:31 +01:00
607379b06d Merge branch 'feature/auto-login' 2025-03-06 11:45:36 +01:00
c51da8650a Editorial changes. 2025-03-06 11:43:32 +01:00
12c72b3f52 Update link. 2025-03-06 09:16:05 +01:00
7dc68b5ac7 Add documentation. 2025-03-05 23:24:03 +01:00
5ae162445c Add number format. 2025-03-05 18:10:54 +01:00
fb73a17748 Fix link. 2025-03-05 15:38:08 +01:00
961098a984 Merge branch 'main' into feature/auto-login 2025-03-05 15:37:54 +01:00
0e57a4e862 Merge branch 'feature/auto-login' 2025-03-05 13:37:09 +01:00
2524172c12 Centralize evaluation of console accessibility. 2025-03-05 13:02:00 +01:00
7437a17c9f Moved missed condition. 2025-03-05 10:24:40 +01:00
7e650bf980 Use more constants. 2025-03-05 10:18:16 +01:00
940913cf89 Use UserLoggedIn condition for access control. 2025-03-04 23:39:22 +01:00
04ccdd7dee Add condition for logged in state. 2025-03-04 21:35:36 +01:00
bfe4c2bb32 Set loggedInUser via Reconciler. 2025-03-04 13:33:54 +01:00
28b1903acc Extend accessibility check. 2025-03-04 09:00:13 +01:00
dfe3038463 So auto login according to pool setting. 2025-03-03 21:26:10 +01:00
701194799f Reduce to secret query. 2025-03-03 21:19:34 +01:00
17e2a7c6f0 Fix some codacy issues. 2025-03-03 12:39:50 +01:00
eb3979dc83 Consistent indentation. 2025-03-03 11:59:40 +01:00
e871fc059b Add bing site auth. 2025-03-03 11:11:55 +01:00
d4fdd4209f Activate sitemap. 2025-03-03 10:57:23 +01:00
51a72a162d Sitemap generated by jekyll is incomplete. 2025-03-03 10:50:58 +01:00
fa53a07a52 Update. 2025-03-03 10:41:16 +01:00
6b6a33e702 Need full fetch for javadoc build. 2025-03-03 10:21:33 +01:00
cc78c38efe Remove no longer needed sub-directory. 2025-03-03 10:16:05 +01:00
c004265f5e Don't merge into jdrupes.org any more. 2025-03-03 09:25:58 +01:00
5c7a9f6e5f Merge branch 'feature/auto-login' 2025-03-03 09:21:05 +01:00
03fdabe85a Edit README.md 2025-03-03 06:50:03 +00:00
214085119c Add style for searching. 2025-03-02 23:15:19 +01:00
30bc119178 Update. 2025-03-02 22:57:22 +01:00
987f634f40 Update. 2025-03-02 22:33:22 +01:00
083c6db2da Build own site. 2025-03-02 22:12:43 +01:00
7670857d0a Separate sites. 2025-03-02 21:50:24 +01:00
d6e2a92fe8 Fix url. 2025-03-02 21:20:25 +01:00
d8cff8b942 Track vm-operator separately. 2025-03-02 21:06:26 +01:00
4965845f3d Move robots.txt. 2025-03-02 20:44:39 +01:00
a0dfd25192 Add robots.txt. 2025-03-02 20:29:56 +01:00
fd0bcc9307 Move main site to vm-operator.jdrupes.org. 2025-03-02 20:10:42 +01:00
0e28bcd038 Change (main) site. 2025-03-02 18:48:13 +01:00
60349bca78 Shorten title to make bing happy. 2025-03-02 13:45:38 +01:00
199cd8185e Fix robots.txt "generation". 2025-03-02 13:15:38 +01:00
f6338758d8 Sitemap property must be an absolute url. 2025-03-02 13:13:03 +01:00
3a94602a0d Add sitemap. 2025-03-02 13:02:46 +01:00
07fb07a6a4 Javadoc is hosted on main site only. 2025-03-02 12:57:26 +01:00
687a050ec4 Generate sitemap. 2025-03-02 12:55:40 +01:00
2f6b3d2127 Fix footer. 2025-03-02 12:39:48 +01:00
05d53c58b1 Fix footer. 2025-03-02 12:28:08 +01:00
d7af1f5d06 Fix footer. 2025-03-02 12:23:02 +01:00
6b7c78ed2c Add readthedocs specific footer. 2025-03-02 12:13:33 +01:00
502842f486 Trigger generation of a canonical url. 2025-03-02 12:05:32 +01:00
5b7531c5e5 Add for readthedocs. 2025-03-02 11:51:47 +01:00
d0298eb7e8 Update. 2025-03-02 11:32:36 +01:00
01db49397a Try readthedocs. 2025-03-02 11:28:08 +01:00
f8cc26e657 Define some more constants. 2025-03-01 22:51:51 +01:00
41ae658e0c Reorganize imports. 2025-03-01 21:51:33 +01:00
e822d472f9 Optimize status update. 2025-03-01 17:44:52 +01:00
4a242c4657 Clear logged in user on startup and shutdown. 2025-03-01 17:12:02 +01:00
62a7210117 Fix and clarify usage of methods for status update. 2025-03-01 17:00:14 +01:00
5e282c4d2b Move automatic login request to CRD.
Undoes reorganize constants.
2025-03-01 15:44:05 +01:00
5366e24092 Move automatic login request to CRD.
Also reorganizes constants.
2025-03-01 11:02:52 +01:00
3152ff842b Allow console take over. 2025-02-27 22:55:50 +01:00
b409443499 Fix icon. 2025-02-27 22:54:19 +01:00
5ec220d0a6 Use gids for id management and isolate home directories. 2025-02-27 17:41:57 +01:00
5cbdab9da8 Fix log message. 2025-02-27 14:55:31 +01:00
b4cb3b8694 Control login. 2025-02-27 14:46:13 +01:00
59bf4937ef Avoid multi-line message. 2025-02-27 14:45:25 +01:00
3119349450 Fix user switching. 2025-02-26 22:33:08 +01:00
b4bc0c7b0f Delay enable until VM operator agent started. 2025-02-26 22:00:32 +01:00
a1e941276e Working login script. 2025-02-26 21:59:38 +01:00
4a7a309f07 Get started with vmop-agent. 2025-02-25 15:43:47 +01:00
d2c39dc06a Rename. 2025-02-25 13:48:23 +01:00
2119c215fc Prevent publishing doc in branches (except main). 2025-02-25 10:44:56 +01:00
d1bc335db9 Prepare auto login. 2025-02-24 21:21:58 +01:00
c6704c886f Avoid duplicate constants. 2025-02-24 18:18:29 +01:00
bc33640c98 Avoid duplicate constants. 2025-02-24 18:09:14 +01:00
ddab466fd0 Restrict pagefind search to project. 2025-02-24 14:03:49 +01:00
c45c452c83 Adjust class name. 2025-02-24 13:20:28 +01:00
e3b5f5a04d Refactor QEMU socket connection handling and start vmop agent. 2025-02-24 11:58:13 +01:00
f236b376ae Back to testing. 2025-02-23 12:05:55 +01:00
558f4d96c9 Add pagefind. 2025-02-23 12:00:27 +01:00
5b8b47f95c Add some metadata to make bing happy. 2025-02-23 11:47:13 +01:00
3012da3e87 Add login information to display secret. 2025-02-23 11:14:46 +01:00
0828d03835 Javadoc fixes. 2025-02-22 21:27:39 +01:00
81b128e4a3 Clarify responsibilities of display secret monitor and reconciler. 2025-02-22 21:24:58 +01:00
e291352828 Prepare usage of guest os command. 2025-02-21 20:54:27 +01:00
5ad052ffe4 Delay console opening for pool VMs. 2025-02-19 21:04:08 +01:00
c582763fbf Merge branch 'feature/booted-status' 2025-02-18 16:53:22 +01:00
777ae73c74 Add OS icons. 2025-02-18 16:50:43 +01:00
bccc4ac219 Add pretty os name to displayed data. 2025-02-18 12:15:50 +01:00
ec8152bd51 Add booted state. 2025-02-17 20:47:00 +01:00
e4bba582a0 Merge branch 'prep/v4.0.0' 2025-02-17 10:23:23 +01:00
0287ae7998 Merge branch 'testing' 2025-02-17 10:20:31 +01:00
46cb2466fe Merge branch 'feature/auto-login' into prep/v4.0.0 2025-02-17 09:52:37 +01:00
3e713b4ff2 Extend comment. 2025-02-17 09:49:06 +01:00
0ded0ff9a9 Add usage info. 2025-02-16 21:06:49 +01:00
5078001f4b Add guest agent client and retrieve OS info. 2025-02-10 22:24:10 +01:00
1fc26647b6 Add maintenance script. 2025-02-02 16:50:52 +01:00
aea8b9540e Merge branch 'prep/v4.0.0' into testing 2025-02-02 13:59:44 +01:00
d27339b1e9 Make VM extra data a class. 2025-02-02 13:54:10 +01:00
d5e589709f Handle conflict properly. 2025-02-02 12:10:22 +01:00
21108771d9 Fix warning. 2025-02-01 22:10:47 +01:00
b7ea6860ff Adapt name to previous change. 2025-02-01 22:08:57 +01:00
85a4521299 Combine VmDefinitionModel and VmDefinition. 2025-02-01 22:06:30 +01:00
b250398213 Merge branch 'testing' into prep/v4.0.0 2025-02-01 18:53:06 +01:00
54747b25e8 Use VmChannel's event pipeline to update assignment. 2025-02-01 18:51:19 +01:00
9986e4c8bf Merge branch 'prep/v4.0.0' into testing 2025-01-31 22:22:27 +01:00
b5ae22a8ea Avoid duplicate assignment. 2025-01-31 22:09:17 +01:00
b78b33a6f1 Merge branch 'prep/v4.0.0' 2025-01-31 15:58:56 +01:00
b159bae5da Allow users to start assigned VMs. 2025-01-31 15:26:42 +01:00
23bc41d68d Prefer running VMs for new assignments. 2025-01-31 15:26:25 +01:00
6a1273e701 Prevent concurrent modification exception. 2025-01-31 12:24:21 +01:00
4fc0d6fc63 Support both string and boolean for deleteConnectionFile. 2025-01-31 11:15:43 +01:00
ecd7ba7baf Fix trailing space. 2025-01-30 22:32:51 +01:00
150b9f2908 Fix spaces. 2025-01-30 22:14:09 +01:00
29dd6aab82 Javadoc fixes. 2025-01-30 22:04:41 +01:00
99c96e44c3 Allow access to vmpools. 2025-01-30 22:00:10 +01:00
1f4d69075a Merge branch 'fix/cloud-init-hostname' into 'main'
Generate metaData even if only cloudInit is specified.

See merge request org/jdrupes/vm-operator!13
2025-01-30 19:56:39 +00:00
ad79e8542a Merge branch 'fix/cloud-init-hostname' into testing 2025-01-30 16:57:26 +01:00
e447a944dc Generate metaData even if only cloudInit is specified. 2025-01-30 16:55:54 +01:00
49566584a2 Merge branch 'feature/console-action' into 'main'
Add more actions to VM management conlet.

See merge request org/jdrupes/vm-operator!12
2025-01-30 11:47:42 +00:00
e4e00c8ec8 Merge branch 'feature/console-action' into testing 2025-01-29 21:06:41 +01:00
ebda41346a Simplify permission management. 2025-01-29 21:01:49 +01:00
8d96307bb5 Add reset action to VM management. 2025-01-29 18:42:10 +01:00
af41c78c07 Add console access to VM management. 2025-01-29 17:33:16 +01:00
5cd4edcec1 Don't add channels until fully initialized. 2025-01-28 18:09:31 +01:00
85be5b9cbf Merge branch 'feature/pools' into 'main'
Add VM pools

See merge request org/jdrupes/vm-operator!11
2025-01-27 11:50:53 +00:00
50ad911265 Merge branch 'feature/pools' into testing 2025-01-27 11:11:25 +01:00
86f6ece264 Adjust auto close time. 2025-01-26 21:55:24 +01:00
1b5ad5b73e Prevent unauthorized console take over. 2025-01-26 21:49:37 +01:00
3ca632c8da Exchange columns. 2025-01-26 17:21:36 +01:00
e7cc7cc879 Enhance console connection entry. 2025-01-26 15:58:38 +01:00
981cbe2744 Improve readability. 2025-01-26 14:52:26 +01:00
224855efd3 Disable empty lists. 2025-01-25 22:27:52 +01:00
aaf1a0c545 Add operator for testing. 2025-01-25 22:27:07 +01:00
53a58a2aca Add users for pool testing. 2025-01-25 15:14:58 +01:00
574ad5226b Fix typescript error. 2025-01-25 14:33:56 +01:00
a0d626cc31 Fix warnings. 2025-01-25 13:40:51 +01:00
2a70c74234 Use consistent method names. 2025-01-25 13:39:22 +01:00
5d722abd2e Add assignment based on last usage. 2025-01-25 13:35:51 +01:00
877d4c69cd Remove obsolete method. 2025-01-25 13:34:16 +01:00
80d4165500 Upgrade base library. 2025-01-25 13:32:27 +01:00
a5ddf6ac97 Avoid updating immutable fields. 2025-01-25 13:32:00 +01:00
9318b1279a Move jackson to base library. 2025-01-23 21:17:06 +01:00
fb69c1d793 Minor style change. 2025-01-23 18:25:37 +01:00
edc3596e7d Move "used from" to details. 2025-01-23 18:05:38 +01:00
ba18e1f0d0 Add assigned to. 2025-01-23 17:35:46 +01:00
8799bcc8f2 Update permissions on VM change. 2025-01-23 15:33:01 +01:00
1cb90b0c94 Update GUI on pool permission changes. 2025-01-23 15:25:16 +01:00
6d5ba8829c Basically working. 2025-01-23 13:41:53 +01:00
d060a9334a Return only defined pools. 2025-01-18 17:50:33 +01:00
9b47ad3136 Don't duplicate pool management. 2025-01-18 17:16:54 +01:00
76be59a5b3 Don't duplicate VM management. 2025-01-18 17:02:30 +01:00
5bd6700541 Name not needed. 2025-01-15 22:06:00 +01:00
bd5227fda3 Simplify pool management. 2025-01-15 21:58:08 +01:00
4943baf3e3 Save assignment information. 2025-01-14 10:23:32 +01:00
15ac0721a6 Minor refactoring. 2025-01-14 10:22:56 +01:00
db7fbe2b7c Cleanup CR deletion. 2024-12-02 12:18:41 +01:00
84ac4bb28c Add hashCode and equals. 2024-12-01 16:43:35 +01:00
c3428ea4a5 Rename pool manager to monitor. 2024-12-01 14:22:18 +01:00
2dc93f1370 Unify permission usage. 2024-12-01 14:21:25 +01:00
367aebeee5 Pool configurable in GUI. 2024-12-01 13:41:18 +01:00
77cfcff2ed Fix toString. 2024-11-29 14:17:13 +01:00
4c600e7118 Merge branch 'main' into testing 2024-11-24 16:59:10 +01:00
e839f7b3b2 Rename conlet. 2024-11-23 14:08:45 +01:00
4ceaaa9fa2 Provide backward compatibility for configuration. 2024-11-23 14:08:01 +01:00
2be88d0f34 Remove viewer. 2024-11-23 13:06:51 +01:00
c361f9296d Remove conlet. 2024-11-23 13:03:04 +01:00
dc21dc8a7b Test pools. 2024-11-23 12:56:32 +01:00
64035d8dc1 Remove poolaccess conlet. 2024-11-23 12:55:48 +01:00
22446c3618 Clarify documentation. 2024-11-23 12:54:57 +01:00
5efef2a083 Deliver events on dedicated pipeline. 2024-11-23 12:54:29 +01:00
eabb2d9cf0 Add method. 2024-11-23 12:52:42 +01:00
27f983c18d Rename vmviewer to vmaccess. 2024-11-23 12:50:25 +01:00
00adeba625 Add pool manager. 2024-11-19 17:10:55 +01:00
0f4768d707 Merge timestamp fix. 2024-11-17 16:33:34 +01:00
5a68b976c8 Merge branch 'main' into testing 2024-11-17 16:33:13 +01:00
4690b897e9 Adapt to changed timestamp format.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-11-17 16:32:55 +01:00
043666a932 Generate test VMs. 2024-11-17 15:08:22 +01:00
bc0d25d00e Merge fix from main. 2024-11-17 13:21:16 +01:00
44ae5d405b Merge branch 'main' into testing 2024-11-17 13:20:37 +01:00
e7da41f838 Fix summary evaluation. 2024-11-17 13:20:20 +01:00
5d90a6a8a9 Move test to tc1. 2024-11-17 12:21:16 +01:00
1495301ec8 Merge branch 'main' into feature/pools 2024-11-16 17:34:35 +01:00
f513e6c395 Merge branch 'main' into testing 2024-11-16 17:34:06 +01:00
dec4c11785 Fix sync selection. 2024-11-16 17:33:37 +01:00
13cd262a47 New CRD. 2024-11-16 17:15:31 +01:00
a7ee3d0515 Merge branch 'main' into testing 2024-11-15 17:31:06 +01:00
31758b5ef1 Add fallbacks for efi vars as well. 2024-11-15 17:30:50 +01:00
97e94a8e9a Merge branch 'main' into testing 2024-11-15 15:29:37 +01:00
28df6ede15 Force fixed webconsole library. 2024-11-15 15:29:19 +01:00
d8132de6c2 Merge branch 'main' into testing 2024-11-15 09:59:18 +01:00
558d8f9548 Fix layout. 2024-11-15 09:58:30 +01:00
dc7382dc86 Always update console user. 2024-11-15 09:53:34 +01:00
6e3f554d8d Merge branch 'main' into testing 2024-11-14 22:10:57 +01:00
e864f677c3 Allow operator to patch CR status. 2024-11-14 22:10:18 +01:00
93a1a2b2f9 Merge branch 'main' into testing 2024-11-14 21:24:06 +01:00
69507b540c Improve messages. 2024-11-14 20:17:33 +01:00
0ba8d922ef Add used by information. 2024-11-14 18:46:19 +01:00
4ea568ea17 Automatically repeat status update in case of conflict. 2024-11-14 12:40:45 +01:00
811164f7b9 Move api client to base class. 2024-11-14 11:59:14 +01:00
f1d973502d Fix transparency. 2024-11-14 11:58:51 +01:00
4d447717c2 Improve tracking. 2024-11-13 23:45:54 +01:00
9773207307 Better name. 2024-11-13 11:14:53 +01:00
82a6d53156 Merge remote-tracking branch 'origin/main' into feature/track-clients 2024-11-13 11:03:21 +01:00
5ec7f58bbd Merge remote-tracking branch 'origin/main' into testing 2024-11-12 22:56:06 +01:00
228322748b Use 4m bios if smaller version is not available. 2024-11-12 22:41:54 +01:00
40cbeb694b Avoid NPE. 2024-11-12 22:04:15 +01:00
5897b4b386 Merge remote-tracking branch 'origin/main' into testing 2024-11-12 20:49:51 +01:00
65ceed93b6 Merge branch 'fix/choose-bios' into 'main'
Enhance logging

See merge request org/jdrupes/vm-operator!10
2024-11-12 19:29:46 +00:00
12d6745d75 Add some logging messages and enhance logging configurability. 2024-11-12 19:29:46 +00:00
0aaa375ffc Merge remote-tracking branch 'origin/main' into testing 2024-11-10 17:21:16 +01:00
b8aa925a49 Merge branch 'feature/track-clients' into 'main'
Feature/track clients

See merge request org/jdrupes/vm-operator!9
2024-11-10 16:19:46 +00:00
355eded86b Add tracking of client (viewer) connection state and visualize in GUI. 2024-11-10 16:19:46 +00:00
090d504b77 Add in-use visualization. 2024-11-10 17:10:08 +01:00
e5fd45ebcb Show console client. 2024-11-10 14:41:45 +01:00
12408143a7 Fix warning. 2024-11-10 14:15:17 +01:00
c7b65ca581 Report console connection events. 2024-11-10 14:13:34 +01:00
4d76225442 Fix image path. 2024-11-09 13:09:11 +01:00
9019907224 Add "testing" to branches with default versioning. 2024-11-09 13:03:45 +01:00
5b209c935e Pull from "testing" when using this branch. 2024-11-09 12:45:39 +01:00
3d446836b5 Add "testing" to branches with default versioning. 2024-11-09 12:41:22 +01:00
2d51421e19 Merge branch 'fix/viewer-control' into 'main'
Fix/viewer control

See merge request org/jdrupes/vm-operator!7
2024-11-09 11:08:59 +00:00
abe06b4658 Adapt viewer preview controls to permission changes. 2024-11-09 11:08:59 +00:00
a9c31a378e Merge branch 'feature/less-gson' into 'main'
Feature/less gson

See merge request org/jdrupes/vm-operator!6
2024-11-08 16:48:08 +00:00
c8781c2d8e Use less gson internally. 2024-11-08 16:48:07 +00:00
8e692a03fe Upgrade webconsole.base.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-10-26 21:40:23 +02:00
3708244571 Fix warnings. 2024-10-26 21:16:50 +02:00
23703bad3c Adapt to new webconsole base. 2024-10-26 21:11:50 +02:00
80fe921e6e Cleanup. 2024-10-26 20:47:00 +02:00
1bc63abadf Merge branch 'cleanup/v3.4' into 'main'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Prepare release v3.4

See merge request org/jdrupes/vm-operator!5
2024-10-06 10:05:10 +00:00
54445ef531 Prepare release v3.4 2024-10-06 10:05:09 +00:00
31a3f79e2a Merge branch 'feature/no-sts' into 'main'
Deploy pod instead of stateful set

See merge request org/jdrupes/vm-operator!4
2024-10-04 15:01:58 +00:00
83908b7cfd Deploy pod instead of stateful set 2024-10-04 15:01:58 +00:00
525696ffe9 Merge branch 'feature/delConnFile' into 'main'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Feature/del conn file

See merge request org/jdrupes/vm-operator!3
2024-09-13 10:17:20 +00:00
5c736faf09 Add "delete-this-file=1" in connection file by default. 2024-09-13 10:17:20 +00:00
69a9629ea9 Minor edits. 2024-08-20 10:09:59 +02:00
9856284e39 Merge branch 'bug/ioexception' into 'main'
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Use Java-21 virtual threads

See merge request org/jdrupes/vm-operator!2
2024-08-10 10:41:50 +00:00
bef47b308d Use Java-21 virtual threads 2024-08-10 10:41:50 +00:00
82eb6671a3 Merge branch 'feature/multi-monitor' into 'main'
Feature/multi monitor

See merge request org/jdrupes/vm-operator!1
2024-08-05 20:21:36 +00:00
4384c18910 Feature/multi monitor 2024-08-05 20:21:36 +00:00
51c1d8d7a9 Minor edit. 2024-07-17 22:59:28 +02:00
605ede7cf3 Merge branch 'main' of github.com:mnlipp/VM-Operator 2024-07-17 22:46:14 +02:00
4c04bb0e0a Minor edit. 2024-07-17 22:26:29 +02:00
Michael N. Lipp
45e271e6d0
Update webgui.md 2024-07-17 07:55:16 +02:00
Michael N. Lipp
959a35ca9e
Update manager.md 2024-07-17 07:52:03 +02:00
Michael N. Lipp
a62b8c2899
Update index.md 2024-07-15 08:46:23 +02:00
1b2d7ec330 Fix link. 2024-07-11 13:21:46 +02:00
e7cdaea205 Minor edit. 2024-07-11 12:54:12 +02:00
33856fffc2 Fix link. 2024-07-11 12:52:28 +02:00
e3fc4747e4 Minor edits. 2024-07-09 17:31:58 +02:00
fcf0c1d1af Fix syntax. 2024-07-09 17:19:15 +02:00
8f8a38771e Use longer titles. 2024-07-09 17:09:52 +02:00
f45c5698d1 Update. 2024-07-09 11:21:14 +02:00
299bded9de Add backlink. 2024-07-06 10:35:26 +02:00
9a5e1800ff Remove duplicate h1. 2024-07-04 09:38:02 +02:00
8802666944 Fix link. 2024-07-02 13:39:20 +02:00
63e77c0a8a Fix HTML error. 2024-06-30 15:09:20 +02:00
e3da87d94f Version tag may only be applied when all projects are "clean". 2024-06-27 16:50:49 +02:00
b74de67c6d Update icon. 2024-06-27 14:56:06 +02:00
6852c575ae Fix hover. 2024-06-26 21:43:28 +02:00
1fe7960d24 Update. 2024-06-26 14:14:45 +02:00
fc29786afe Fix links.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-06-24 11:18:35 +02:00
8a434d8410 Use newest branch if we have multiple matches. 2024-06-24 10:04:09 +02:00
e994fa1543 Another attempt to find the branch. 2024-06-23 21:41:00 +02:00
8b83a0cbc8 Make sure to switch to branch. 2024-06-23 17:02:44 +02:00
7df0cc386c Fix image building. 2024-06-23 12:52:18 +02:00
12ca211fdb Fix env reference. 2024-06-23 11:25:19 +02:00
e830815400 Add dependency. 2024-06-22 23:03:28 +02:00
28691b6443 Make dependencies branch specific. 2024-06-22 16:06:31 +02:00
86837bc81b Fix token.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-06-22 15:11:01 +02:00
a50809211d Fix name.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-06-22 14:46:54 +02:00
63c03dae81 Publish javadoc with JDK-21. 2024-06-22 14:40:14 +02:00
7dea95660c Update. 2024-06-22 14:32:11 +02:00
128ffbc2ad Update. 2024-06-22 11:52:32 +02:00
9d5f3cf702 Update link. 2024-06-20 23:11:01 +02:00
6a2d711643 New publishing style. 2024-06-20 22:42:26 +02:00
f0ccb83b39 Move pages. 2024-06-20 20:41:33 +02:00
98ce74f42b Adjust link. 2024-06-18 21:32:01 +02:00
92f4b3aaf8 Avoid unnecessary pages runs. 2024-06-18 18:03:14 +02:00
Michael N. Lipp
7f80f4c6e9
Wip/gitlab ci (#32)
Start CI/CD on a gitlab instance.
2024-06-18 13:51:59 +02:00
Michael N. Lipp
10182efea1
Feature/use java21 (#31)
Switch to using Java-21.
2024-06-13 22:15:33 +02:00
9c31f574b8 Fix accessing console without password.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-06-12 20:53:18 +02:00
d5c9a0c302 Upgrade base library.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-06-10 23:10:11 +02:00
6213aa5970 Use better version descriptors. 2024-06-10 23:09:48 +02:00
bbd9d3baff Upgrade OIDC login library.
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
2024-06-10 15:15:00 +02:00
Michael N. Lipp
65a5cfd286
Develop/v3 (#27)
Some checks failed
Java CI with Gradle / build (push) Has been cancelled
Prepare release.
2024-06-09 22:54:42 +02:00
305 changed files with 23947 additions and 6660 deletions

View file

@ -22,10 +22,10 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Install graphviz - name: Install graphviz
run: sudo apt-get install graphviz run: sudo apt-get install graphviz
- name: Set up JDK 17 - name: Set up JDK 21
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} stage run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} stage

89
.github/workflows/jekyll.yml vendored Normal file
View file

@ -0,0 +1,89 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
name: Deploy Jekyll site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["main"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between
# the run in-progress and latest queued. However, do NOT cancel
# in-progress runs as we want to allow these production deployments
# to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build job
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3' # Not needed with a .ruby-version file
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
cache-version: 0 # Increment this number if you need to re-download cached gems
working-directory: webpages
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
- name: Build with Jekyll
# Outputs to the './_site' directory by default
run: cd webpages && bundle exec jekyll build
env:
JEKYLL_ENV: production
- name: Install graphviz
run: sudo apt-get install graphviz
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
java-version: '21'
distribution: 'temurin'
- name: Build apidocs
run: ./gradlew apidocs
- name: Copy javadoc
run: cp -a build/javadoc webpages/_site/
- name: Generate the sitemap
uses: cicirello/generate-sitemap@v1
with:
path-to-root: webpages/_site
base-url-path: https://vm-operator.jdrupes.org
- name: Index pagefind
run: cd webpages && npx pagefind --source "_site"
- name: Upload artifact
# Automatically uploads an artifact from the './_site' directory by default
uses: actions/upload-pages-artifact@v3
with:
path: './webpages/_site'
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View file

@ -18,10 +18,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
ref: main
- name: Install graphviz - name: Install graphviz
run: sudo apt-get install graphviz run: sudo apt-get install graphviz
- name: Install podman - name: Install podman
@ -32,10 +31,10 @@ jobs:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK 17 - name: Set up JDK 21
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
java-version: '17' java-version: '21'
distribution: 'temurin' distribution: 'temurin'
- name: Push with Gradle - name: Push with Gradle
run: ./gradlew -Prepo.access.token=${{ secrets.REPO_ACCESS_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage pushImages run: ./gradlew -Pwebsite.push.token=${{ secrets.WEBSITE_PUSH_TOKEN }} -Pdocker.registry=ghcr.io/${{ github.actor }} stage publishImage

View file

@ -1,28 +0,0 @@
default:
# Template project: https://gitlab.com/pages/jekyll
# Docs: https://docs.gitlab.com/ee/pages/
image: ruby:3.2
before_script:
- git fetch origin gh-pages
- git checkout gh-pages
- gem install bundler
- bundle install
variables:
JEKYLL_ENV: production
LC_ALL: C.UTF-8
test:
stage: test
script:
- bundle exec jekyll build -d test
artifacts:
paths:
- test
pages:
stage: deploy
script:
- bundle exec jekyll build -d public
artifacts:
paths:
- public
environment: production

30
.markdownlint.yaml Normal file
View file

@ -0,0 +1,30 @@
# See [rules](https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml)
# Default state for all rules
default: true
# MD007/ul-indent : Unordered list indentation :
# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md007.md
MD007:
# Spaces for indent
indent: 2
# Whether to indent the first level of the list
start_indented: true
# Spaces for first level indent (when start_indented is set)
start_indent: 2
# MD025/single-title/single-h1 : Multiple top-level headings in the same document :
# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md025.md
MD025:
# Heading level
level: 1
# RegExp for matching title in front matter (disable)
front_matter_title: ""
# MD036/no-emphasis-as-heading : Emphasis used instead of a heading :
# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md036.md
MD036: false
# MD043/required-headings : Required heading structure :
# https://github.com/DavidAnson/markdownlint/blob/v0.37.4/doc/md043.md
MD043: false

38
.woodpecker/build.yaml Normal file
View file

@ -0,0 +1,38 @@
when:
- event: push
evaluate: 'CI_SYSTEM_HOST == "woodpecker.mnl.de"'
clone:
- name: git
image: woodpeckerci/plugin-git
settings:
partial: false
tags: true
depth: 0
steps:
- name: prepare
image: alpine
commands:
# Because we run the next step as user 1000 to make podman work:
- mkdir /woodpecker/workflow
- chown 1000:1000 /woodpecker/workflow
- chown -R 1000:1000 $CI_WORKSPACE
- name: build-jars
image: registry.mnl.de/mnl/jdk21-builder:v4
environment:
HOME: /woodpecker/workflow
REGISTRY: registry.mnl.de
REGISTRY_USER: mnl
REGISTRY_TOKEN:
from_secret: REGISTRY_TOKEN
commands:
- echo $REGISTRY_TOKEN | podman login -u $REGISTRY_USER --password-stdin $REGISTRY
- ./gradlew -Pdocker.registry=$REGISTRY/$REGISTRY_USER build apidocs publishImage
backend_options:
kubernetes:
securityContext:
privileged: true
runAsUser: 1000
runAsGroup: 1000

View file

@ -3,11 +3,23 @@
![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest) ![Latest Manager](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=manager*&label=latest)
![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest) ![Latest Runner](https://img.shields.io/github/v/tag/mnlipp/vm-operator?filter=runner-qemu*&label=latest)
# Run Qemu in Kubernetes Pods # Run QEMU/KVM in Kubernetes Pods
The goal of this project is to provide the means for running Qemu ![Overview picture](webpages/index-pic.svg)
based VMs in Kubernetes pods.
See the [project's home page](https://mnlipp.github.io/VM-Operator/) This project provides an easy to use and flexible solution for running
QEMU/KVM based VMs in Kubernetes pods.
The central component of this solution is the kubernetes operator that
manages "runners". These run in pods and are used to start and manage
the QEMU/KVM process for the VMs (optionally together with a SW-TPM).
A web GUI for administrators provides an overview of the VMs together
with some basic control over the VMs. A web GUI for users provides an
interface to access and optionally start, stop and reset the VMs.
Advanced features of the operator include pooling of VMs and automatic
login.
See the [project's home page](https://vm-operator.jdrupes.org/)
for details. for details.

View file

@ -5,9 +5,10 @@ buildscript {
} }
plugins { plugins {
id 'org.ajoberstar.grgit' version '5.2.0' apply false id 'org.ajoberstar.grgit' version '5.2.0'
id 'org.ajoberstar.git-publish' version '4.2.0' apply false id 'org.ajoberstar.git-publish' version '4.2.0' apply false
id 'pl.allegro.tech.build.axion-release' version '1.15.0' apply false id 'pl.allegro.tech.build.axion-release' version '1.17.2' apply false
id 'org.jdrupes.vmoperator.versioning-conventions'
id 'org.jdrupes.vmoperator.java-doc-conventions' id 'org.jdrupes.vmoperator.java-doc-conventions'
id 'eclipse' id 'eclipse'
id "com.github.node-gradle.node" version "7.0.1" id "com.github.node-gradle.node" version "7.0.1"
@ -18,7 +19,7 @@ allprojects {
} }
task stage { task stage {
description = 'To be executed by CI, build and update JavaDoc.' description = 'To be executed by CI.'
group = 'build' group = 'build'
// Build everything first // Build everything first
@ -26,11 +27,6 @@ task stage {
dependsOn subprojects.tasks.collect { dependsOn subprojects.tasks.collect {
tc -> tc.findByName("build") }.flatten() tc -> tc.findByName("build") }.flatten()
} }
if (JavaVersion.current() == JavaVersion.VERSION_17) {
// Publish JavaDoc
dependsOn gitPublishPush
}
} }
eclipse { eclipse {

View file

@ -1,9 +1,7 @@
#
#Wed Oct 02 14:48:43 CEST 2024
eclipse.preferences.version=1 eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull
org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable
org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=21 org.eclipse.jdt.core.compiler.compliance=21
@ -11,12 +9,5 @@ org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
org.eclipse.jdt.core.compiler.problem.nullReference=warning
org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=21 org.eclipse.jdt.core.compiler.source=21

View file

@ -1,3 +1,3 @@
eclipse.preferences.version=1 eclipse.preferences.version=1
groovy.compiler.level=40 groovy.compiler.level=-1
groovy.script.filters=**/*.dsld,y,**/*.gradle,n groovy.script.filters=**/*.dsld,y,**/*.gradle,n

View file

@ -1,9 +1,3 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This project uses @Incubating APIs which are subject to change.
*/
plugins { plugins {
// Support convention plugins written in Groovy. Convention plugins // Support convention plugins written in Groovy. Convention plugins
// are build scripts in 'src/main' that automatically become available // are build scripts in 'src/main' that automatically become available
@ -14,52 +8,24 @@ plugins {
id 'eclipse' id 'eclipse'
} }
repositories {
// Use the plugin portal to apply community plugins in convention plugins.
gradlePluginPortal()
}
sourceSets { sourceSets {
main { main {
groovy { groovy {
srcDirs = ['src'] srcDirs = ['src']
} }
} resources {
srcDirs = ['resources']
test {
groovy {
srcDirs = ['test']
} }
} }
} }
eclipse { eclipse {
project {
file {
// closure executed after .project content is loaded from existing file
// and before gradle build information is merged
beforeMerged { project ->
project.natures.clear()
project.buildCommands.clear()
}
project.natures += 'org.eclipse.buildship.core.gradleprojectnature'
// Don't build, result not used by Eclipse anyway
// project.buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
}
}
classpath {
downloadJavadoc = true
downloadSources = true
}
jdt { jdt {
file { file {
withProperties { properties -> withProperties { properties ->
def formatterPrefs = new Properties() def formatterPrefs = new Properties()
rootProject.file("gradle/org.eclipse.jdt.core.formatter.prefs") rootProject.file("../gradle/org.eclipse.jdt.core.formatter.prefs")
.withInputStream { formatterPrefs.load(it) } .withInputStream { formatterPrefs.load(it) }
properties.putAll(formatterPrefs) properties.putAll(formatterPrefs)
} }

View file

@ -1,7 +0,0 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This settings file is used to specify which projects to include in your build-logic build.
*/
rootProject.name = 'buildSrc'

View file

@ -5,6 +5,11 @@
*/ */
plugins { plugins {
// Apply the common versioning conventions.
// Put this at the start, because accessing project.version before
// this is applied makes things fail.
id 'org.jdrupes.vmoperator.versioning-conventions'
// Apply the java Plugin to add support for Java. // Apply the java Plugin to add support for Java.
id 'java' id 'java'
@ -13,9 +18,6 @@ plugins {
// Access to git information // Access to git information
id 'org.ajoberstar.grgit' id 'org.ajoberstar.grgit'
// Apply the common versioning conventions.
id 'org.jdrupes.vmoperator.versioning-conventions'
} }
repositories { repositories {
@ -53,21 +55,25 @@ sourceSets {
java { java {
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(17) languageVersion = JavaLanguageVersion.of(21)
} }
} }
jar { jar {
manifest { manifest {
inputs.property("gitDescriptor", { grgit.describe(always: true) }) def matchExpr = [ project.tagName + "*" ]
inputs.property("gitDescriptor",
{ grgit.describe(always: true, match: matchExpr) })
// Set Git revision information in the manifests of built bundles // Set Git revision information in the manifests of built bundles
def gitDesc = grgit.describe(always: true, match: matchExpr)
attributes([ attributes([
"Implementation-Title": project.name, "Implementation-Title": project.name,
"Implementation-Version": "$project.version (built from ${grgit.describe(always: true)})", "Implementation-Version": "$project.version (built from ${gitDesc})",
"Implementation-Vendor": grgit.repository.jgit.repository.config.getString("user", null, "name") "Implementation-Vendor": grgit.repository.jgit.repository.config.getString("user", null, "name")
+ " (" + grgit.repository.jgit.repository.config.getString("user", null, "email") + ")", + " (" + grgit.repository.jgit.repository.config.getString("user", null, "email") + ")",
"Git-Descriptor": grgit.describe(always: true), "Git-Descriptor": gitDesc,
"Git-SHA": grgit.head().id, "Git-SHA": grgit.head().id,
]) ])
} }

View file

@ -22,31 +22,28 @@ configurations {
} }
dependencies { dependencies {
markdownDoclet "org.jdrupes.mdoclet:doclet:3.1.0" markdownDoclet "org.jdrupes.mdoclet:doclet:4.0.0"
javadocTaglets "org.jdrupes.taglets:plantuml-taglet:2.1.0" javadocTaglets "org.jdrupes.taglets:plantuml-taglet:3.0.0"
}
task javadocResources(type: Copy) {
into file(docDestinationDir)
from ("${rootProject.rootDir}/misc") {
include '*.woff2'
}
} }
task apidocs (type: JavaExec) { task apidocs (type: JavaExec) {
// Does not work on JitPack, no /usr/bin/dot // Does not work on JitPack, no /usr/bin/dot
enabled = JavaVersion.current() == JavaVersion.VERSION_17 enabled = JavaVersion.current() == JavaVersion.VERSION_21
dependsOn javadocResources
outputs.dir(docDestinationDir) outputs.dir(docDestinationDir)
inputs.file rootProject.file('overview.md') inputs.file rootProject.file('overview.md')
inputs.file "${rootProject.rootDir}/misc/stylesheet.css" inputs.file "${rootProject.rootDir}/misc/javadoc-overwrites.css"
jvmArgs = ['--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED', jvmArgs = ['--add-exports=jdk.compiler/com.sun.tools.doclint=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED'] '--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
main = 'jdk.javadoc.internal.tool.Main' '--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit=ALL-UNNAMED',
'--add-opens=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit.resources.releases=ALL-UNNAMED',
'-Duser.language=en', '-Duser.region=US']
mainClass = 'jdk.javadoc.internal.tool.Main'
gradle.projectsEvaluated { gradle.projectsEvaluated {
// Make sure that other projects' compileClasspaths are resolved // Make sure that other projects' compileClasspaths are resolved
@ -69,8 +66,8 @@ task apidocs (type: JavaExec) {
'-package', '-package',
'-use', '-use',
'-linksource', '-linksource',
'-link', 'https://docs.oracle.com/en/java/javase/17/docs/api/', '-link', 'https://docs.oracle.com/en/java/javase/21/docs/api/',
'-link', 'https://mnlipp.github.io/jgrapes/latest-release/javadoc/', '-link', 'https://jgrapes.org/latest-release/javadoc/',
'-link', 'https://freemarker.apache.org/docs/api/', '-link', 'https://freemarker.apache.org/docs/api/',
'--add-exports', 'jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED', '--add-exports', 'jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', '--add-exports', 'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
@ -88,7 +85,7 @@ task apidocs (type: JavaExec) {
'-bottom', rootProject.file("misc/javadoc.bottom.txt").text, '-bottom', rootProject.file("misc/javadoc.bottom.txt").text,
'--allow-script-in-comments', '--allow-script-in-comments',
'-Xdoclint:-html', '-Xdoclint:-html',
'--main-stylesheet', "${rootProject.rootDir}/misc/stylesheet.css", '--add-stylesheet', "${rootProject.rootDir}/misc/javadoc-overwrites.css",
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.formats.html=ALL-UNNAMED', '--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.formats.html=ALL-UNNAMED',
'-quiet' '-quiet'
] ]
@ -97,34 +94,27 @@ task apidocs (type: JavaExec) {
ignoreExitValue true ignoreExitValue true
} }
task testJavadoc(type: Javadoc) {
enabled = JavaVersion.current() == JavaVersion.VERSION_21
source = fileTree(dir: 'testfiles', include: '**/*.java')
destinationDir = project.file("build/testfiles-gradle")
options.docletpath = configurations.markdownDoclet.files.asType(List)
options.doclet = 'org.jdrupes.mdoclet.MDoclet'
options.overview = 'testfiles/overview.md'
options.addStringOption('Xdoclint:-html', '-quiet')
options.setJFlags([
'--add-exports=jdk.compiler/com.sun.tools.doclint=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
'--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.tool=ALL-UNNAMED',
'--add-exports=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit=ALL-UNNAMED',
'--add-opens=jdk.javadoc/jdk.javadoc.internal.doclets.toolkit.resources.releases=ALL-UNNAMED'])
}
// Prepare github authentication for plugins // Prepare github authentication for plugins
if (System.properties['org.ajoberstar.grgit.auth.username'] == null) { if (System.properties['org.ajoberstar.grgit.auth.username'] == null) {
System.setProperty('org.ajoberstar.grgit.auth.username', System.setProperty('org.ajoberstar.grgit.auth.username',
project.rootProject.properties['repo.access.token'] ?: "nouser") project.rootProject.properties['website.push.token'] ?: "nouser")
}
gitPublish {
repoUri = 'https://github.com/mnlipp/VM-Operator.git'
branch = 'gh-pages'
contents {
from("${rootProject.buildDir}/javadoc") {
into 'javadoc'
}
if (!findProject(':org.jdrupes.vmoperator.runner.qemu').isSnapshot
&& !findProject(':org.jdrupes.vmoperator.manager').isSnapshot) {
from("${rootProject.buildDir}/javadoc") {
into 'latest-release/javadoc'
}
}
}
preserve { include '**/*' }
commitMessage = "Updated."
}
gradle.projectsEvaluated {
tasks.gitPublishReset.mustRunAfter subprojects.tasks
.collect { tc -> tc.findByName("build") }.flatten()
tasks.gitPublishReset.mustRunAfter subprojects.tasks
.collect { tc -> tc.findByName("test") }.flatten()
tasks.gitPublishCopy.dependsOn apidocs
} }

View file

@ -11,21 +11,26 @@ plugins {
id 'pl.allegro.tech.build.axion-release' id 'pl.allegro.tech.build.axion-release'
} }
def shortened = project.name.startsWith(project.group + ".") ?
project.name.substring(project.group.length() + 1) : project.name
if (shortened == "manager") {
shortened = "manager-app";
}
var tagName = shortened.replace('.', '-') + "-"
if (grgit.branch.current.name != "main"
&& grgit.branch.current.name != "HEAD"
&& !grgit.branch.current.name.startsWith("testing")
&& !grgit.branch.current.name.startsWith("release")
&& !grgit.branch.current.name.startsWith("develop")) {
tagName = tagName + grgit.branch.current.name.replace('/', '-') + "-"
}
project.ext.tagName = tagName
scmVersion { scmVersion {
versionIncrementer 'incrementMinor' versionIncrementer 'incrementMinor'
tag { tag {
def shortened = project.name.startsWith(project.group + ".") ? prefix = project.tagName
project.name.substring(project.group.length() + 1) : project.name
if (shortened == "manager") {
shortened = "manager-app";
}
var p = shortened.replace('.', '-') + "-"
if (grgit.branch.current.name != "main"
&& !grgit.branch.current.name.startsWith("release")) {
p = p + grgit.branch.current.name.replace('/', '-') + "-"
}
prefix = p
} }
} }
version = scmVersion.version project.version = scmVersion.version
ext.isSnapshot = version.endsWith('-SNAPSHOT') ext.isSnapshot = version.endsWith('-SNAPSHOT')

View file

@ -0,0 +1,74 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: vmpools.vmoperator.jdrupes.org
spec:
group: vmoperator.jdrupes.org
# list of versions supported by this CustomResourceDefinition
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
retention:
description: >-
Defines the timeout for assignments. The time may be
specified as ISO 8601 time or duration. When specifying
a duration, it will be added to the last time the VM's
console was used to obtain the timeout.
type: string
pattern: '^(?:\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3])(?:|:?[0-5]\d))|P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+[Hh])?(?:\d+[Mm])?(?:\d+(?:\.\d{1,9})?[Ss])?)?)$'
default: "PT1h"
loginOnAssignment:
description: >-
If set to true, the user will be automatically logged in
to the VM's console when the VM is assigned to him.
type: boolean
default: false
permissions:
type: array
description: >-
Defines permissions for accessing and manipulating the Pool.
items:
type: object
description: >-
Permissions can be granted to a user or to a role.
oneOf:
- required:
- user
- required:
- role
properties:
user:
type: string
role:
type: string
may:
type: array
items:
type: string
enum:
- start
- stop
- reset
- accessConsole
- "*"
default: ["accessConsole"]
required:
- permissions
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: vmpools
# singular name to be used as an alias on the CLI and for display
singular: vmpool
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: VmPool
listKind: VmPoolList

View file

@ -994,6 +994,10 @@ spec:
type: array type: array
description: >- description: >-
Defines permissions for accessing and manipulating the VM. Defines permissions for accessing and manipulating the VM.
The meaning of most permissions should be obvious. The
difference between "accessConsole" and "takeConsole" is
that "takeConsole" allows the user to take control of
the console even if it is already in use by another user.
items: items:
type: object type: object
description: >- description: >-
@ -1012,8 +1016,26 @@ spec:
type: array type: array
items: items:
type: string type: string
enum: ["start", "stop", "accessConsole", "*"] enum:
- start
- stop
- reset
- accessConsole
- takeConsole
- "*"
default: [] default: []
pools:
type: array
description: >-
List of pools this VM belongs to.
items:
type: string
default: []
loggingProperties:
type: string
description: >-
Override the default logging properties for
the runner for this VM.
vm: vm:
type: object type: object
description: Defines the VM. description: Defines the VM.
@ -1405,6 +1427,15 @@ spec:
display: display:
type: object type: object
properties: properties:
outputs:
type: integer
default: 1
loggedInUser:
description: >-
The name of a user that should be automatically
logged in on the display. Note that this requires
support from an agent in the guest OS.
type: string
spice: spice:
type: object type: object
properties: properties:
@ -1439,6 +1470,10 @@ spec:
type: object type: object
default: {} default: {}
properties: properties:
runnerVersion:
description: >-
The version string of the runner.
type: string
cpus: cpus:
description: >- description: >-
Number of CPUs currently in use. Number of CPUs currently in use.
@ -1449,12 +1484,50 @@ spec:
Amount of memory in use. Amount of memory in use.
type: string type: string
default: "0" default: "0"
consoleClient:
description: >-
The hostname of the currently connected client.
type: string
default: ""
consoleUser:
description: >-
The id of the user who has last requested a console
connection.
type: string
default: ""
loggedInUser:
description: >-
The name of a user that is currently logged in by the
VM operator agent.
type: string
displayPasswordSerial: displayPasswordSerial:
description: >- description: >-
Counts changes of the display password. Set to -1 Counts changes of the display password. Set to -1
by the runner if password protection is not enabled. by the runner if password protection is not enabled.
type: integer type: integer
default: 0 default: 0
osinfo:
description: Copy of the OS info provided by the guest agent.
type: object
x-kubernetes-preserve-unknown-fields: true
assignment:
description: >-
The assignment of this VM to a a particular user.
type: object
properties:
pool:
description: >-
The pool this VM is taken from.
type: string
user:
description: >-
The user this VM is assigned to.
type: string
lastUsed:
description: >-
The last time this VM was used by the user.
type: string
default: {}
conditions: conditions:
description: >- description: >-
List of component conditions observed List of component conditions observed
@ -1465,6 +1538,30 @@ spec:
lastTransitionTime: "1970-01-01T00:00:00Z" lastTransitionTime: "1970-01-01T00:00:00Z"
reason: Creation reason: Creation
message: "Creation of CR" message: "Creation of CR"
- type: Booted
status: "False"
observedGeneration: 1
lastTransitionTime: "1970-01-01T00:00:00Z"
reason: Creation
message: "Creation of CR"
- type: VmopAgentConnected
status: "False"
observedGeneration: 1
lastTransitionTime: "1970-01-01T00:00:00Z"
reason: Creation
message: "Creation of CR"
- type: UserLoggedIn
status: "False"
observedGeneration: 1
lastTransitionTime: "1970-01-01T00:00:00Z"
reason: Creation
message: "Creation of CR"
- type: ConsoleConnected
status: "False"
observedGeneration: 1
lastTransitionTime: "1970-01-01T00:00:00Z"
reason: Creation
message: "Creation of CR"
type: array type: array
items: items:
type: object type: object

View file

@ -21,22 +21,31 @@ spec:
- name: vm-operator - name: vm-operator
image: >- image: >-
ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest ghcr.io/mnlipp/org.jdrupes.vmoperator.manager:latest
imagePullPolicy: Always
env:
- name: JAVA_OPTS
# The VM operator needs about 25 MB of memory, plus 1 MB for
# each VM. The reason is that for the sake of effeciency, we
# have to keep a parsed representation of the CRD in memory,
# which requires about 512 KB per VM. While handling updates,
# we temporarily have the old and the new version of the CRD
# in memory, so we need another 512 KB per VM.
value: "-Xmx128m"
resources:
requests:
cpu: 100m
memory: 128Mi
volumeMounts: volumeMounts:
- name: config - name: config
mountPath: /etc/opt/vmoperator mountPath: /etc/opt/vmoperator
- name: vmop-image-repository - name: vmop-image-repository
mountPath: /var/local/vmop-image-repository mountPath: /var/local/vmop-image-repository
imagePullPolicy: Always
securityContext: securityContext:
capabilities: capabilities:
drop: drop:
- ALL - ALL
readOnlyRootFilesystem: true readOnlyRootFilesystem: true
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
resources:
requests:
cpu: 100m
memory: 128Mi
volumes: volumes:
- name: config - name: config
configMap: configMap:

View file

@ -9,8 +9,15 @@ rules:
- vmoperator.jdrupes.org - vmoperator.jdrupes.org
resources: resources:
- vms - vms
- vmpools
verbs: verbs:
- '*' - '*'
- apiGroups:
- vmoperator.jdrupes.org
resources:
- vms/status
verbs:
- patch
- apiGroups: - apiGroups:
- apps - apps
resources: resources:
@ -28,9 +35,12 @@ rules:
- apiGroups: - apiGroups:
- "" - ""
resources: resources:
- persistentvolumeclaims
- pods - pods
verbs: verbs:
- watch
- list - list
- get - get
- create
- delete - delete
- patch - patch

View file

@ -1 +1,4 @@
/test-vm-ci.yaml /test-vm-ci.yaml
/kubeconfig.yaml
/crds/
/.vm-operator-cmd.rc

View file

@ -3,9 +3,9 @@
The CRD must be deployed independently. Apart from that, the The CRD must be deployed independently. Apart from that, the
`kustomize.yaml` `kustomize.yaml`
* creates a small cdrom image repository and * creates a small cdrom image repository and
* deploys the operator in namespace `vmop-dev` with a replica of 0. * deploys the operator in namespace `vmop-dev` with a replica of 0.
This allows you to run the manager in your IDE. This allows you to run the manager in your IDE.

View file

@ -7,8 +7,28 @@
"/Controller": "/Controller":
namespace: vmop-dev namespace: vmop-dev
"/Reconciler": "/Reconciler":
runnerData: runnerDataPvc:
storageClassName: null storageClassName: rook-cephfs
loadBalancerService:
labels:
label1: label1
label2: toBeReplaced
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.168.1
metallb.universe.tf/ip-allocated-from-pool: single-common
metallb.universe.tf/allow-shared-ip: single-common
loggingProperties: |
# Defaults for namespace (VM domain)
handlers=java.util.logging.ConsoleHandler
#org.jgrapes.level=FINE
#org.jgrapes.core.handlerTracking.level=FINER
org.jdrupes.vmoperator.runner.qemu.level=FINEST
java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format=%1$tb %1$td %1$tT %4$s %5$s%6$s%n
"/GuiSocketServer": "/GuiSocketServer":
port: 8888 port: 8888
"/GuiHttpServer": "/GuiHttpServer":
@ -20,15 +40,30 @@
- name: admin - name: admin
fullName: Administrator fullName: Administrator
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
- name: test - name: operator
fullName: Test Account fullName: Operator
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test1
fullName: Test Account 1
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test2
fullName: Test Account 2
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test3
fullName: Test Account 3
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
"/RoleConfigurator": "/RoleConfigurator":
rolesByUser: rolesByUser:
# User admin has role admin # User admin has role admin
admin: admin:
- admin - admin
test: operator:
- operator
test1:
- user
test2:
- user
test3:
- user - user
# All users have role other # All users have role other
"*": "*":
@ -39,13 +74,16 @@
# Admins can use all conlets # Admins can use all conlets
admin: admin:
- "*" - "*"
operator:
- org.jdrupes.vmoperator.vmmgmt.VmMgmt
- org.jdrupes.vmoperator.vmaccess.VmAccess
user: user:
- org.jdrupes.vmoperator.vmviewer.VmViewer - org.jdrupes.vmoperator.vmaccess.VmAccess
# Others cannot use any conlet (except login conlet to log out) # Others cannot use any conlet (except login conlet to log out)
other: other:
- org.jgrapes.webconlet.oidclogin.LoginConlet - org.jgrapes.webconlet.oidclogin.LoginConlet
"/ComponentCollector": "/ComponentCollector":
"/VmViewer": "/VmAccess":
displayResource: displayResource:
preferredIpVersion: ipv4 preferredIpVersion: ipv4
syncPreviewsFor: syncPreviewsFor:

47
dev-example/gen-pool-vm-crds Executable file
View file

@ -0,0 +1,47 @@
#!/bin/bash
function usage() {
cat >&2 <<EOF
Usage: $0 [OPTION]... [TEMPLATE]
Generate VM CRDs using TEMPLATE.
-c, --count Count of VMs to generate
-d, --destination DIR Generate into given directory (default: ".")
-h, --help Print this help
-p, --prefix PREFIX Prefix for generated file (default: basename of template)
EOF
exit 1
}
count=0
destination=.
template=""
prefix=""
while [ "$#" -gt 0 ]; do
case "$1" in
-c|--count) shift; count=$1;;
-d|--destination) shift; destination="$1";;
-h|--help) shift; usage;;
-p|--prefix) shift; prefix="$1";;
-*) echo >&2 "Unknown option: $1"; exit 1;;
*) template="$1";;
esac
shift
done
if [ -z "$template" ]; then
usage
fi
if [ "$count" = "0" ]; then
exit 0
fi
for number in $(seq 1 $count); do
if [ -z "$prefix" ]; then
prefix=$(basename $template .tpl.yaml)
fi
name="$prefix$(printf %03d $number)"
index=$(($number - 1))
esh -o $destination/$name.yaml $template number=$number index=$index
done

View file

@ -35,6 +35,14 @@ patches:
"/Reconciler": "/Reconciler":
runnerData: runnerData:
storageClassName: null storageClassName: null
loadBalancerService:
labels:
label1: label1
label2: toBeReplaced
annotations:
metallb.universe.tf/loadBalancerIPs: 192.168.168.1
metallb.universe.tf/ip-allocated-from-pool: single-common
metallb.universe.tf/allow-shared-ip: single-common
"/GuiSocketServer": "/GuiSocketServer":
port: 8888 port: 8888
"/GuiHttpServer": "/GuiHttpServer":
@ -43,10 +51,16 @@ patches:
"/WebConsole": "/WebConsole":
"/LoginConlet": "/LoginConlet":
users: users:
admin: - name: admin
fullName: Administrator fullName: Administrator
password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21." password: "$2b$05$NiBd74ZGdplLC63ePZf1f.UtjMKkbQ23cQoO2OKOFalDBHWAOy21."
test: - name: test1
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test2
fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
- name: test3
fullName: Test Account fullName: Test Account
password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2" password: "$2b$05$hZaI/jToXf/d3BctZdT38Or7H7h6Pn2W3WiB49p5AyhDHFkkYCvo2"
"/RoleConfigurator": "/RoleConfigurator":
@ -54,7 +68,11 @@ patches:
# User admin has role admin # User admin has role admin
admin: admin:
- admin - admin
test: test1:
- user
test2:
- user
test3:
- user - user
# All users have role other # All users have role other
"*": "*":
@ -71,7 +89,7 @@ patches:
other: other:
- org.jgrapes.webconlet.locallogin.LoginConlet - org.jgrapes.webconlet.locallogin.LoginConlet
"/ComponentCollector": "/ComponentCollector":
"/VmViewer": "/VmAccess":
displayResource: displayResource:
preferredIpVersion: ipv4 preferredIpVersion: ipv4
syncPreviewsFor: syncPreviewsFor:

66
dev-example/pool-action Executable file
View file

@ -0,0 +1,66 @@
#!/bin/bash
function usage() {
cat >&2 <<EOF
Usage: $0 pool-name action
Applys action to all VMs in the pool.
--context Context to be passed to kubectl (required)
-n, --namespace Namespace to be passed to kubectl
Action is one of "start", "stop", "delete" or "delete-disks"
Defaults for context and namespace are read from .vm-operator-cmd.rc.
EOF
exit 1
}
unset pool
unset action
unset context
namespace=default
if [ -r .vm-operator-cmd.rc ]; then
. .vm-operator-cmd.rc
fi
while [ "$#" -gt 0 ]; do
case "$1" in
--context) shift; context="$1";;
--context=*) IFS='=' read -r option value <<< "$1"; context="$value";;
-n|--namespace) shift; namespace="$1";;
-*) echo >&2 "Unknown option: $1"; exit 1;;
*) if [ ! -v pool ]; then
pool="$1"
elif [ ! -v action ]; then
action="$1"
else
usage
fi;;
esac
shift
done
if [ ! -v pool -o ! -v "action" -o ! -v context ]; then
echo >&2 "Missing arguments or context not set."
echo >&2
usage
fi
case "$action" in
"start"|"stop"|"delete"|"delete-disks") ;;
*) usage;;
esac
kubectl --context="$context" -n "$namespace" get vms -o json \
| jq -r '.items[] | select(.spec.pools | contains(["'${pool}'"])) | .metadata.name' \
| while read vmName; do
case "$action" in
start) kubectl --context="$context" -n "$namespace" patch vms "$vmName" \
--type='merge' -p '{"spec":{"vm":{"state":"Running"}}}';;
stop) kubectl --context="$context" -n "$namespace" patch vms "$vmName" \
--type='merge' -p '{"spec":{"vm":{"state":"Stopped"}}}';;
delete) kubectl --context="$context" -n "$namespace" delete vm/"$vmName";;
delete-disks) kubectl --context="$context" -n "$namespace" delete \
pvc -l app.kubernetes.io/instance="$vmName" ;;
esac
done

View file

@ -0,0 +1,17 @@
apiVersion: "vmoperator.jdrupes.org/v1"
kind: VmPool
metadata:
namespace: vmop-dev
name: test-vms
spec:
retention: "PT1m"
loginOnAssignment: true
permissions:
- user: admin
may:
- accessConsole
- start
- role: user
may:
- accessConsole
- start

View file

@ -0,0 +1,10 @@
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
namespace: vmop-dev
name: test-vm-system-disk-snapshot
spec:
volumeSnapshotClassName: csi-rbdplugin-snapclass
source:
persistentVolumeClaimName: test-vm-system-disk

View file

@ -0,0 +1,66 @@
apiVersion: "vmoperator.jdrupes.org/v1"
kind: VirtualMachine
metadata:
namespace: vmop-dev
name: test-vm<%= $(printf "%02d" ${number}) %>
annotations:
argocd.argoproj.io/sync-wave: "20"
spec:
image:
source: ghcr.io/mnlipp/org.jdrupes.vmoperator.runner.qemu-arch:latest
# source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing
# source: docker-registry.lan.mnl.de/vmoperator/org.jdrupes.vmoperator.runner.qemu-arch:latest
pullPolicy: Always
runnerTemplate:
update: true
permissions:
- role: admin
may:
- "*"
guestShutdownStops: true
cloudInit:
metaData: {}
pools:
- test-vms
vm:
# state: Running
bootMenu: true
maximumCpus: 4
currentCpus: 2
maximumRam: 6Gi
currentRam: 4Gi
networks:
# No bridge on TC1
# - tap: {}
- user: {}
disks:
- volumeClaimTemplate:
metadata:
name: system
spec:
storageClassName: ceph-rbd3slow
dataSource:
name: test-vm-system-disk-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 40Gi
- cdrom:
image: ""
# image: https://download.fedoraproject.org/pub/fedora/linux/releases/38/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-38-1.6.iso
display:
spice:
port: <%= $((5910 + number)) %>

View file

@ -5,18 +5,13 @@ metadata:
name: test-vm name: test-vm
spec: spec:
image: image:
repository: docker-registry.lan.mnl.de source: registry.mnl.de/org/jdrupes/vm-operator/org.jdrupes.vmoperator.runner.qemu-arch:testing
path: vmoperator/org.jdrupes.vmoperator.runner.qemu-alpine
version: latest
pullPolicy: Always pullPolicy: Always
permissions: permissions:
- user: admin - user: admin
may: may:
- "*" - "*"
- user: test
may:
- "accessConsole"
resources: resources:
requests: requests:
@ -37,8 +32,9 @@ spec:
currentCpus: 4 currentCpus: 4
networks: networks:
- tap: # No bridge on test cluster
mac: "02:16:3e:33:58:10" - user: {}
disks: disks:
- volumeClaimTemplate: - volumeClaimTemplate:
metadata: metadata:
@ -62,3 +58,5 @@ spec:
spice: spice:
port: 5810 port: 5810
generateSecret: true generateSecret: true
loadBalancerService: {}

View file

@ -0,0 +1,2 @@
SUBSYSTEM=="virtio-ports", ATTR{name}=="org.jdrupes.vmop_agent.0", \
TAG+="systemd" ENV{SYSTEMD_WANTS}="vmop-agent.service"

View file

@ -0,0 +1,3 @@
#!/bin/sh
sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf

146
dev-example/vmop-agent/vmop-agent Executable file
View file

@ -0,0 +1,146 @@
#!/usr/bin/bash
# Note that this script requires "jq" to be installed and a version
# of loginctl that accepts the "-j" option.
while [ "$#" -gt 0 ]; do
case "$1" in
--path) shift; ttyPath="$1";;
--path=*) IFS='=' read -r option value <<< "$1"; ttyPath="$value";;
esac
shift
done
ttyPath="${ttyPath:-/dev/virtio-ports/org.jdrupes.vmop_agent.0}"
if [ ! -w "$ttyPath" ]; then
echo >&2 "Device $ttyPath not writable"
exit 1
fi
# Create fd for the tty in variable con
if ! exec {con}<>"$ttyPath"; then
echo >&2 "Cannot open device $ttyPath"
exit 1
fi
# Temporary file for logging error messages, clear tty and signal ready
temperr=$(mktemp)
clear >/dev/tty1
echo >&${con} "220 Hello"
# This script uses the (shared) home directory as "dictonary" for
# synchronizing the username and the uid between hosts.
#
# Every user has a directory with his username. The directory is
# owned by root to prevent changes of access rights by the user.
# The uid and gid of the directory are equal. Thus the name of the
# directory and the id from the group ownership also provide the
# association between the username and the uid.
# Add the user with name $1 to the host's "user database". This
# may not be invoked concurrently.
createUser() {
local missing=$1
local uid
local userHome="/home/$missing"
local createOpts=""
# Retrieve or create the uid for the username
if [ -d "$userHome" ]; then
# If a home directory exists, use the id from the group ownership as uid
uid=$(ls -ldn "$userHome" | head -n 1 | awk '{print $4}')
createOpts="--no-create-home"
else
# Else get the maximum of all ids from the group ownership +1
uid=$(ls -ln "/home" | tail -n +2 | awk '{print $4}' | sort | tail -1)
uid=$(( $uid + 1 ))
if [ $uid -lt 1100 ]; then
uid=1100
fi
createOpts="--create-home"
fi
groupadd -g $uid $missing
useradd $missing -u $uid -g $uid $createOpts
}
# Login the user, i.e. create a desktopn for the user.
doLogin() {
user=$1
if [ "$user" = "root" ]; then
echo >&${con} "504 Won't log in root"
return
fi
# Check if this user is already logged in on tty2
curUser=$(loginctl -j | jq -r '.[] | select(.tty=="tty2") | .user')
if [ "$curUser" = "$user" ]; then
echo >&${con} "201 User already logged in"
return
fi
# Terminate a running desktop (fail safe)
attemptLogout
# Check if username is known on this host. If not, create user
uid=$(id -u ${user} 2>/dev/null)
if [ $? != 0 ]; then
( flock 200
createUser ${user}
) 200>/home/.gen-uid-lock
# This should now work, else something went wrong
uid=$(id -u ${user} 2>/dev/null)
if [ $? != 0 ]; then
echo >&${con} "451 Cannot determine uid"
return
fi
fi
# Configure user as auto login user
sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf
sed -i '/\[daemon\]/a AutomaticLoginEnable=true\nAutomaticLogin='$user \
/etc/gdm/custom.conf
# Activate user
systemctl restart gdm
if [ $? -eq 0 ]; then
echo >&${con} "201 User logged in successfully"
else
echo >&${con} "451 $(tr '\n' ' ' <${temperr})"
fi
}
# Attempt to log out a user currently using tty1. This is an intermediate
# operation that can be invoked from other operations
attemptLogout() {
sed -i '/AutomaticLogin/d' /etc/gdm/custom.conf
systemctl stop gdm
echo >&${con} "102 Desktop stopped"
}
# Log out any user currently using tty1. This is invoked when executing
# the logout command and therefore sends back a 2xx return code.
# Also try to restart gdm, if it is not running.
doLogout() {
attemptLogout
systemctl restart gdm
echo >&${con} "202 User logged out"
}
while read line <&${con}; do
case $line in
"login "*) IFS=' ' read -ra args <<< "$line"; doLogin ${args[1]};;
"logout") doLogout;;
esac
done
onExit() {
doLogout
if [ -n "$temperr" ]; then
rm -f $temperr
fi
echo >&${con} "240 Quit"
}
trap onExit EXIT

View file

@ -0,0 +1,15 @@
[Unit]
Description=VM-Operator (Guest) Agent
BindsTo=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device
After=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device multi-user.target
IgnoreOnIsolate=True
[Service]
UMask=0077
#EnvironmentFile=/etc/sysconfig/vmop-agent
ExecStart=/usr/local/libexec/vmop-agent
Restart=always
RestartSec=0
[Install]
WantedBy=dev-virtio\x2dports-org.jdrupes.vmop_agent.0.device

1
gradle.properties Normal file
View file

@ -0,0 +1 @@
org.gradle.parallel=true

Binary file not shown.

View file

@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

31
gradlew vendored
View file

@ -55,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -83,10 +83,8 @@ done
# This is normally unused # This is normally unused
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@ -133,10 +131,13 @@ location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045 # shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
done done
fi fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
# shell script including quotes and variable substitutions, so put them in DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded. # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

20
gradlew.bat vendored
View file

@ -43,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail
@ -57,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail goto fail

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,2 @@
:root { --body-font-size: 16px;}
:root { --code-font-size: 16px;}

View file

@ -4,26 +4,33 @@
<a href="https://github.com/site/terms" target="_top">Terms</a> <a href="https://github.com/site/terms" target="_top">Terms</a>
&mdash; <a href="https://github.com/site/privacy" target="_top">Privacy</a></p> &mdash; <a href="https://github.com/site/privacy" target="_top">Privacy</a></p>
<script type="text/javascript"> <script type="text/javascript">
if (location.hostname.indexOf("github") !== -1) { if (location.hostname.indexOf("github") !== -1 || location.hostname.indexOf("jdrupes.org") !== -1) {
document.getElementById("githubfooter").style.visibility="visible"; document.getElementById("githubfooter").style.visibility="visible";
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.mnlipp.github.io"]);
_paq.push(["setDomains", ["*.mnlipp.github.io"]]);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//piwik.mnl.de/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '14']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
} }
</script> </script>
<noscript> <noscript>
<div>JavaScript is disabled on your browser, terms and privacy links may not be shown correctly.</div> <div>JavaScript is disabled on your browser, terms and privacy links may not be shown correctly.</div>
</noscript> </noscript>
<!-- Matomo anonymous, no cookies (https://matomo.org/blog/2018/04/how-to-not-process-any-personal-data-with-matomo-and-what-it-means-for-you/) -->
<script>
var _paq = _paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);
_paq.push(["setCookieDomain", "*.mnlipp.github.io"]);
_paq.push(["setDomains", ["*.mnlipp.github.io", "*.jdrupes.org", "kubernetes-vm-operator.readthedocs.io"]]);
_paq.push(['disableCookies']);
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://piwik.mnl.de/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '17']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
<noscript><p><img referrerpolicy="no-referrer-when-downgrade"
src="//piwik.mnl.de/matomo.php?idsite=17&amp;rec=1&amp;action_name=VM-Operator" style="border:0;" alt="" /></p></noscript>
<!-- End Matomo Code -->
<script defer src="https://gotit.mnl.de/script.js" data-website-id="14b277ad-d330-4a54-82f1-a77d111240ac"></script>
</div> </div>

View file

@ -1,912 +0,0 @@
/*
* Javadoc style sheet
*/
@font-face {
font-family: 'DejaVu Serif';
src: local('DejaVu Serif'), url('DejaVuSerif.woff2');
}
@font-face {
font-family: 'DejaVu Serif';
font-weight: bold;
src: local('DejaVu Serif Bold'), url('DejaVuSerif-Bold.woff2');
}
@font-face {
font-family: 'DejaVu Sans';
src: local('DejaVu Sans'), url('DejaVuSans.woff2');
}
@font-face {
font-family: 'DejaVu Sans';
font-weight: bold;
src: local('DejaVu Sans Bold'), url('DejaVuSans-Bold.woff2');
}
@font-face {
font-family: 'DejaVu Sans Mono';
src: local('DejaVu Sans Mono'), url('DejaVuSansMono.woff2');
}
@font-face {
font-family: 'DejaVu Sans Mono';
font-weight: bold;
src: local('DejaVu Sans Mono Bold'), url('DejaVuSansMono-Bold.woff2');
}
/*
* Styles for individual HTML elements.
*
* These are styles that are specific to individual HTML elements. Changing them affects the style of a particular
* HTML element throughout the page.
*/
body {
background-color:#ffffff;
color:#353833;
font: normal 16px/1.5 "DejaVu Serif", serif;
margin:0;
padding:0;
height:100%;
width:100%;
}
iframe {
margin:0;
padding:0;
height:100%;
width:100%;
overflow-y:scroll;
border:none;
}
a:link, a:visited {
text-decoration:none;
color:#4A6782;
}
a[href]:hover, a[href]:focus {
text-decoration:none;
color:#bb7a2a;
}
a[name] {
color:#353833;
}
pre {
font-family: "DejaVu Sans Mono", monospace;
}
h1 {
font-family: "DejaVu Sans", sans;
font-size:20px;
}
h2 {
font-family: "DejaVu Sans", sans;
font-size:18px;
}
h3 {
font-family: "DejaVu Sans", sans;
font-size:16px;
}
h4 {
font-family: "DejaVu Sans", sans;
font-size:15px;
}
h5 {
font-family: "DejaVu Sans", sans;
font-size:14px;
}
h6 {
font-family: "DejaVu Sans", sans;
font-size:13px;
}
ul {
list-style-type:disc;
}
code, tt {
font-family: "DejaVu Sans Mono", monospace;
}
:not(h1, h2, h3, h4, h5, h6) > code,
:not(h1, h2, h3, h4, h5, h6) > tt {
/* font-size:14px; */
padding-top:4px;
margin-top:8px;
line-height:1.4em;
}
dt code {
font-family: "DejaVu Sans Mono", monospace;
font-size:14px;
padding-top:4px;
}
.summary-table dt code {
font-family: "DejaVu Sans Mono", monospace;
font-size:14px;
vertical-align:top;
padding-top:4px;
}
sup {
font-size:8px;
}
/*
* Styles for HTML generated by javadoc.
*
* These are style classes that are used by the standard doclet to generate HTML documentation.
*/
/*
* Styles for document title and copyright.
*/
.clear {
clear:both;
height:0;
overflow:hidden;
}
.about-language {
float:right;
padding:0 21px 8px 8px;
font-size:11px;
margin-top:-9px;
height:2.9em;
}
.legal-copy {
margin-left:.5em;
}
.tab {
background-color:#0066FF;
color:#ffffff;
padding:8px;
width:5em;
font-weight:bold;
}
/*
* Styles for navigation bar.
*/
@media screen {
.flex-box {
position:fixed;
display:flex;
flex-direction:column;
height: 100%;
width: 100%;
}
.flex-header {
flex: 0 0 auto;
}
.flex-content {
flex: 1 1 auto;
overflow-y: auto;
}
}
.top-nav {
background-color:#4D7A97;
color:#FFFFFF;
float:left;
padding:0;
width:100%;
clear:right;
min-height:2.8em;
padding-top:10px;
overflow:hidden;
font-family: "DejaVu Sans", sans;
font-size:80%;
}
.sub-nav {
background-color:#dee3e9;
float:left;
width:100%;
overflow:hidden;
font-family: "DejaVu Sans", sans;
font-size:80%;
}
.sub-nav div {
clear:left;
float:left;
padding:0 0 5px 6px;
text-transform:uppercase;
}
.sub-nav .nav-list {
padding-top:5px;
}
ul.nav-list {
display:block;
margin:0 25px 0 0;
padding:0;
}
ul.sub-nav-list {
float:left;
margin:0 25px 0 0;
padding:0;
}
ul.nav-list li {
list-style:none;
float:left;
padding: 5px 6px;
text-transform:uppercase;
}
.sub-nav .nav-list-search {
float:right;
margin:0 0 0 0;
padding:5px 6px;
clear:none;
}
.nav-list-search label {
position:relative;
right:-16px;
}
ul.sub-nav-list li {
list-style:none;
float:left;
padding-top:10px;
}
.top-nav a:link, .top-nav a:active, .top-nav a:visited {
color:#FFFFFF;
text-decoration:none;
text-transform:uppercase;
}
.top-nav a:hover {
text-decoration:none;
color:#bb7a2a;
text-transform:uppercase;
}
.nav-bar-cell1-rev {
background-color:#F8981D;
color:#253441;
margin: auto 5px;
}
.skip-nav {
position:absolute;
top:auto;
left:-9999px;
overflow:hidden;
}
/*
* Hide navigation links and search box in print layout
*/
@media print {
ul.nav-list, div.sub-nav {
display:none;
}
}
/*
* Styles for page header and footer.
*/
.title {
color:#2c4557;
margin:10px 0;
}
.sub-title {
margin:5px 0 0 0;
}
.header ul {
margin:0 0 15px 0;
padding:0;
}
.header ul li, .footer ul li {
list-style:none;
font-size:80%;
}
/*
* Styles for headings.
*/
body.class-declaration-page .summary h2,
body.class-declaration-page .details h2,
body.class-use-page h2,
body.module-declaration-page .block-list h2 {
font-style: italic;
padding:0;
margin:15px 0;
}
body.class-declaration-page .summary h3,
body.class-declaration-page .details h3,
body.class-declaration-page .summary .inherited-list h2 {
background-color:#dee3e9;
border:1px solid #d0d9e0;
margin:0 0 6px -8px;
padding:7px 5px;
}
/*
* Styles for page layout containers.
*/
main {
clear:both;
padding:10px 20px;
position:relative;
}
dl.notes > dt {
font-family: "DejaVu Sans", sans;
font-weight:bold;
margin:10px 0 0 0;
color:#4E4E4E;
}
dl.notes > dd {
margin:5px 10px 10px 0;
}
dl.name-value > dt {
margin-left:1px;
/* font-size:1.1em; */
display:inline;
font-weight:bold;
}
dl.name-value > dd {
margin:0 0 0 1px;
/* font-size:1.1em; */
display:inline;
}
/*
* Styles for lists.
*/
li.circle {
list-style:circle;
}
ul.horizontal li {
display:inline;
/* font-size:0.9em; */
}
div.inheritance {
margin:0;
padding:0;
}
div.inheritance div.inheritance {
margin-left:2em;
}
ul.block-list,
ul.details-list,
ul.member-list,
ul.summary-list {
margin:10px 0 10px 0;
padding:0;
}
ul.block-list > li,
ul.details-list > li,
ul.member-list > li,
ul.summary-list > li {
list-style:none;
margin-bottom:15px;
line-height:1.4;
}
.summary-table dl, .summary-table dl dt, .summary-table dl dd {
margin-top:0;
margin-bottom:1px;
}
ul.see-list, ul.see-list-long {
padding-left: 0;
list-style: none;
}
ul.see-list li {
display: inline;
}
ul.see-list li:not(:last-child):after,
ul.see-list-long li:not(:last-child):after {
content: ", ";
white-space: pre-wrap;
}
/*
* Styles for tables.
*/
.summary-table, .details-table {
width:100%;
border-spacing:0;
border-left:1px solid #EEE;
border-right:1px solid #EEE;
border-bottom:1px solid #EEE;
padding:0;
}
.caption {
position:relative;
text-align:left;
background-repeat:no-repeat;
color:#253441;
font-weight:bold;
clear:none;
overflow:hidden;
padding:0;
padding-top:10px;
padding-left:1px;
margin:0;
white-space:pre;
font-family: 'DejaVu Sans';
}
.caption a:link, .caption a:visited {
color:#1f389c;
}
.caption a:hover,
.caption a:active {
color:#FFFFFF;
}
.caption span {
white-space:nowrap;
padding-top:5px;
padding-left:12px;
padding-right:12px;
padding-bottom:7px;
display:inline-block;
float:left;
background-color:#F8981D;
border: none;
height:16px;
}
div.table-tabs {
padding:10px 0 0 1px;
margin:0;
}
div.table-tabs > button {
border: none;
cursor: pointer;
padding: 5px 12px 7px 12px;
font-weight: bold;
margin-right: 3px;
}
div.table-tabs > button.active-table-tab {
background: #F8981D;
color: #253441;
}
div.table-tabs > button.table-tab {
background: #4D7A97;
color: #FFFFFF;
}
.two-column-summary {
display: grid;
grid-template-columns: minmax(15%, max-content) minmax(15%, auto);
}
.three-column-summary {
display: grid;
grid-template-columns: minmax(10%, max-content) minmax(15%, max-content) minmax(15%, auto);
}
#method-summary-table .three-column-summary {
grid-template-columns: minmax(10%, 20%) minmax(15%, max-content) minmax(15%, auto);
}
.four-column-summary {
display: grid;
grid-template-columns: minmax(10%, max-content) minmax(10%, max-content) minmax(10%, max-content) minmax(10%, auto);
}
@media screen and (max-width: 600px) {
.two-column-summary {
display: grid;
grid-template-columns: 1fr;
}
}
@media screen and (max-width: 800px) {
.three-column-summary {
display: grid;
grid-template-columns: minmax(10%, max-content) minmax(25%, auto);
}
.three-column-summary .col-last {
grid-column-end: span 2;
}
}
@media screen and (max-width: 1000px) {
.four-column-summary {
display: grid;
grid-template-columns: minmax(15%, max-content) minmax(15%, auto);
}
}
.summary-table > div, .details-table > div {
text-align:left;
padding: 8px 3px 3px 7px;
}
.col-first, .col-second, .col-last, .col-constructor-name, .col-summary-item-name {
vertical-align:top;
padding-right:0;
padding-top:8px;
padding-bottom:3px;
}
.table-header {
background:#dee3e9;
font-family: 'DejaVu Sans';
font-weight: bold;
}
/*
.col-first, .col-first {
font-size:13px;
}
.col-second, .col-second, .col-last, .col-constructor-name, .col-summary-item-name, .col-last {
font-size:13px;
}
*/
.col-first, .col-second, .col-constructor-name {
vertical-align:top;
overflow: auto;
}
.col-last {
white-space:normal;
}
/*
.col-first a:link, .col-first a:visited,
.col-second a:link, .col-second a:visited,
.col-first a:link, .col-first a:visited,
.col-second a:link, .col-second a:visited,
.col-constructor-name a:link, .col-constructor-name a:visited,
.col-summary-item-name a:link, .col-summary-item-name a:visited,
.constant-values-container a:link, .constant-values-container a:visited,
.all-classes-container a:link, .all-classes-container a:visited,
.all-packages-container a:link, .all-packages-container a:visited {
font-weight:bold;
}
*/
.table-sub-heading-color {
background-color:#EEEEFF;
}
.even-row-color, .even-row-color .table-header {
background-color:#FFFFFF;
}
.odd-row-color, .odd-row-color .table-header {
background-color:#EEEEEF;
}
/*
* Styles for contents.
*/
.deprecated-content {
margin:0;
padding:10px 0;
}
/*
div.block {
font-size:14px;
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
}
*/
.col-last div {
padding-top:0;
}
.col-last a {
padding-bottom:3px;
}
.module-signature,
.package-signature,
.type-signature,
.member-signature {
font-family: "DejaVu Sans Mono", monospace;
/* font-size:14px; */
margin:14px 0;
white-space: pre-wrap;
}
.module-signature,
.package-signature,
.type-signature {
margin-top: 0;
}
.member-signature .type-parameters-long,
.member-signature .parameters,
.member-signature .exceptions {
display: inline-block;
vertical-align: top;
white-space: pre;
}
.member-signature .type-parameters {
white-space: normal;
}
/*
* Styles for formatting effect.
*/
.source-line-no {
color:green;
padding:0 30px 0 0;
}
h1.hidden {
visibility:hidden;
overflow:hidden;
/* font-size:10px; */
}
.block {
display:block;
margin:0 10px 5px 0;
color:#474747;
}
.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link,
.module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type,
.package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label {
font-family: "DejaVu Sans", sans;
font-weight:bold;
}
.sub-title, .inheritance, .all-packages-table-tab1.col-first,
.summary-table .col-first {
font-family: "DejaVu Sans", sans;
}
.deprecation-comment, .help-footnote, .preview-comment {
font-style:italic;
}
.deprecation-block {
/* font-size:14px; */
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
border-style:solid;
border-width:thin;
border-radius:10px;
padding:10px;
margin-bottom:10px;
margin-right:10px;
display:inline-block;
}
.preview-block {
/* font-size:14px; */
font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif;
border-style:solid;
border-width:thin;
border-radius:10px;
padding:10px;
margin-bottom:10px;
margin-right:10px;
display:inline-block;
}
div.block div.deprecation-comment {
font-style:normal;
}
/*
* Styles specific to HTML5 elements.
*/
main, nav, header, footer, section {
display:block;
}
/*
* Styles for javadoc search.
*/
.ui-autocomplete-category {
font-weight:bold;
/* font-size:15px; */
padding:7px 0 7px 3px;
background-color:#4D7A97;
color:#FFFFFF;
}
.result-item {
/* font-size:13px; */
}
.ui-autocomplete {
max-height:85%;
max-width:65%;
overflow-y:scroll;
overflow-x:scroll;
white-space:nowrap;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}
ul.ui-autocomplete {
position:fixed;
z-index:999999;
}
ul.ui-autocomplete li {
float:left;
clear:both;
width:100%;
}
.result-highlight {
font-weight:bold;
}
#search-input {
background-image:url('resources/glass.png');
background-size:13px;
background-repeat:no-repeat;
background-position:2px 3px;
padding-left:20px;
position:relative;
right:-18px;
width:400px;
}
#reset-button {
background-color: rgb(255,255,255);
background-image:url('resources/x.png');
background-position:center;
background-repeat:no-repeat;
background-size:12px;
border:0 none;
width:16px;
height:16px;
position:relative;
left:-4px;
top:-4px;
font-size:0px;
}
.watermark {
color:#545454;
}
.search-tag-desc-result {
font-style:italic;
/* font-size:11px; */
}
.search-tag-holder-result {
font-style:italic;
/* font-size:12px; */
}
.search-tag-result:target {
background-color:yellow;
}
.module-graph span {
display:none;
position:absolute;
}
.module-graph:hover span {
display:block;
margin: -100px 0 0 100px;
z-index: 1;
}
.inherited-list {
margin: 10px 0 10px 0;
}
section.class-description {
line-height: 1.4;
}
.summary section[class$="-summary"], .details section[class$="-details"],
.class-uses .detail, .serialized-class-details {
padding: 0px 20px 5px 10px;
border: 1px solid #ededed;
background-color: #f8f8f8;
}
.inherited-list, section[class$="-details"] .detail {
padding:0 0 5px 8px;
background-color:#ffffff;
border:none;
}
.vertical-separator {
padding: 0 5px;
}
ul.help-section-list {
margin: 0;
}
ul.help-subtoc > li {
display: inline-block;
padding-right: 5px;
/* font-size: smaller; */
}
ul.help-subtoc > li::before {
content: "\2022" ;
padding-right:2px;
}
span.help-note {
font-style: italic;
}
/*
* Indicator icon for external links.
*/
main a[href*="://"]::after {
content:"";
display:inline-block;
background-image:url('data:image/svg+xml; utf8, \
<svg xmlns="http://www.w3.org/2000/svg" width="768" height="768">\
<path d="M584 664H104V184h216V80H0v688h688V448H584zM384 0l132 \
132-240 240 120 120 240-240 132 132V0z" fill="%234a6782"/>\
</svg>');
background-size:100% 100%;
width:7px;
height:7px;
margin-left:2px;
margin-bottom:4px;
}
main a[href*="://"]:hover::after,
main a[href*="://"]:focus::after {
background-image:url('data:image/svg+xml; utf8, \
<svg xmlns="http://www.w3.org/2000/svg" width="768" height="768">\
<path d="M584 664H104V184h216V80H0v688h688V448H584zM384 0l132 \
132-240 240 120 120 240-240 132 132V0z" fill="%23bb7a2a"/>\
</svg>');
}
/*
* Styles for user-provided tables.
*
* borderless:
* No borders, vertical margins, styled caption.
* This style is provided for use with existing doc comments.
* In general, borderless tables should not be used for layout purposes.
*
* plain:
* Plain borders around table and cells, vertical margins, styled caption.
* Best for small tables or for complex tables for tables with cells that span
* rows and columns, when the "striped" style does not work well.
*
* striped:
* Borders around the table and vertical borders between cells, striped rows,
* vertical margins, styled caption.
* Best for tables that have a header row, and a body containing a series of simple rows.
*/
table.borderless,
table.plain,
table.striped {
margin-top: 10px;
margin-bottom: 10px;
}
table.borderless > caption,
table.plain > caption,
table.striped > caption {
font-weight: bold;
/* font-size: smaller; */
}
table.borderless th, table.borderless td,
table.plain th, table.plain td,
table.striped th, table.striped td {
padding: 2px 5px;
}
table.borderless,
table.borderless > thead > tr > th, table.borderless > tbody > tr > th, table.borderless > tr > th,
table.borderless > thead > tr > td, table.borderless > tbody > tr > td, table.borderless > tr > td {
border: none;
}
table.borderless > thead > tr, table.borderless > tbody > tr, table.borderless > tr {
background-color: transparent;
}
table.plain {
border-collapse: collapse;
border: 1px solid black;
}
table.plain > thead > tr, table.plain > tbody tr, table.plain > tr {
background-color: transparent;
}
table.plain > thead > tr > th, table.plain > tbody > tr > th, table.plain > tr > th,
table.plain > thead > tr > td, table.plain > tbody > tr > td, table.plain > tr > td {
border: 1px solid black;
}
table.striped {
border-collapse: collapse;
border: 1px solid black;
}
table.striped > thead {
background-color: #E3E3E3;
}
table.striped > thead > tr > th, table.striped > thead > tr > td {
border: 1px solid black;
}
table.striped > tbody > tr:nth-child(even) {
background-color: #EEE
}
table.striped > tbody > tr:nth-child(odd) {
background-color: #FFF
}
table.striped > tbody > tr > th, table.striped > tbody > tr > td {
border-left: 1px solid black;
border-right: 1px solid black;
}
table.striped > tbody > tr > th {
font-weight: normal;
}
/**
* Tweak font sizes and paddings for small screens.
*/
@media screen and (max-width: 1050px) {
#search-input {
width: 300px;
}
}
@media screen and (max-width: 800px) {
#search-input {
width: 200px;
}
.top-nav,
.bottom-nav {
font-size: 80%;
padding-top: 6px;
}
.sub-nav {
font-size: 80%;
}
.about-language {
padding-right: 16px;
}
ul.nav-list li,
.sub-nav .nav-list-search {
padding: 6px;
}
ul.sub-nav-list li {
padding-top: 5px;
}
main {
padding: 10px;
}
.summary section[class$="-summary"], .details section[class$="-details"],
.class-uses .detail, .serialized-class-details {
padding: 0 8px 5px 8px;
}
body {
-webkit-text-size-adjust: none;
}
}
@media screen and (max-width: 500px) {
#search-input {
width: 150px;
}
.top-nav,
.bottom-nav {
font-size: 80%;
}
.sub-nav {
font-size: 80%;
}
.about-language {
font-size: 80%;
padding-right: 12px;
}
}

View file

@ -10,6 +10,8 @@ plugins {
dependencies { dependencies {
api project(':org.jdrupes.vmoperator.util') api project(':org.jdrupes.vmoperator.util')
api 'org.jgrapes:org.jgrapes.core:[1.22.1,2)'
api 'io.kubernetes:client-java:[19.0.0,20.0.0)' api 'io.kubernetes:client-java:[19.0.0,20.0.0)'
api 'org.yaml:snakeyaml' api 'org.yaml:snakeyaml'
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
} }

View file

@ -27,15 +27,101 @@ public class Constants {
/** The Constant APP_NAME. */ /** The Constant APP_NAME. */
public static final String APP_NAME = "vm-runner"; public static final String APP_NAME = "vm-runner";
/** The Constant COMP_DISPLAY_SECRETS. */
public static final String COMP_DISPLAY_SECRET = "display-secret";
/** The Constant VM_OP_NAME. */ /** The Constant VM_OP_NAME. */
public static final String VM_OP_NAME = "vm-operator"; public static final String VM_OP_NAME = "vm-operator";
/** The Constant VM_OP_GROUP. */ /**
public static final String VM_OP_GROUP = "vmoperator.jdrupes.org"; * Constants related to the CRD.
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Crd {
/** The Constant GROUP. */
public static final String GROUP = "vmoperator.jdrupes.org";
/** The Constant VM_OP_KIND_VM. */ /** The Constant KIND_VM. */
public static final String VM_OP_KIND_VM = "VirtualMachine"; public static final String KIND_VM = "VirtualMachine";
/** The Constant KIND_VM_POOL. */
public static final String KIND_VM_POOL = "VmPool";
}
/**
* Status related constants.
*/
public static class Status {
/** The Constant RUNNER_VERSION. */
public static final String RUNNER_VERSION = "runnerVersion";
/** The Constant CPUS. */
public static final String CPUS = "cpus";
/** The Constant RAM. */
public static final String RAM = "ram";
/** The Constant OSINFO. */
public static final String OSINFO = "osinfo";
/** The Constant DISPLAY_PASSWORD_SERIAL. */
public static final String DISPLAY_PASSWORD_SERIAL
= "displayPasswordSerial";
/** The Constant LOGGED_IN_USER. */
public static final String LOGGED_IN_USER = "loggedInUser";
/** The Constant CONSOLE_CLIENT. */
public static final String CONSOLE_CLIENT = "consoleClient";
/** The Constant CONSOLE_USER. */
public static final String CONSOLE_USER = "consoleUser";
/** The Constant ASSIGNMENT. */
public static final String ASSIGNMENT = "assignment";
/**
* Conditions used in Status.
*/
public static class Condition {
/** The Constant COND_RUNNING. */
public static final String RUNNING = "Running";
/** The Constant COND_BOOTED. */
public static final String BOOTED = "Booted";
/** The Constant COND_VMOP_AGENT. */
public static final String VMOP_AGENT = "VmopAgentConnected";
/** The Constant COND_USER_LOGGED_IN. */
public static final String USER_LOGGED_IN = "UserLoggedIn";
/** The Constant COND_CONSOLE. */
public static final String CONSOLE_CONNECTED = "ConsoleConnected";
/**
* Reasons used in conditions.
*/
public static class Reason {
/** The Constant NOT_REQUESTED. */
public static final String NOT_REQUESTED = "NotRequested";
/** The Constant USER_LOGGED_IN. */
public static final String LOGGED_IN = "LoggedIn";
}
}
}
/**
* DisplaySecret related constants.
*/
public static class DisplaySecret {
/** The Constant NAME. */
public static final String NAME = "display-secret";
/** The Constant PASSWORD. */
public static final String PASSWORD = "display-password";
/** The Constant EXPIRY. */
public static final String EXPIRY = "password-expiry";
}
} }

View file

@ -32,13 +32,11 @@ import java.util.regex.Pattern;
public class Convertions { public class Convertions {
@SuppressWarnings({ "PMD.UseConcurrentHashMap", @SuppressWarnings({ "PMD.UseConcurrentHashMap",
"PMD.FieldNamingConventions", "PMD.VariableNamingConventions" }) "PMD.FieldNamingConventions" })
private static final Map<String, BigInteger> unitMap = new HashMap<>(); private static final Map<String, BigInteger> unitMap = new HashMap<>();
@SuppressWarnings({ "PMD.FieldNamingConventions", @SuppressWarnings({ "PMD.FieldNamingConventions" })
"PMD.VariableNamingConventions" })
private static final List<Map.Entry<String, BigInteger>> unitMappings; private static final List<Map.Entry<String, BigInteger>> unitMappings;
@SuppressWarnings({ "PMD.FieldNamingConventions", @SuppressWarnings({ "PMD.FieldNamingConventions" })
"PMD.VariableNamingConventions" })
private static final Pattern memorySize private static final Pattern memorySize
= Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*"); = Pattern.compile("^\\s*(\\d+(\\.\\d+)?)\\s*([A-Za-z]*)\\s*");
@ -69,7 +67,6 @@ public class Convertions {
* @param amount the amount * @param amount the amount
* @return the big integer * @return the big integer
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public static BigInteger parseMemory(Object amount) { public static BigInteger parseMemory(Object amount) {
if (amount == null) { if (amount == null) {
return (BigInteger) amount; return (BigInteger) amount;

View file

@ -47,8 +47,7 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Helpers for K8s API. * Helpers for K8s API.
*/ */
@SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass", @SuppressWarnings({ "PMD.ShortClassName", "PMD.UseUtilityClass" })
"PMD.DataflowAnomalyAnalysis" })
public class K8s { public class K8s {
/** /**
@ -113,7 +112,6 @@ public class K8s {
public static JsonObject yamlToJson(ApiClient client, Reader yaml) { public static JsonObject yamlToJson(ApiClient client, Reader yaml) {
// Avoid Yaml.load due to // Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741 // https://github.com/kubernetes-client/java/issues/2741
@SuppressWarnings("PMD.UseConcurrentHashMap")
Map<String, Object> yamlData Map<String, Object> yamlData
= new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml); = new Yaml(new SafeConstructor(new LoaderOptions())).load(yaml);
@ -157,27 +155,6 @@ public class K8s {
return Optional.of(apiRes); return Optional.of(apiRes);
} }
/**
* Get an object from its metadata.
*
* @param <T> the generic type
* @param <LT> the generic type
* @param api the api
* @param meta the meta
* @return the object
*/
@Deprecated
@SuppressWarnings("PMD.GenericsNaming")
public static <T extends KubernetesObject, LT extends KubernetesListObject>
Optional<T>
get(GenericKubernetesApi<T, LT> api, V1ObjectMeta meta) {
var response = api.get(meta.getNamespace(), meta.getName());
if (response.isSuccess()) {
return Optional.of(response.getObject());
}
return Optional.empty();
}
/** /**
* Apply the given patch data. * Apply the given patch data.
* *

View file

@ -48,8 +48,7 @@ import okhttp3.Response;
* A client with some additional properties. * A client with some additional properties.
*/ */
@SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods", @SuppressWarnings({ "PMD.ExcessivePublicCount", "PMD.TooManyMethods",
"PMD.LinguisticNaming", "checkstyle:LineLength", "checkstyle:LineLength", "PMD.CouplingBetweenObjects", "PMD.GodClass" })
"PMD.CouplingBetweenObjects", "PMD.GodClass" })
public class K8sClient extends ApiClient { public class K8sClient extends ApiClient {
private ApiClient apiClient; private ApiClient apiClient;
@ -231,7 +230,6 @@ public class K8sClient extends ApiClient {
* @return the api client * @return the api client
* @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[]) * @see ApiClient#setKeyManagers(javax.net.ssl.KeyManager[])
*/ */
@SuppressWarnings("PMD.UseVarargs")
@Override @Override
public ApiClient setKeyManagers(KeyManager[] managers) { public ApiClient setKeyManagers(KeyManager[] managers) {
return apiClient().setKeyManagers(managers); return apiClient().setKeyManagers(managers);
@ -638,7 +636,6 @@ public class K8sClient extends ApiClient {
* @return the string * @return the string
* @see ApiClient#selectHeaderAccept(java.lang.String[]) * @see ApiClient#selectHeaderAccept(java.lang.String[])
*/ */
@SuppressWarnings("PMD.UseVarargs")
@Override @Override
public String selectHeaderAccept(String[] accepts) { public String selectHeaderAccept(String[] accepts) {
return apiClient().selectHeaderAccept(accepts); return apiClient().selectHeaderAccept(accepts);
@ -651,7 +648,6 @@ public class K8sClient extends ApiClient {
* @return the string * @return the string
* @see ApiClient#selectHeaderContentType(java.lang.String[]) * @see ApiClient#selectHeaderContentType(java.lang.String[])
*/ */
@SuppressWarnings("PMD.UseVarargs")
@Override @Override
public String selectHeaderContentType(String[] contentTypes) { public String selectHeaderContentType(String[] contentTypes) {
return apiClient().selectHeaderContentType(contentTypes); return apiClient().selectHeaderContentType(contentTypes);
@ -818,7 +814,7 @@ public class K8sClient extends ApiClient {
* @throws ApiException the api exception * @throws ApiException the api exception
* @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback) * @see ApiClient#buildCall(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
*/ */
@SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" }) @SuppressWarnings({ "rawtypes" })
@Override @Override
public Call buildCall(String path, String method, List<Pair> queryParams, public Call buildCall(String path, String method, List<Pair> queryParams,
List<Pair> collectionQueryParams, Object body, List<Pair> collectionQueryParams, Object body,
@ -847,7 +843,7 @@ public class K8sClient extends ApiClient {
* @throws ApiException the api exception * @throws ApiException the api exception
* @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback) * @see ApiClient#buildRequest(java.lang.String, java.lang.String, java.util.List, java.util.List, java.lang.Object, java.util.Map, java.util.Map, java.util.Map, java.lang.String[], io.kubernetes.client.openapi.ApiCallback)
*/ */
@SuppressWarnings({ "rawtypes", "PMD.ExcessiveParameterList" }) @SuppressWarnings({ "rawtypes" })
@Override @Override
public Request buildRequest(String path, String method, public Request buildRequest(String path, String method,
List<Pair> queryParams, List<Pair> collectionQueryParams, List<Pair> queryParams, List<Pair> collectionQueryParams,

View file

@ -45,7 +45,7 @@ import java.util.function.Function;
* @param <O> the generic type * @param <O> the generic type
* @param <L> the generic type * @param <L> the generic type
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis") @SuppressWarnings({ "PMD.CouplingBetweenObjects" })
public class K8sClusterGenericStub<O extends KubernetesObject, public class K8sClusterGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> { L extends KubernetesListObject> {
protected final K8sClient client; protected final K8sClient client;
@ -239,6 +239,7 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @param <L> the object list type * @param <L> the object list type
* @param <R> the result type * @param <R> the result type
*/ */
@FunctionalInterface
public interface GenericSupplier<O extends KubernetesObject, public interface GenericSupplier<O extends KubernetesObject,
L extends KubernetesListObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> { R extends K8sClusterGenericStub<O, L>> {
@ -253,7 +254,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @param name the name * @param name the name
* @return the result * @return the result
*/ */
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client, R get(Class<O> objectClass, Class<L> objectListClass, K8sClient client,
APIResource context, String name); APIResource context, String name);
} }
@ -282,7 +282,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> R extends K8sClusterGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass, R get(Class<O> objectClass, Class<L> objectListClass,
@ -313,8 +312,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> R extends K8sClusterGenericStub<O, L>>
R get(Class<O> objectClass, Class<L> objectListClass, R get(Class<O> objectClass, Class<L> objectListClass,
@ -339,8 +336,6 @@ public class K8sClusterGenericStub<O extends KubernetesObject,
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sClusterGenericStub<O, L>> R extends K8sClusterGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass, R create(Class<O> objectClass, Class<L> objectListClass,

View file

@ -29,7 +29,6 @@ import io.kubernetes.client.openapi.models.V1ObjectMeta;
* notably the metadata, is made available through the methods * notably the metadata, is made available through the methods
* defined by {@link KubernetesObject}. * defined by {@link KubernetesObject}.
*/ */
@SuppressWarnings("PMD.DataClass")
public class K8sDynamicModel implements KubernetesObject { public class K8sDynamicModel implements KubernetesObject {
private final V1ObjectMeta metadata; private final V1ObjectMeta metadata;
@ -102,7 +101,7 @@ public class K8sDynamicModel implements KubernetesObject {
* *
* @return the JSON object describing the status * @return the JSON object describing the status
*/ */
public JsonObject status() { public JsonObject statusJson() {
return data.getAsJsonObject("status"); return data.getAsJsonObject("status");
} }

View file

@ -31,7 +31,6 @@ import java.util.Collection;
* state and can therefore be used for any kind of object, especially * state and can therefore be used for any kind of object, especially
* custom objects. * custom objects.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sDynamicStub public class K8sDynamicStub
extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> { extends K8sDynamicStubBase<K8sDynamicModel, K8sDynamicModels> {
@ -64,8 +63,6 @@ public class K8sDynamicStub
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client, public static K8sDynamicStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name) GroupVersionKind gvk, String namespace, String name)
throws ApiException { throws ApiException {
@ -83,8 +80,6 @@ public class K8sDynamicStub
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static K8sDynamicStub get(K8sClient client, public static K8sDynamicStub get(K8sClient client,
APIResource context, String namespace, String name) { APIResource context, String namespace, String name) {
return new K8sDynamicStub(client, context, namespace, name); return new K8sDynamicStub(client, context, namespace, name);

View file

@ -26,7 +26,6 @@ import io.kubernetes.client.Discovery.APIResource;
* state and can therefore be used for any kind of object, especially * state and can therefore be used for any kind of object, especially
* custom objects. * custom objects.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public abstract class K8sDynamicStubBase<O extends K8sDynamicModel, public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
L extends K8sDynamicModelsBase<O>> extends K8sGenericStub<O, L> { L extends K8sDynamicModelsBase<O>> extends K8sGenericStub<O, L> {
@ -40,7 +39,6 @@ public abstract class K8sDynamicStubBase<O extends K8sDynamicModel,
* @param namespace the namespace * @param namespace the namespace
* @param name the name * @param name the name
*/ */
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
public K8sDynamicStubBase(Class<O> objectClass, public K8sDynamicStubBase(Class<O> objectClass,
Class<L> objectListClass, DynamicTypeAdapterFactory<O, L> taf, Class<L> objectListClass, DynamicTypeAdapterFactory<O, L> taf,
K8sClient client, APIResource context, String namespace, K8sClient client, APIResource context, String namespace,

View file

@ -27,6 +27,7 @@ import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.util.Strings; import io.kubernetes.client.util.Strings;
import io.kubernetes.client.util.generic.GenericKubernetesApi; import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.KubernetesApiResponse; import io.kubernetes.client.util.generic.KubernetesApiResponse;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.options.GetOptions; import io.kubernetes.client.util.generic.options.GetOptions;
import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions; import io.kubernetes.client.util.generic.options.PatchOptions;
@ -47,7 +48,7 @@ import java.util.function.Function;
* @param <O> the generic type * @param <O> the generic type
* @param <L> the generic type * @param <L> the generic type
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis") @SuppressWarnings({ "PMD.TooManyMethods" })
public class K8sGenericStub<O extends KubernetesObject, public class K8sGenericStub<O extends KubernetesObject,
L extends KubernetesListObject> { L extends KubernetesListObject> {
protected final K8sClient client; protected final K8sClient client;
@ -192,30 +193,92 @@ public class K8sGenericStub<O extends KubernetesObject,
} }
/** /**
* Updates the object's status. * Updates the object's status. Does not retry in case of conflict.
* *
* @param object the current state of the object (passed to `status`) * @param object the current state of the object (passed to `status`)
* @param status function that returns the new status * @param updater function that returns the new status
* @return the updated model or empty if not successful * @return the updated model or empty if the object was not found
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public Optional<O> updateStatus(O object, public Optional<O> updateStatus(O object, Function<O, Object> updater)
Function<O, Object> status) throws ApiException { throws ApiException {
return K8s.optional(api.updateStatus(object, status)); return K8s.optional(api.updateStatus(object, updater));
} }
/** /**
* Updates the status. * Updates the status of the given object. In case of conflict,
* get the current version of the object and tries again. Retries
* up to `retries` times.
* *
* @param status the status * @param updater the function updating the status
* @param current the current state of the object, used for the first
* attempt to update
* @param retries the retries in case of conflict
* @return the updated model or empty if the object was not found
* @throws ApiException the api exception
*/
@SuppressWarnings({ "PMD.AssignmentInOperand" })
public Optional<O> updateStatus(Function<O, Object> updater, O current,
int retries) throws ApiException {
while (true) {
try {
if (current == null) {
current = api.get(namespace, name)
.throwsApiException().getObject();
}
return updateStatus(current, updater);
} catch (ApiException e) {
if (HttpURLConnection.HTTP_CONFLICT != e.getCode()
|| retries-- <= 0) {
throw e;
}
// Get current version for new attempt
current = null;
}
}
}
/**
* Gets the object and updates the status. In case of conflict, retries
* up to `retries` times.
*
* @param updater the function updating the status
* @param retries the retries in case of conflict
* @return the updated model or empty if the object was not found
* @throws ApiException the api exception
*/
public Optional<O> updateStatus(Function<O, Object> updater, int retries)
throws ApiException {
return updateStatus(updater, null, retries);
}
/**
* Updates the status of the given object. In case of conflict,
* get the current version of the object and tries again. Retries
* up to `retries` times.
*
* @param updater the function updating the status
* @param current the current
* @return the kubernetes api response * @return the kubernetes api response
* the updated model or empty if not successful * the updated model or empty if not successful
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public Optional<O> updateStatus(Function<O, Object> status) public Optional<O> updateStatus(Function<O, Object> updater, O current)
throws ApiException { throws ApiException {
return updateStatus( return updateStatus(updater, current, 16);
api.get(namespace, name).throwsApiException().getObject(), status); }
/**
* Updates the status. In case of conflict, retries up to 16 times.
*
* @param updater the function updating the status
* @return the kubernetes api response
* the updated model or empty if not successful
* @throws ApiException the api exception
*/
public Optional<O> updateStatus(Function<O, Object> updater)
throws ApiException {
return updateStatus(updater, null);
} }
/** /**
@ -224,7 +287,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param patchType the patch type * @param patchType the patch type
* @param patch the patch * @param patch the patch
* @param options the options * @param options the options
* @return the kubernetes api response * @return the kubernetes api response if successful
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public Optional<O> patch(String patchType, V1Patch patch, public Optional<O> patch(String patchType, V1Patch patch,
@ -239,7 +302,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* *
* @param patchType the patch type * @param patchType the patch type
* @param patch the patch * @param patch the patch
* @return the kubernetes api response * @return the kubernetes api response if successful
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
public Optional<O> public Optional<O>
@ -248,6 +311,21 @@ public class K8sGenericStub<O extends KubernetesObject,
return patch(patchType, patch, opts); return patch(patchType, patch, opts);
} }
/**
* Apply the given definition.
*
* @param def the def
* @return the kubernetes api response if successful
* @throws ApiException the api exception
*/
public Optional<O> apply(DynamicKubernetesObject def) throws ApiException {
PatchOptions opts = new PatchOptions();
opts.setForce(true);
opts.setFieldManager("kubernetes-java-kubectl-apply");
return patch(V1Patch.PATCH_FORMAT_APPLY_YAML,
new V1Patch(client.getJSON().serialize(def)), opts);
}
/** /**
* Update the object. * Update the object.
* *
@ -279,6 +357,7 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param <L> the object list type * @param <L> the object list type
* @param <R> the result type * @param <R> the result type
*/ */
@FunctionalInterface
public interface GenericSupplier<O extends KubernetesObject, public interface GenericSupplier<O extends KubernetesObject,
L extends KubernetesListObject, R extends K8sGenericStub<O, L>> { L extends KubernetesListObject, R extends K8sGenericStub<O, L>> {
@ -290,7 +369,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @param name the name * @param name the name
* @return the result * @return the result
*/ */
@SuppressWarnings("PMD.UseObjectForClearerAPI")
R get(K8sClient client, String namespace, String name); R get(K8sClient client, String namespace, String name);
} }
@ -316,8 +394,6 @@ public class K8sGenericStub<O extends KubernetesObject,
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static <O extends KubernetesObject, L extends KubernetesListObject, public static <O extends KubernetesObject, L extends KubernetesListObject,
R extends K8sGenericStub<O, L>> R extends K8sGenericStub<O, L>>
R create(Class<O> objectClass, Class<L> objectListClass, R create(Class<O> objectClass, Class<L> objectListClass,

View file

@ -27,9 +27,11 @@ import io.kubernetes.client.util.generic.GenericKubernetesApi;
import io.kubernetes.client.util.generic.options.ListOptions; import io.kubernetes.client.util.generic.options.ListOptions;
import java.time.Duration; import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.Optional;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jgrapes.core.Components;
/** /**
* An observer that watches namespaced resources in a given context and * An observer that watches namespaced resources in a given context and
@ -48,7 +50,6 @@ public class K8sObserver<O extends KubernetesObject,
ADDED, MODIFIED, DELETED ADDED, MODIFIED, DELETED
} }
@SuppressWarnings("PMD.FieldNamingConventions")
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
protected final K8sClient client; protected final K8sClient client;
@ -71,9 +72,8 @@ public class K8sObserver<O extends KubernetesObject,
* @param namespace the namespace * @param namespace the namespace
* @param options the options * @param options the options
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop", @SuppressWarnings({ "PMD.AvoidCatchingThrowable",
"PMD.UseObjectForClearerAPI", "PMD.AvoidCatchingThrowable", "PMD.CognitiveComplexity", "PMD.AvoidCatchingGenericException" })
"PMD.CognitiveComplexity" })
public K8sObserver(Class<O> objectClass, Class<L> objectListClass, public K8sObserver(Class<O> objectClass, Class<L> objectListClass,
K8sClient client, APIResource context, String namespace, K8sClient client, APIResource context, String namespace,
ListOptions options) { ListOptions options) {
@ -85,23 +85,32 @@ public class K8sObserver<O extends KubernetesObject,
api = new GenericKubernetesApi<>(objectClass, objectListClass, api = new GenericKubernetesApi<>(objectClass, objectListClass,
context.getGroup(), context.getPreferredVersion(), context.getGroup(), context.getPreferredVersion(),
context.getResourcePlural(), client); context.getResourcePlural(), client);
thread = new Thread(() -> { thread = (Components.useVirtualThreads() ? Thread.ofVirtual()
: Thread.ofPlatform()).unstarted(() -> {
try { try {
logger.config(() -> "Watching " + context.getResourcePlural() logger.fine(() -> "Observing " + context.getResourcePlural()
+ " (" + context.getPreferredVersion() + ")" + " (" + context.getPreferredVersion() + ")"
+ Optional.ofNullable(options.getLabelSelector())
.map(ls -> " with labels " + ls).orElse("")
+ " in " + namespace); + " in " + namespace);
// Watch sometimes terminates without apparent reason. // Watch sometimes terminates without apparent reason.
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
Instant startedAt = Instant.now(); Instant startedAt = Instant.now();
try { try {
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") var changed
var changed = api.watch(namespace, options).iterator(); = api.watch(namespace, options).iterator();
while (changed.hasNext()) { while (changed.hasNext()) {
handler.accept(client, changed.next()); var response = changed.next();
logger.fine(() -> "Resource "
+ context.getKind() + "/"
+ response.object.getMetadata().getName()
+ " " + response.type);
handler.accept(client, response);
} }
} catch (ApiException e) { } catch (ApiException | RuntimeException e) {
logger.log(Level.FINE, e, () -> "Problem watching" logger.log(Level.FINE, e, () -> "Problem watching"
+ " resource " + context.getKind()
+ " (will retry): " + e.getMessage()); + " (will retry): " + e.getMessage());
delayRestart(startedAt); delayRestart(startedAt);
} }
@ -117,7 +126,6 @@ public class K8sObserver<O extends KubernetesObject,
} }
} }
}); });
thread.setDaemon(true);
} }
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition") @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
@ -222,7 +230,6 @@ public class K8sObserver<O extends KubernetesObject,
} }
@Override @Override
@SuppressWarnings("PMD.UseLocaleWithCaseConversions")
public String toString() { public String toString() {
return "Observer for " + K8s.toString(context) + " " + namespace; return "Observer for " + K8s.toString(context) + " " + namespace;
} }

View file

@ -26,7 +26,6 @@ import java.util.List;
/** /**
* A stub for config maps (v1). * A stub for config maps (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1ConfigMapStub public class K8sV1ConfigMapStub
extends K8sGenericStub<V1ConfigMap, V1ConfigMapList> { extends K8sGenericStub<V1ConfigMap, V1ConfigMapList> {

View file

@ -29,7 +29,6 @@ import java.util.Optional;
/** /**
* A stub for pods (v1). * A stub for pods (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1DeploymentStub public class K8sV1DeploymentStub
extends K8sGenericStub<V1Deployment, V1DeploymentList> { extends K8sGenericStub<V1Deployment, V1DeploymentList> {

View file

@ -29,7 +29,6 @@ import java.util.List;
/** /**
* A stub for nodes (v1). * A stub for nodes (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1NodeStub extends K8sClusterGenericStub<V1Node, V1NodeList> { public class K8sV1NodeStub extends K8sClusterGenericStub<V1Node, V1NodeList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"), public static final APIResource CONTEXT = new APIResource("", List.of("v1"),
@ -74,8 +73,7 @@ public class K8sV1NodeStub extends K8sClusterGenericStub<V1Node, V1NodeList> {
/** /**
* Provide {@link GenericSupplier}. * Provide {@link GenericSupplier}.
*/ */
@SuppressWarnings({ "PMD.UnusedFormalParameter", @SuppressWarnings({ "PMD.UnusedFormalParameter" })
"PMD.UnusedPrivateMethod" })
private static K8sV1NodeStub getGeneric(Class<V1Node> objectClass, private static K8sV1NodeStub getGeneric(Class<V1Node> objectClass,
Class<V1NodeList> objectListClass, K8sClient client, Class<V1NodeList> objectListClass, K8sClient client,
APIResource context, String name) { APIResource context, String name) {

View file

@ -29,7 +29,6 @@ import java.util.List;
/** /**
* A stub for pods (v1). * A stub for pods (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> { public class K8sV1PodStub extends K8sGenericStub<V1Pod, V1PodList> {
/** The pods' context. */ /** The pods' context. */

View file

@ -0,0 +1,81 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import io.kubernetes.client.Discovery.APIResource;
import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim;
import io.kubernetes.client.openapi.models.V1PersistentVolumeClaimList;
import io.kubernetes.client.util.generic.options.ListOptions;
import java.util.Collection;
import java.util.List;
/**
* A stub for pods (v1).
*/
public class K8sV1PvcStub extends
K8sGenericStub<V1PersistentVolumeClaim, V1PersistentVolumeClaimList> {
/** The pods' context. */
public static final APIResource CONTEXT
= new APIResource("", List.of("v1"), "v1", "PersistentVolumeClaim",
true, "persistentvolumeclaims", "persistentvolumeclaim");
/**
* Instantiates a new stub.
*
* @param client the client
* @param namespace the namespace
* @param name the name
*/
protected K8sV1PvcStub(K8sClient client, String namespace, String name) {
super(V1PersistentVolumeClaim.class, V1PersistentVolumeClaimList.class,
client, CONTEXT, namespace, name);
}
/**
* Gets the stub for the given namespace and name.
*
* @param client the client
* @param namespace the namespace
* @param name the name
* @return the kpod stub
*/
public static K8sV1PvcStub get(K8sClient client, String namespace,
String name) {
return new K8sV1PvcStub(client, namespace, name);
}
/**
* Get the stubs for the objects in the given namespace that match
* the criteria from the given options.
*
* @param client the client
* @param namespace the namespace
* @param options the options
* @return the collection
* @throws ApiException the api exception
*/
public static Collection<K8sV1PvcStub> list(K8sClient client,
String namespace, ListOptions options) throws ApiException {
return K8sGenericStub.list(V1PersistentVolumeClaim.class,
V1PersistentVolumeClaimList.class, client, CONTEXT, namespace,
options, (clnt, nscp, name) -> new K8sV1PvcStub(clnt, nscp, name));
}
}

View file

@ -29,7 +29,6 @@ import java.util.List;
/** /**
* A stub for secrets (v1). * A stub for secrets (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> { public class K8sV1SecretStub extends K8sGenericStub<V1Secret, V1SecretList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"), public static final APIResource CONTEXT = new APIResource("", List.of("v1"),

View file

@ -29,7 +29,6 @@ import java.util.List;
/** /**
* A stub for secrets (v1). * A stub for secrets (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> { public class K8sV1ServiceStub extends K8sGenericStub<V1Service, V1ServiceList> {
public static final APIResource CONTEXT = new APIResource("", List.of("v1"), public static final APIResource CONTEXT = new APIResource("", List.of("v1"),

View file

@ -26,7 +26,6 @@ import java.util.List;
/** /**
* A stub for stateful sets (v1). * A stub for stateful sets (v1).
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class K8sV1StatefulSetStub public class K8sV1StatefulSetStub
extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> { extends K8sGenericStub<V1StatefulSet, V1StatefulSetList> {

View file

@ -0,0 +1,499 @@
/*
* VM-Operator
* Copyright (C) 2025 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import io.kubernetes.client.openapi.JSON;
import io.kubernetes.client.openapi.models.V1Condition;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.jdrupes.vmoperator.common.Constants.Status;
import org.jdrupes.vmoperator.common.Constants.Status.Condition;
import org.jdrupes.vmoperator.common.Constants.Status.Condition.Reason;
import org.jdrupes.vmoperator.util.DataPath;
/**
* Represents a VM definition.
*/
@SuppressWarnings({ "PMD.DataClass", "PMD.TooManyMethods" })
public class VmDefinition extends K8sDynamicModel {
@SuppressWarnings({ "unused" })
private static final Logger logger
= Logger.getLogger(VmDefinition.class.getName());
@SuppressWarnings("PMD.FieldNamingConventions")
private static final Gson gson = new JSON().getGson();
@SuppressWarnings("PMD.FieldNamingConventions")
private static final ObjectMapper objectMapper
= new ObjectMapper().registerModule(new JavaTimeModule());
private final Model model;
private VmExtraData extraData;
/**
* The VM state from the VM definition.
*/
public enum RequestedVmState {
STOPPED, RUNNING
}
/**
* Permissions for accessing and manipulating the VM.
*/
public enum Permission {
START("start"), STOP("stop"), RESET("reset"),
ACCESS_CONSOLE("accessConsole"), TAKE_CONSOLE("takeConsole");
@SuppressWarnings("PMD.UseConcurrentHashMap")
private static Map<String, Permission> reprs = new HashMap<>();
static {
for (var value : EnumSet.allOf(Permission.class)) {
reprs.put(value.repr, value);
}
}
private final String repr;
Permission(String repr) {
this.repr = repr;
}
/**
* Create permission from representation in CRD.
*
* @param value the value
* @return the permission
*/
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
public static Set<Permission> parse(String value) {
if ("*".equals(value)) {
return EnumSet.allOf(Permission.class);
}
return Set.of(reprs.get(value));
}
/**
* To string.
*
* @return the string
*/
@Override
public String toString() {
return repr;
}
}
/**
* Permissions granted to a user or role.
*
* @param user the user
* @param role the role
* @param may the may
*/
public record Grant(String user, String role, Set<Permission> may) {
/**
* To string.
*
* @return the string
*/
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
if (user != null) {
builder.append("User ").append(user);
} else {
builder.append("Role ").append(role);
}
builder.append(" may=").append(may).append(']');
return builder.toString();
}
}
/**
* The assignment information.
*
* @param pool the pool
* @param user the user
* @param lastUsed the last used
*/
public record Assignment(String pool, String user, Instant lastUsed) {
}
/**
* Instantiates a new vm definition.
*
* @param delegate the delegate
* @param json the json
*/
public VmDefinition(Gson delegate, JsonObject json) {
super(delegate, json);
model = gson.fromJson(json, Model.class);
}
/**
* Gets the spec.
*
* @return the spec
*/
public Map<String, Object> spec() {
return model.getSpec();
}
/**
* Get a value from the spec using {@link DataPath#get}.
*
* @param <T> the generic type
* @param selectors the selectors
* @return the value, if found
*/
public <T> Optional<T> fromSpec(Object... selectors) {
return DataPath.get(spec(), selectors);
}
/**
* The pools that this VM belongs to.
*
* @return the list
*/
public List<String> pools() {
return this.<List<String>> fromSpec("pools")
.orElse(Collections.emptyList());
}
/**
* Get a value from the `spec().get("vm")` using {@link DataPath#get}.
*
* @param <T> the generic type
* @param selectors the selectors
* @return the value, if found
*/
public <T> Optional<T> fromVm(Object... selectors) {
return DataPath.get(spec(), "vm")
.flatMap(vm -> DataPath.get(vm, selectors));
}
/**
* Gets the status.
*
* @return the status
*/
public Map<String, Object> status() {
return model.getStatus();
}
/**
* Get a value from the status using {@link DataPath#get}.
*
* @param <T> the generic type
* @param selectors the selectors
* @return the value, if found
*/
public <T> Optional<T> fromStatus(Object... selectors) {
return DataPath.get(status(), selectors);
}
/**
* The assignment information.
*
* @return the optional
*/
public Optional<Assignment> assignment() {
return this.<Map<String, Object>> fromStatus(Status.ASSIGNMENT)
.filter(m -> !m.isEmpty()).map(a -> new Assignment(
a.get("pool").toString(), a.get("user").toString(),
Instant.parse(a.get("lastUsed").toString())));
}
/**
* Return a condition from the status.
*
* @param name the condition's name
* @return the status, if the condition is defined
*/
public Optional<V1Condition> condition(String name) {
return this.<List<Map<String, Object>>> fromStatus("conditions")
.orElse(Collections.emptyList()).stream()
.filter(cond -> DataPath.get(cond, "type")
.map(name::equals).orElse(false))
.findFirst()
.map(cond -> objectMapper.convertValue(cond, V1Condition.class));
}
/**
* Return a condition's status.
*
* @param name the condition's name
* @return the status, if the condition is defined
*/
public Optional<Boolean> conditionStatus(String name) {
return this.<List<Map<String, Object>>> fromStatus("conditions")
.orElse(Collections.emptyList()).stream()
.filter(cond -> DataPath.get(cond, "type")
.map(name::equals).orElse(false))
.findFirst().map(cond -> DataPath.get(cond, "status")
.map("True"::equals).orElse(false));
}
/**
* Return true if the console is in use.
*
* @return true, if successful
*/
public boolean consoleConnected() {
return conditionStatus("ConsoleConnected").orElse(false);
}
/**
* Return the last known console user.
*
* @return the optional
*/
public Optional<String> consoleUser() {
return this.<String> fromStatus(Status.CONSOLE_USER);
}
/**
* Set extra data (unknown to kubernetes).
* @return the VM definition
*/
/* default */ VmDefinition extra(VmExtraData extraData) {
this.extraData = extraData;
return this;
}
/**
* Return the extra data.
*
* @return the data
*/
public VmExtraData extra() {
return extraData;
}
/**
* Returns the definition's name.
*
* @return the string
*/
public String name() {
return metadata().getName();
}
/**
* Returns the definition's namespace.
*
* @return the string
*/
public String namespace() {
return metadata().getNamespace();
}
/**
* Return the requested VM state.
*
* @return the string
*/
public RequestedVmState vmState() {
return fromVm("state")
.map(s -> "Running".equals(s) ? RequestedVmState.RUNNING
: RequestedVmState.STOPPED)
.orElse(RequestedVmState.STOPPED);
}
/**
* Collect all permissions for the given user with the given roles.
* If permission "takeConsole" is granted, the result will also
* contain "accessConsole" to simplify checks.
*
* @param user the user
* @param roles the roles
* @return the sets the
*/
public Set<Permission> permissionsFor(String user,
Collection<String> roles) {
var result = this.<List<Map<String, Object>>> fromSpec("permissions")
.orElse(Collections.emptyList()).stream()
.filter(p -> DataPath.get(p, "user").map(u -> u.equals(user))
.orElse(false)
|| DataPath.get(p, "role").map(roles::contains).orElse(false))
.map(p -> DataPath.<List<String>> get(p, "may")
.orElse(Collections.emptyList()).stream())
.flatMap(Function.identity())
.map(Permission::parse).map(Set::stream)
.flatMap(Function.identity())
.collect(Collectors.toCollection(HashSet::new));
// Take console implies access console, simplify checks
if (result.contains(Permission.TAKE_CONSOLE)) {
result.add(Permission.ACCESS_CONSOLE);
}
return result;
}
/**
* Check if the console is accessible. Always returns `true` if
* the VM is running and the permissions allow taking over the
* console. Else, returns `true` if
*
* * the permissions allow access to the console and
*
* * the VM is running and
*
* * the console is currently unused or used by the given user and
*
* * if user login is requested, the given user is logged in.
*
* @param user the user
* @param permissions the permissions
* @return true, if successful
*/
@SuppressWarnings("PMD.SimplifyBooleanReturns")
public boolean consoleAccessible(String user, Set<Permission> permissions) {
// Basic checks
if (!conditionStatus(Condition.RUNNING).orElse(false)) {
return false;
}
if (permissions.contains(Permission.TAKE_CONSOLE)) {
return true;
}
if (!permissions.contains(Permission.ACCESS_CONSOLE)) {
return false;
}
// If the console is in use by another user, deny access
if (conditionStatus(Condition.CONSOLE_CONNECTED).orElse(false)
&& !consoleUser().map(cu -> cu.equals(user)).orElse(false)) {
return false;
}
// If no login is requested, allow access, else check if user matches
if (condition(Condition.USER_LOGGED_IN).map(V1Condition::getReason)
.map(r -> Reason.NOT_REQUESTED.equals(r)).orElse(false)) {
return true;
}
return user.equals(status().get(Status.LOGGED_IN_USER));
}
/**
* Get the display password serial.
*
* @return the optional
*/
public Optional<Long> displayPasswordSerial() {
return this.<Number> fromStatus(Status.DISPLAY_PASSWORD_SERIAL)
.map(Number::longValue);
}
/**
* Hash code.
*
* @return the int
*/
@Override
public int hashCode() {
return Objects.hash(metadata().getNamespace(), metadata().getName());
}
/**
* Equals.
*
* @param obj the obj
* @return true, if successful
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
VmDefinition other = (VmDefinition) obj;
return Objects.equals(metadata().getNamespace(),
other.metadata().getNamespace())
&& Objects.equals(metadata().getName(), other.metadata().getName());
}
/**
* The Class Model.
*/
public static class Model {
private Map<String, Object> spec;
private Map<String, Object> status;
/**
* Gets the spec.
*
* @return the spec
*/
public Map<String, Object> getSpec() {
return spec;
}
/**
* Sets the spec.
*
* @param spec the spec to set
*/
public void setSpec(Map<String, Object> spec) {
this.spec = spec;
}
/**
* Gets the status.
*
* @return the status
*/
public Map<String, Object> getStatus() {
return status;
}
/**
* Sets the status.
*
* @param status the status to set
*/
public void setStatus(Map<String, Object> status) {
this.status = status;
}
}
}

View file

@ -1,123 +0,0 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jdrupes.vmoperator.util.GsonPtr;
/**
* Represents a VM definition.
*/
@SuppressWarnings("PMD.DataClass")
public class VmDefinitionModel extends K8sDynamicModel {
/**
* Permissions for accessing and manipulating the VM.
*/
public enum Permission {
START("start"), STOP("stop"), ACCESS_CONSOLE("accessConsole");
@SuppressWarnings("PMD.UseConcurrentHashMap")
private static Map<String, Permission> reprs = new HashMap<>();
static {
for (var value : EnumSet.allOf(Permission.class)) {
reprs.put(value.repr, value);
}
}
private final String repr;
Permission(String repr) {
this.repr = repr;
}
/**
* Create permission from representation in CRD.
*
* @param value the value
* @return the permission
*/
@SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
public static Set<Permission> parse(String value) {
if ("*".equals(value)) {
return EnumSet.allOf(Permission.class);
}
return Set.of(reprs.get(value));
}
@Override
public String toString() {
return repr;
}
}
/**
* Instantiates a new model from the JSON representation.
*
* @param delegate the gson instance to use for extracting structured data
* @param json the JSON
*/
public VmDefinitionModel(Gson delegate, JsonObject json) {
super(delegate, json);
}
/**
* Collect all permissions for the given user with the given roles.
*
* @param user the user
* @param roles the roles
* @return the sets the
*/
public Set<Permission> permissionsFor(String user,
Collection<String> roles) {
return GsonPtr.to(data())
.getAsListOf(JsonObject.class, "spec", "permissions")
.stream().filter(p -> GsonPtr.to(p).getAsString("user")
.map(u -> u.equals(user)).orElse(false)
|| GsonPtr.to(p).getAsString("role").map(roles::contains)
.orElse(false))
.map(p -> GsonPtr.to(p).getAsListOf(JsonPrimitive.class, "may")
.stream())
.flatMap(Function.identity()).map(p -> p.getAsString())
.map(Permission::parse).map(Set::stream)
.flatMap(Function.identity()).collect(Collectors.toSet());
}
/**
* Get the display password serial.
*
* @return the optional
*/
public Optional<Long> displayPasswordSerial() {
return GsonPtr.to(status())
.get(JsonPrimitive.class, "displayPasswordSerial")
.map(JsonPrimitive::getAsLong);
}
}

View file

@ -31,12 +31,11 @@ import java.util.Collection;
* state and can therefore be used for any kind of object, especially * state and can therefore be used for any kind of object, especially
* custom objects. * custom objects.
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
public class VmDefinitionStub public class VmDefinitionStub
extends K8sDynamicStubBase<VmDefinitionModel, VmDefinitionModels> { extends K8sDynamicStubBase<VmDefinition, VmDefinitions> {
private static DynamicTypeAdapterFactory<VmDefinitionModel, private static DynamicTypeAdapterFactory<VmDefinition,
VmDefinitionModels> taf = new VmDefintionModelTypeAdapterFactory(); VmDefinitions> taf = new VmDefintionModelTypeAdapterFactory();
/** /**
* Instantiates a new stub for VM defintions. * Instantiates a new stub for VM defintions.
@ -48,7 +47,7 @@ public class VmDefinitionStub
*/ */
public VmDefinitionStub(K8sClient client, APIResource context, public VmDefinitionStub(K8sClient client, APIResource context,
String namespace, String name) { String namespace, String name) {
super(VmDefinitionModel.class, VmDefinitionModels.class, taf, client, super(VmDefinition.class, VmDefinitions.class, taf, client,
context, namespace, name); context, namespace, name);
} }
@ -64,8 +63,6 @@ public class VmDefinitionStub
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static VmDefinitionStub get(K8sClient client, public static VmDefinitionStub get(K8sClient client,
GroupVersionKind gvk, String namespace, String name) GroupVersionKind gvk, String namespace, String name)
throws ApiException { throws ApiException {
@ -83,8 +80,6 @@ public class VmDefinitionStub
* @return the stub if the object exists * @return the stub if the object exists
* @throws ApiException the api exception * @throws ApiException the api exception
*/ */
@SuppressWarnings({ "PMD.AvoidBranchingStatementAsLastInLoop",
"PMD.AvoidInstantiatingObjectsInLoops", "PMD.UseObjectForClearerAPI" })
public static VmDefinitionStub get(K8sClient client, public static VmDefinitionStub get(K8sClient client,
APIResource context, String namespace, String name) { APIResource context, String namespace, String name) {
return new VmDefinitionStub(client, context, namespace, name); return new VmDefinitionStub(client, context, namespace, name);
@ -101,10 +96,10 @@ public class VmDefinitionStub
*/ */
public static VmDefinitionStub createFromYaml(K8sClient client, public static VmDefinitionStub createFromYaml(K8sClient client,
APIResource context, Reader yaml) throws ApiException { APIResource context, Reader yaml) throws ApiException {
var model = new VmDefinitionModel(client.getJSON().getGson(), var model = new VmDefinition(client.getJSON().getGson(),
K8s.yamlToJson(client, yaml)); K8s.yamlToJson(client, yaml));
return K8sGenericStub.create(VmDefinitionModel.class, return K8sGenericStub.create(VmDefinition.class,
VmDefinitionModels.class, client, context, model, VmDefinitions.class, client, context, model,
(c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); (c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
} }
@ -121,8 +116,8 @@ public class VmDefinitionStub
public static Collection<VmDefinitionStub> list(K8sClient client, public static Collection<VmDefinitionStub> list(K8sClient client,
APIResource context, String namespace, ListOptions options) APIResource context, String namespace, ListOptions options)
throws ApiException { throws ApiException {
return K8sGenericStub.list(VmDefinitionModel.class, return K8sGenericStub.list(VmDefinition.class,
VmDefinitionModels.class, client, context, namespace, options, VmDefinitions.class, client, context, namespace, options,
(c, ns, n) -> new VmDefinitionStub(c, context, ns, n)); (c, ns, n) -> new VmDefinitionStub(c, context, ns, n));
} }
@ -144,13 +139,13 @@ public class VmDefinitionStub
* A factory for creating VmDefinitionModel(s) objects. * A factory for creating VmDefinitionModel(s) objects.
*/ */
public static class VmDefintionModelTypeAdapterFactory extends public static class VmDefintionModelTypeAdapterFactory extends
DynamicTypeAdapterFactory<VmDefinitionModel, VmDefinitionModels> { DynamicTypeAdapterFactory<VmDefinition, VmDefinitions> {
/** /**
* Instantiates a new dynamic model type adapter factory. * Instantiates a new dynamic model type adapter factory.
*/ */
public VmDefintionModelTypeAdapterFactory() { public VmDefintionModelTypeAdapterFactory() {
super(VmDefinitionModel.class, VmDefinitionModels.class); super(VmDefinition.class, VmDefinitions.class);
} }
} }

View file

@ -22,10 +22,10 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
/** /**
* Represents a list of {@link VmDefinitionModel}s. * Represents a list of {@link VmDefinition}s.
*/ */
public class VmDefinitionModels public class VmDefinitions
extends K8sDynamicModelsBase<VmDefinitionModel> { extends K8sDynamicModelsBase<VmDefinition> {
/** /**
* Initialize the object list using the given JSON data. * Initialize the object list using the given JSON data.
@ -33,7 +33,7 @@ public class VmDefinitionModels
* @param delegate the gson instance to use for extracting structured data * @param delegate the gson instance to use for extracting structured data
* @param data the data * @param data the data
*/ */
public VmDefinitionModels(Gson delegate, JsonObject data) { public VmDefinitions(Gson delegate, JsonObject data) {
super(VmDefinitionModel.class, delegate, data); super(VmDefinition.class, delegate, data);
} }
} }

View file

@ -0,0 +1,179 @@
/*
* VM-Operator
* Copyright (C) 2025 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import io.kubernetes.client.util.Strings;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Represents internally used dynamic data associated with a
* {@link VmDefinition}.
*/
public class VmExtraData {
private static final Logger logger
= Logger.getLogger(VmExtraData.class.getName());
private final VmDefinition vmDef;
private String nodeName = "";
private List<String> nodeAddresses = Collections.emptyList();
private long resetCount;
/**
* Initializes a new instance.
*
* @param vmDef the VM definition
*/
public VmExtraData(VmDefinition vmDef) {
this.vmDef = vmDef;
vmDef.extra(this);
}
/**
* Sets the node info.
*
* @param name the name
* @param addresses the addresses
* @return the VM extra data
*/
public VmExtraData nodeInfo(String name, List<String> addresses) {
nodeName = name;
nodeAddresses = addresses;
return this;
}
/**
* Return the node name.
*
* @return the string
*/
public String nodeName() {
return nodeName;
}
/**
* Gets the node addresses.
*
* @return the nodeAddresses
*/
public List<String> nodeAddresses() {
return nodeAddresses;
}
/**
* Sets the reset count.
*
* @param resetCount the reset count
* @return the vm extra data
*/
public VmExtraData resetCount(long resetCount) {
this.resetCount = resetCount;
return this;
}
/**
* Returns the reset count.
*
* @return the long
*/
public long resetCount() {
return resetCount;
}
/**
* Create a connection file.
*
* @param password the password
* @param preferredIpVersion the preferred IP version
* @param deleteConnectionFile the delete connection file
* @return the string
*/
public Optional<String> connectionFile(String password,
Class<?> preferredIpVersion, boolean deleteConnectionFile) {
var addr = displayIp(preferredIpVersion);
if (addr.isEmpty()) {
logger
.severe(() -> "Failed to find display IP for " + vmDef.name());
return Optional.empty();
}
var port = vmDef.<Number> fromVm("display", "spice", "port")
.map(Number::longValue);
if (port.isEmpty()) {
logger
.severe(() -> "No port defined for display of " + vmDef.name());
return Optional.empty();
}
StringBuffer data = new StringBuffer(100)
.append("[virt-viewer]\ntype=spice\nhost=")
.append(addr.get().getHostAddress()).append("\nport=")
.append(port.get().toString())
.append('\n');
if (password != null) {
data.append("password=").append(password).append('\n');
}
vmDef.<String> fromVm("display", "spice", "proxyUrl")
.ifPresent(u -> {
if (!Strings.isNullOrEmpty(u)) {
data.append("proxy=").append(u).append('\n');
}
});
if (deleteConnectionFile) {
data.append("delete-this-file=1\n");
}
return Optional.of(data.toString());
}
private Optional<InetAddress> displayIp(Class<?> preferredIpVersion) {
Optional<String> server = vmDef.fromVm("display", "spice", "server");
if (server.isPresent()) {
var srv = server.get();
try {
var addr = InetAddress.getByName(srv);
logger.fine(() -> "Using IP address from CRD for "
+ vmDef.metadata().getName() + ": " + addr);
return Optional.of(addr);
} catch (UnknownHostException e) {
logger.log(Level.SEVERE, e, () -> "Invalid server address "
+ srv + ": " + e.getMessage());
return Optional.empty();
}
}
var addrs = nodeAddresses.stream().map(a -> {
try {
return InetAddress.getByName(a);
} catch (UnknownHostException e) {
logger.warning(() -> "Invalid IP address: " + a);
return null;
}
}).filter(Objects::nonNull).toList();
logger.fine(
() -> "Known IP addresses for " + vmDef.name() + ": " + addrs);
return addrs.stream()
.filter(a -> preferredIpVersion.isAssignableFrom(a.getClass()))
.findFirst().or(() -> addrs.stream().findFirst());
}
}

View file

@ -0,0 +1,226 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.common;
import java.time.Duration;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jdrupes.vmoperator.common.VmDefinition.Assignment;
import org.jdrupes.vmoperator.common.VmDefinition.Grant;
import org.jdrupes.vmoperator.common.VmDefinition.Permission;
import org.jdrupes.vmoperator.util.DataPath;
/**
* Represents a VM pool.
*/
public class VmPool {
private final String name;
private String retention;
private boolean loginOnAssignment;
private boolean defined;
private List<Grant> permissions = Collections.emptyList();
private final Set<String> vms
= Collections.synchronizedSet(new HashSet<>());
/**
* Instantiates a new vm pool.
*
* @param name the name
*/
public VmPool(String name) {
this.name = name;
}
/**
* Fill the properties of a provisionally created pool from
* the definition.
*
* @param definition the definition
*/
public void defineFrom(VmPool definition) {
retention = definition.retention();
permissions = definition.permissions();
loginOnAssignment = definition.loginOnAssignment();
defined = true;
}
/**
* Returns the name.
*
* @return the name
*/
public String name() {
return name;
}
/**
* Checks if is login on assignment.
*
* @return the loginOnAssignment
*/
public boolean loginOnAssignment() {
return loginOnAssignment;
}
/**
* Checks if is defined.
*
* @return the result
*/
public boolean isDefined() {
return defined;
}
/**
* Marks the pool as undefined.
*/
public void setUndefined() {
defined = false;
}
/**
* Gets the retention.
*
* @return the retention
*/
public String retention() {
return retention;
}
/**
* Permissions granted for a VM from the pool.
*
* @return the permissions
*/
public List<Grant> permissions() {
return permissions;
}
/**
* Returns the VM names.
*
* @return the vms
*/
public Set<String> vms() {
return vms;
}
/**
* Collect all permissions for the given user with the given roles.
*
* @param user the user
* @param roles the roles
* @return the sets the
*/
public Set<Permission> permissionsFor(String user,
Collection<String> roles) {
return permissions.stream()
.filter(g -> DataPath.get(g, "user").map(u -> u.equals(user))
.orElse(false)
|| DataPath.get(g, "role").map(roles::contains).orElse(false))
.map(g -> DataPath.<Set<Permission>> get(g, "may")
.orElse(Collections.emptySet()).stream())
.flatMap(Function.identity()).collect(Collectors.toSet());
}
/**
* Checks if the given VM belongs to the pool and is not in use.
*
* @param vmDef the vm def
* @return true, if is assignable
*/
@SuppressWarnings("PMD.SimplifyBooleanReturns")
public boolean isAssignable(VmDefinition vmDef) {
// Check if the VM is in the pool
if (!vmDef.pools().contains(name)) {
return false;
}
// Check if the VM is not in use
if (vmDef.consoleConnected()) {
return false;
}
// If not assigned, it's usable
if (vmDef.assignment().isEmpty()) {
return true;
}
// Check if it is to be retained
if (vmDef.assignment().map(Assignment::lastUsed).map(this::retainUntil)
.map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
return false;
}
// Additional check in case lastUsed has not been updated
// by PoolMonitor#onVmResourceChanged() yet ("race condition")
if (vmDef.condition("ConsoleConnected")
.map(cc -> cc.getLastTransitionTime().toInstant())
.map(this::retainUntil)
.map(ru -> Instant.now().isBefore(ru)).orElse(false)) {
return false;
}
return true;
}
/**
* Return the instant until which an assignment should be retained.
*
* @param lastUsed the last used
* @return the instant
*/
public Instant retainUntil(Instant lastUsed) {
if (retention.startsWith("P")) {
return lastUsed.plus(Duration.parse(retention));
}
return Instant.parse(retention);
}
/**
* To string.
*
* @return the string
*/
@Override
@SuppressWarnings({ "PMD.AvoidLiteralsInIfCondition",
"PMD.AvoidSynchronizedStatement" })
public String toString() {
StringBuilder builder = new StringBuilder(50);
builder.append("VmPool [name=").append(name).append(", permissions=")
.append(permissions).append(", vms=");
if (vms.size() <= 3) {
builder.append(vms);
} else {
synchronized (vms) {
builder.append('[').append(vms.stream().limit(3)
.map(s -> s + ",").collect(Collectors.joining()))
.append("...]");
}
}
builder.append(']');
return builder.toString();
}
}

View file

@ -9,7 +9,5 @@ plugins {
} }
dependencies { dependencies {
api 'org.jgrapes:org.jgrapes.core:[1.19.0,2)'
api project(':org.jdrupes.vmoperator.common') api project(':org.jdrupes.vmoperator.common')
api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:[2.16.1,3]'
} }

View file

@ -18,45 +18,43 @@
package org.jdrupes.vmoperator.manager.events; package org.jdrupes.vmoperator.manager.events;
import java.util.Optional; import org.jdrupes.vmoperator.manager.events.GetVms.VmData;
import org.jdrupes.vmoperator.common.VmDefinitionModel;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
* Gets the current display secret and optionally updates it. * Assign a VM from a pool to a user.
*/ */
@SuppressWarnings("PMD.DataClass") public class AssignVm extends Event<VmData> {
public class GetDisplayPassword extends Event<String> {
private final VmDefinitionModel vmDef; private final String fromPool;
private final String toUser;
/** /**
* Instantiates a new returns the display secret. * Instantiates a new event.
* *
* @param vmDef the vm name * @param fromPool the from pool
* @param toUser the to user
*/ */
public GetDisplayPassword(VmDefinitionModel vmDef) { public AssignVm(String fromPool, String toUser) {
this.vmDef = vmDef; this.fromPool = fromPool;
this.toUser = toUser;
} }
/** /**
* Gets the vm definition. * Gets the pool to assign from.
* *
* @return the vm definition * @return the pool
*/ */
public VmDefinitionModel vmDefinition() { public String fromPool() {
return vmDef; return fromPool;
} }
/** /**
* Return the password. May only be called when the event is completed. * Gets the user to assign to.
* *
* @return the optional * @return the to user
*/ */
public Optional<String> password() { public String toUser() {
if (!isDone()) { return toUser;
throw new IllegalStateException("Event is not done.");
}
return currentResults().stream().findFirst();
} }
} }

View file

@ -0,0 +1,112 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import org.jgrapes.core.Channel;
/**
* Supports the lookup of a channel by a name (an id). As a convenience,
* it is possible to additionally associate arbitrary data with the entry
* (and thus with the channel). Note that this interface defines a
* read-only view of the dictionary.
*
* @param <K> the key type
* @param <C> the channel type
* @param <A> the type of the associated data
*/
public interface ChannelDictionary<K, C extends Channel, A> {
/**
* Combines the channel and the associated data.
*
* @param <C> the channel type
* @param <A> the type of the associated data
* @param channel the channel
* @param associated the associated
*/
public record Value<C extends Channel, A>(C channel, A associated) {
}
/**
* Returns all known keys.
*
* @return the keys
*/
Set<K> keys();
/**
* Return all known values.
*
* @return the collection
*/
Collection<Value<C, A>> values();
/**
* Returns the channel and associates data registered for the key
* or an empty optional if no entry exists.
*
* @param key the key
* @return the result
*/
Optional<Value<C, A>> value(K key);
/**
* Return all known channels.
*
* @return the collection
*/
default Collection<C> channels() {
return values().stream().map(v -> v.channel).toList();
}
/**
* Returns the channel registered for the key or an empty optional
* if no mapping exists.
*
* @param key the key
* @return the optional
*/
default Optional<C> channel(K key) {
return value(key).map(b -> b.channel);
}
/**
* Returns all known associated data.
*
* @return the collection
*/
default Collection<A> associated() {
return values().stream()
.filter(v -> v.associated() != null)
.map(v -> v.associated).toList();
}
/**
* Return the data associated with the entry for the channel.
*
* @param key the key
* @return the data
*/
default Optional<A> associated(K key) {
return value(key).map(b -> b.associated);
}
}

View file

@ -27,53 +27,24 @@ import java.util.function.Function;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
/** /**
* A channel manager that maintains mappings from a key to a channel. * Provides an actively managed implementation of the {@link ChannelDictionary}.
* As a convenience, it is possible to additionally associate arbitrary
* data with the entry (and thus with the channel).
* *
* The manager should be used by a component that defines channels for * The {@link ChannelManager} can be used for housekeeping by any component
* housekeeping. It can be shared between this component and another * that creates channels. It can be shared between this component and
* component, preferably using the {@link #fixed()} view for the * some other component, preferably passing it as {@link ChannelDictionary}
* second component. Alternatively, the second component can use a * (the read-only view) to the second component. Alternatively, the other
* {@link ChannelCache} to track the mappings using events. * component can use a {@link ChannelTracker} to track the mappings using
* events.
* *
* @param <K> the key type * @param <K> the key type
* @param <C> the channel type * @param <C> the channel type
* @param <A> the type of the associated data * @param <A> the type of the associated data
*/ */
public class ChannelManager<K, C extends Channel, A> { public class ChannelManager<K, C extends Channel, A>
implements ChannelDictionary<K, C, A> {
private final Map<K, Both<C, A>> channels = new ConcurrentHashMap<>(); private final Map<K, Value<C, A>> entries = new ConcurrentHashMap<>();
private final Function<K, C> supplier; private final Function<K, C> supplier;
private ChannelManager<K, C, A> readOnly;
/**
* Combines the channel and the associated data.
*
* @param <C> the generic type
* @param <A> the generic type
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Both<C extends Channel, A> {
/** The channel. */
public C channel;
/** The associated. */
public A associated;
/**
* Instantiates a new both.
*
* @param channel the channel
* @param associated the associated
*/
public Both(C channel, A associated) {
super();
this.channel = channel;
this.associated = associated;
}
}
/** /**
* Instantiates a new channel manager. * Instantiates a new channel manager.
@ -91,6 +62,26 @@ public class ChannelManager<K, C extends Channel, A> {
this(k -> null); this(k -> null);
} }
/**
* Return all keys.
*
* @return the keys.
*/
@Override
public Set<K> keys() {
return entries.keySet();
}
/**
* Return all known values.
*
* @return the collection
*/
@Override
public Collection<Value<C, A>> values() {
return entries.values();
}
/** /**
* Returns the channel and associates data registered for the key * Returns the channel and associates data registered for the key
* or an empty optional if no mapping exists. * or an empty optional if no mapping exists.
@ -98,10 +89,8 @@ public class ChannelManager<K, C extends Channel, A> {
* @param key the key * @param key the key
* @return the result * @return the result
*/ */
public Optional<Both<C, A>> both(K key) { public Optional<Value<C, A>> value(K key) {
synchronized (channels) { return Optional.ofNullable(entries.get(key));
return Optional.ofNullable(channels.get(key));
}
} }
/** /**
@ -113,7 +102,7 @@ public class ChannelManager<K, C extends Channel, A> {
* @return the channel manager * @return the channel manager
*/ */
public ChannelManager<K, C, A> put(K key, C channel, A associated) { public ChannelManager<K, C, A> put(K key, C channel, A associated) {
channels.put(key, new Both<>(channel, associated)); entries.put(key, new Value<>(channel, associated));
return this; return this;
} }
@ -130,14 +119,15 @@ public class ChannelManager<K, C extends Channel, A> {
} }
/** /**
* Returns the channel registered for the key or an empty optional * Creates a new channel without adding it to the channel manager.
* if no mapping exists. * After fully initializing the channel, it should be added to the
* manager using {@link #put(K, C)}.
* *
* @param key the key * @param key the key
* @return the optional * @return the c
*/ */
public Optional<C> channel(K key) { public C createChannel(K key) {
return both(key).map(b -> b.channel); return supplier.apply(key);
} }
/** /**
@ -147,8 +137,8 @@ public class ChannelManager<K, C extends Channel, A> {
* @param key the key * @param key the key
* @return the channel * @return the channel
*/ */
public Optional<C> getChannel(K key) { public C channelGet(K key) {
return getChannel(key, supplier); return computeIfAbsent(key, supplier);
} }
/** /**
@ -159,19 +149,9 @@ public class ChannelManager<K, C extends Channel, A> {
* @param supplier the supplier * @param supplier the supplier
* @return the channel * @return the channel
*/ */
@SuppressWarnings({ "PMD.AssignmentInOperand", public C computeIfAbsent(K key, Function<K, C> supplier) {
"PMD.DataflowAnomalyAnalysis" }) return entries.computeIfAbsent(key,
public Optional<C> getChannel(K key, Function<K, C> supplier) { k -> new Value<>(supplier.apply(k), null)).channel();
synchronized (channels) {
return Optional
.of(Optional.ofNullable(channels.get(key))
.map(v -> v.channel)
.orElseGet(() -> {
var channel = supplier.apply(key);
channels.put(key, new Both<>(channel, null));
return channel;
}));
}
} }
/** /**
@ -183,121 +163,17 @@ public class ChannelManager<K, C extends Channel, A> {
* @return the channel manager * @return the channel manager
*/ */
public ChannelManager<K, C, A> associate(K key, A data) { public ChannelManager<K, C, A> associate(K key, A data) {
synchronized (channels) { Optional.ofNullable(entries.computeIfPresent(key,
Optional.ofNullable(channels.get(key)) (k, existing) -> new Value<>(existing.channel(), data)));
.ifPresent(v -> v.associated = data);
}
return this; return this;
} }
/**
* Return the data associated with the entry for the channel.
*
* @param key the key
* @return the data
*/
public Optional<A> associated(K key) {
return both(key).map(b -> b.associated);
}
/**
* Returns all associated data.
*
* @return the collection
*/
public Collection<A> associated() {
synchronized (channels) {
return channels.values().stream()
.filter(v -> v.associated != null)
.map(v -> v.associated).toList();
}
}
/** /**
* Removes the channel with the given name. * Removes the channel with the given name.
* *
* @param name the name * @param name the name
*/ */
public void remove(String name) { public void remove(String name) {
synchronized (channels) { entries.remove(name);
channels.remove(name);
}
}
/**
* Returns all known keys.
*
* @return the sets the
*/
public Set<K> keys() {
return channels.keySet();
}
/**
* Returns a read only view of this channel manager. The methods
* that usually create a new entry refrain from doing so. The
* methods that change the value of channel and {@link #remove(String)}
* do nothing. The associated data, however, can still be changed.
*
* @return the channel manager
*/
public ChannelManager<K, C, A> fixed() {
if (readOnly == null) {
readOnly = new ChannelManager<>(supplier) {
@Override
public Optional<Both<C, A>> both(K key) {
return ChannelManager.this.both(key);
}
@Override
public ChannelManager<K, C, A> put(K key, C channel,
A associated) {
return associate(key, associated);
}
@Override
public Optional<C> getChannel(K key) {
return ChannelManager.this.channel(key);
}
@Override
public Optional<C> getChannel(K key, Function<K, C> supplier) {
return ChannelManager.this.channel(key);
}
@Override
public ChannelManager<K, C, A> associate(K key, A data) {
return ChannelManager.this.associate(key, data);
}
@Override
public Optional<A> associated(K key) {
return ChannelManager.this.associated(key);
}
@Override
public Collection<A> associated() {
return ChannelManager.this.associated();
}
@Override
public void remove(String name) {
// Do nothing
}
@Override
public Set<K> keys() {
return ChannelManager.this.keys();
}
@Override
public ChannelManager<K, C, A> fixed() {
return ChannelManager.this.fixed();
}
};
}
return readOnly;
} }
} }

View file

@ -19,6 +19,7 @@
package org.jdrupes.vmoperator.manager.events; package org.jdrupes.vmoperator.manager.events;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -27,20 +28,30 @@ import java.util.concurrent.ConcurrentHashMap;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
/** /**
* A channel manager that tracks mappings from a key to a channel using * Used to track mapping from a key to a channel. Entries must
* "add/remove" (or "open/close") events and the channels on which they * be maintained by handlers for "add/remove" (or "open/close")
* are delivered. * events delivered on the channels that are to be
* made available by the tracker.
*
* The channels are stored in the dictionary using {@link WeakReference}s.
* Removing entries is therefore best practice but not an absolute necessity
* as entries for cleared references are removed when one of the methods
* {@link #values()}, {@link #channels()} or {@link #associated()} is called.
* *
* @param <K> the key type * @param <K> the key type
* @param <C> the channel type * @param <C> the channel type
* @param <A> the type of the associated data * @param <A> the type of the associated data
*/ */
public class ChannelCache<K, C extends Channel, A> { public class ChannelTracker<K, C extends Channel, A>
implements ChannelDictionary<K, C, A> {
private final Map<K, Data<C, A>> channels = new ConcurrentHashMap<>(); private final Map<K, Data<C, A>> entries = new ConcurrentHashMap<>();
/** /**
* Helper * Combines the channel and associated data.
*
* @param <C> the generic type
* @param <A> the generic type
*/ */
@SuppressWarnings("PMD.ShortClassName") @SuppressWarnings("PMD.ShortClassName")
private static class Data<C extends Channel, A> { private static class Data<C extends Channel, A> {
@ -57,32 +68,24 @@ public class ChannelCache<K, C extends Channel, A> {
} }
} }
/** @Override
* Combines the channel and the associated data. public Set<K> keys() {
* return entries.keySet();
* @param <C> the generic type
* @param <A> the generic type
*/
@SuppressWarnings("PMD.ShortClassName")
public static class Both<C extends Channel, A> {
/** The channel. */
public C channel;
/** The associated. */
public A associated;
/**
* Instantiates a new both.
*
* @param channel the channel
* @param associated the associated
*/
public Both(C channel, A associated) {
super();
this.channel = channel;
this.associated = associated;
} }
@Override
public Collection<Value<C, A>> values() {
var result = new ArrayList<Value<C, A>>();
for (var itr = entries.entrySet().iterator(); itr.hasNext();) {
var value = itr.next().getValue();
var channel = value.channel.get();
if (channel == null) {
itr.remove();
continue;
}
result.add(new Value<>(channel, value.associated));
}
return result;
} }
/** /**
@ -92,20 +95,18 @@ public class ChannelCache<K, C extends Channel, A> {
* @param key the key * @param key the key
* @return the result * @return the result
*/ */
public Optional<Both<C, A>> both(K key) { public Optional<Value<C, A>> value(K key) {
synchronized (channels) { var value = entries.get(key);
var value = channels.get(key);
if (value == null) { if (value == null) {
return Optional.empty(); return Optional.empty();
} }
var channel = value.channel.get(); var channel = value.channel.get();
if (channel == null) { if (channel == null) {
// Cleanup old reference // Cleanup old reference
channels.remove(key); entries.remove(key);
return Optional.empty(); return Optional.empty();
} }
return Optional.of(new Both<>(channel, value.associated)); return Optional.of(new Value<>(channel, value.associated));
}
} }
/** /**
@ -116,10 +117,10 @@ public class ChannelCache<K, C extends Channel, A> {
* @param associated the associated * @param associated the associated
* @return the channel manager * @return the channel manager
*/ */
public ChannelCache<K, C, A> put(K key, C channel, A associated) { public ChannelTracker<K, C, A> put(K key, C channel, A associated) {
Data<C, A> data = new Data<>(channel); Data<C, A> data = new Data<>(channel);
data.associated = associated; data.associated = associated;
channels.put(key, data); entries.put(key, data);
return this; return this;
} }
@ -130,22 +131,11 @@ public class ChannelCache<K, C extends Channel, A> {
* @param channel the channel * @param channel the channel
* @return the channel manager * @return the channel manager
*/ */
public ChannelCache<K, C, A> put(K key, C channel) { public ChannelTracker<K, C, A> put(K key, C channel) {
put(key, channel, null); put(key, channel, null);
return this; return this;
} }
/**
* Returns the channel registered for the key or an empty optional
* if no mapping exists.
*
* @param key the key
* @return the optional
*/
public Optional<C> channel(K key) {
return both(key).map(b -> b.channel);
}
/** /**
* Associate the entry for the channel with the given data. The entry * Associate the entry for the channel with the given data. The entry
* for the channel must already exist. * for the channel must already exist.
@ -154,54 +144,18 @@ public class ChannelCache<K, C extends Channel, A> {
* @param data the data * @param data the data
* @return the channel manager * @return the channel manager
*/ */
public ChannelCache<K, C, A> associate(K key, A data) { public ChannelTracker<K, C, A> associate(K key, A data) {
synchronized (channels) { Optional.ofNullable(entries.get(key))
Optional.ofNullable(channels.get(key))
.ifPresent(v -> v.associated = data); .ifPresent(v -> v.associated = data);
}
return this; return this;
} }
/**
* Return the data associated with the entry for the channel.
*
* @param key the key
* @return the data
*/
public Optional<A> associated(K key) {
return both(key).map(b -> b.associated);
}
/**
* Returns all associated data.
*
* @return the collection
*/
public Collection<A> associated() {
synchronized (channels) {
return channels.values().stream()
.filter(v -> v.channel.get() != null && v.associated != null)
.map(v -> v.associated).toList();
}
}
/** /**
* Removes the channel with the given name. * Removes the channel with the given name.
* *
* @param name the name * @param name the name
*/ */
public void remove(String name) { public void remove(String name) {
synchronized (channels) { entries.remove(name);
channels.remove(name);
}
}
/**
* Returns all known keys.
*
* @return the sets the
*/
public Set<K> keys() {
return channels.keySet();
} }
} }

View file

@ -0,0 +1,92 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Event;
/**
* Gets the current display secret and optionally updates it.
*/
public class GetDisplaySecret extends Event<String> {
private final VmDefinition vmDef;
private final String user;
/**
* Instantiates a new request for the display secret.
* After handling the event, a result of `null` means that
* no secret is needed. No result means that the console
* is not accessible.
*
* @param vmDef the vm name
* @param user the requesting user
*/
public GetDisplaySecret(VmDefinition vmDef, String user) {
this.vmDef = vmDef;
this.user = user;
}
/**
* Gets the VM definition.
*
* @return the VM definition
*/
public VmDefinition vmDefinition() {
return vmDef;
}
/**
* Return the id of the user who has requested the password.
*
* @return the string
*/
public String user() {
return user;
}
/**
* Returns `true` if a password is available. May only be called
* when the event is completed. Note that the password returned
* by {@link #secret()} may be `null`, indicating that no password
* is needed.
*
* @return true, if successful
*/
public boolean secretAvailable() {
if (!isDone()) {
throw new IllegalStateException("Event is not done.");
}
return !currentResults().isEmpty();
}
/**
* Return the secret. May only be called when the event has been
* completed with a valid result (see {@link #secretAvailable()}).
*
* @return the password. A value of `null` means that no password
* is required.
*/
public String secret() {
if (!isDone() || currentResults().isEmpty()) {
throw new IllegalStateException("Event is not done.");
}
return currentResults().get(0);
}
}

View file

@ -0,0 +1,87 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.jdrupes.vmoperator.common.VmPool;
import org.jgrapes.core.Event;
/**
* Gets the known pools' definitions.
*/
public class GetPools extends Event<List<VmPool>> {
private String name;
private String user;
private List<String> roles = Collections.emptyList();
/**
* Return only the pool with the given name.
*
* @param name the name
* @return the returns the vms
*/
public GetPools withName(String name) {
this.name = name;
return this;
}
/**
* Return only {@link VmPool}s that are accessible by
* the given user or roles.
*
* @param user the user
* @param roles the roles
* @return the event
*/
public GetPools accessibleFor(String user, List<String> roles) {
this.user = user;
this.roles = roles;
return this;
}
/**
* Returns the name filter criterion, if set.
*
* @return the optional
*/
public Optional<String> name() {
return Optional.ofNullable(name);
}
/**
* Returns the user filter criterion, if set.
*
* @return the optional
*/
public Optional<String> forUser() {
return Optional.ofNullable(user);
}
/**
* Returns the roles criterion.
*
* @return the list
*/
public List<String> forRoles() {
return roles;
}
}

View file

@ -0,0 +1,138 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Event;
/**
* Gets the known VMs' definitions and channels.
*/
public class GetVms extends Event<List<GetVms.VmData>> {
private String name;
private String user;
private List<String> roles = Collections.emptyList();
private String fromPool;
private String toUser;
/**
* Return only the VMs with the given name.
*
* @param name the name
* @return the returns the vms
*/
public GetVms withName(String name) {
this.name = name;
return this;
}
/**
* Return only {@link VmDefinition}s that are accessible by
* the given user or roles.
*
* @param user the user
* @param roles the roles
* @return the event
*/
public GetVms accessibleFor(String user, List<String> roles) {
this.user = user;
this.roles = roles;
return this;
}
/**
* Return only {@link VmDefinition}s that are assigned from the given pool.
*
* @param pool the pool
* @return the returns the vms
*/
public GetVms assignedFrom(String pool) {
this.fromPool = pool;
return this;
}
/**
* Return only {@link VmDefinition}s that are assigned to the given user.
*
* @param user the user
* @return the returns the vms
*/
public GetVms assignedTo(String user) {
this.toUser = user;
return this;
}
/**
* Returns the name filter criterion, if set.
*
* @return the optional
*/
public Optional<String> name() {
return Optional.ofNullable(name);
}
/**
* Returns the user filter criterion, if set.
*
* @return the optional
*/
public Optional<String> user() {
return Optional.ofNullable(user);
}
/**
* Returns the roles criterion.
*
* @return the list
*/
public List<String> roles() {
return roles;
}
/**
* Returns the pool filter criterion, if set.
*
* @return the optional
*/
public Optional<String> fromPool() {
return Optional.ofNullable(fromPool);
}
/**
* Returns the user filter criterion, if set.
*
* @return the optional
*/
public Optional<String> toUser() {
return Optional.ofNullable(toUser);
}
/**
* Return tuple.
*
* @param definition the definition
* @param channel the channel
*/
public record VmData(VmDefinition definition, VmChannel channel) {
}
}

View file

@ -24,7 +24,6 @@ import org.jgrapes.core.Event;
/** /**
* Modifies a VM. * Modifies a VM.
*/ */
@SuppressWarnings("PMD.DataClass")
public class ModifyVm extends Event<Void> { public class ModifyVm extends Event<Void> {
private final String name; private final String name;

View file

@ -1,6 +1,6 @@
/* /*
* VM-Operator * VM-Operator
* Copyright (C) 2024 Michael N. Lipp * Copyright (C) 2023 Michael N. Lipp
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -18,30 +18,38 @@
package org.jdrupes.vmoperator.manager.events; package org.jdrupes.vmoperator.manager.events;
import io.kubernetes.client.openapi.models.V1Service; import io.kubernetes.client.openapi.models.V1Pod;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Components; import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
* Indicates that a service has changed. * Indicates a change in a pod that runs a VM.
*/ */
@SuppressWarnings("PMD.DataClass") public class PodChanged extends Event<Void> {
public class ServiceChanged extends Event<Void> {
private final ResponseType type; private final V1Pod pod;
private final V1Service service; private final K8sObserver.ResponseType type;
/** /**
* Initializes a new service changed event. * Instantiates a new VM changed event.
* *
* @param pod the pod
* @param type the type * @param type the type
* @param service the service
*/ */
public ServiceChanged(ResponseType type, V1Service service) { public PodChanged(V1Pod pod, K8sObserver.ResponseType type) {
this.pod = pod;
this.type = type; this.type = type;
this.service = service; }
/**
* Gets the pod.
*
* @return the pod
*/
public V1Pod pod() {
return pod;
} }
/** /**
@ -49,24 +57,15 @@ public class ServiceChanged extends Event<Void> {
* *
* @return the type * @return the type
*/ */
public ResponseType type() { public K8sObserver.ResponseType type() {
return type; return type;
} }
/**
* Gets the service.
*
* @return the service
*/
public V1Service service() {
return service;
}
@Override @Override
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [") builder.append(Components.objectName(this)).append(" [")
.append(service.getMetadata().getName()).append(' ').append(type); .append(pod.getMetadata().getName()).append(' ').append(type);
if (channels() != null) { if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels())); builder.append(", channels=").append(Channel.toString(channels()));
} }

View file

@ -0,0 +1,47 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import org.jgrapes.core.Event;
/**
* Triggers a reset of the VM.
*/
public class ResetVm extends Event<String> {
private final String vmName;
/**
* Instantiates a new event.
*
* @param vmName the vm name
*/
public ResetVm(String vmName) {
this.vmName = vmName;
}
/**
* Gets the vm name.
*
* @return the vm name
*/
public String vmName() {
return vmName;
}
}

View file

@ -0,0 +1,60 @@
/*
* VM-Operator
* Copyright (C) 2024 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.VmPool;
import org.jgrapes.core.Event;
/**
* Note the assignment to a user in the VM status.
*/
public class UpdateAssignment extends Event<Boolean> {
private final VmPool fromPool;
private final String toUser;
/**
* Instantiates a new event.
*
* @param fromPool the pool from which the VM was assigned
* @param toUser the to user
*/
public UpdateAssignment(VmPool fromPool, String toUser) {
this.fromPool = fromPool;
this.toUser = toUser;
}
/**
* Gets the pool from which the VM was assigned.
*
* @return the pool
*/
public VmPool fromPool() {
return fromPool;
}
/**
* Gets the user to whom the VM was assigned.
*
* @return the to user
*/
public String toUser() {
return toUser;
}
}

View file

@ -19,20 +19,20 @@
package org.jdrupes.vmoperator.manager.events; package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.VmDefinitionModel; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Event;
import org.jgrapes.core.EventPipeline; import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.Subchannel.DefaultSubchannel; import org.jgrapes.core.Subchannel.DefaultSubchannel;
/** /**
* A subchannel used to send the events related to a specific VM. * A subchannel used to send the events related to a specific VM.
*/ */
@SuppressWarnings("PMD.DataClass")
public class VmChannel extends DefaultSubchannel { public class VmChannel extends DefaultSubchannel {
private final EventPipeline pipeline; private final EventPipeline pipeline;
private final K8sClient client; private final K8sClient client;
private VmDefinitionModel vmDefinition; private VmDefinition definition;
private long generation = -1; private long generation = -1;
/** /**
@ -55,19 +55,18 @@ public class VmChannel extends DefaultSubchannel {
* @param definition the definition * @param definition the definition
* @return the watch channel * @return the watch channel
*/ */
@SuppressWarnings("PMD.LinguisticNaming") public VmChannel setVmDefinition(VmDefinition definition) {
public VmChannel setVmDefinition(VmDefinitionModel definition) { this.definition = definition;
this.vmDefinition = definition;
return this; return this;
} }
/** /**
* Returns the last known definition of the VM. * Returns the last known definition of the VM.
* *
* @return the json object * @return the defintion
*/ */
public VmDefinitionModel vmDefinition() { public VmDefinition vmDefinition() {
return vmDefinition; return definition;
} }
/** /**
@ -86,7 +85,6 @@ public class VmChannel extends DefaultSubchannel {
* @param generation the generation to set * @param generation the generation to set
* @return true if value has changed * @return true if value has changed
*/ */
@SuppressWarnings("PMD.LinguisticNaming")
public boolean setGeneration(long generation) { public boolean setGeneration(long generation) {
if (this.generation == generation) { if (this.generation == generation) {
return false; return false;
@ -104,6 +102,19 @@ public class VmChannel extends DefaultSubchannel {
return pipeline; return pipeline;
} }
/**
* Fire the given event on this channel, using the associated
* {@link #pipeline()}.
*
* @param <T> the generic type
* @param event the event
* @return the t
*/
public <T extends Event<?>> T fire(T event) {
pipeline.fire(event, this);
return event;
}
/** /**
* Returns the API client. * Returns the API client.
* *

View file

@ -0,0 +1,87 @@
/*
* VM-Operator
* Copyright (C) 2023 Michael N. Lipp
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.VmPool;
import org.jgrapes.core.Channel;
import org.jgrapes.core.Components;
import org.jgrapes.core.Event;
/**
* Indicates a change in a pool configuration.
*/
public class VmPoolChanged extends Event<Void> {
private final VmPool vmPool;
private final boolean deleted;
/**
* Instantiates a new VM changed event.
*
* @param pool the pool
* @param deleted true, if the pool was deleted
*/
public VmPoolChanged(VmPool pool, boolean deleted) {
vmPool = pool;
this.deleted = deleted;
}
/**
* Instantiates a new VM changed event for an existing pool.
*
* @param pool the pool
*/
public VmPoolChanged(VmPool pool) {
this(pool, false);
}
/**
* Returns the VM pool.
*
* @return the vm pool
*/
public VmPool vmPool() {
return vmPool;
}
/**
* Pool has been deleted.
*
* @return true, if successful
*/
public boolean deleted() {
return deleted;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(30);
builder.append(Components.objectName(this))
.append(" [");
if (deleted) {
builder.append("Deleted: ");
}
builder.append(vmPool);
if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels()));
}
builder.append(']');
return builder.toString();
}
}

View file

@ -19,37 +19,41 @@
package org.jdrupes.vmoperator.manager.events; package org.jdrupes.vmoperator.manager.events;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.VmDefinitionModel; import org.jdrupes.vmoperator.common.VmDefinition;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Components; import org.jgrapes.core.Components;
import org.jgrapes.core.Event; import org.jgrapes.core.Event;
/** /**
* Indicates a change in a VM definition. Note that the definition * Indicates a change in a VM "resource". Note that the resource
* consists of the metadata (mostly immutable), the "spec" and the * combines the VM CR's metadata (mostly immutable), the VM CR's
* "status" parts. Consumers that are only interested in "spec" * "spec" part, the VM CR's "status" subresource and state information
* changes should check {@link #specChanged()} before processing * from the pod. Consumers that are only interested in "spec" changes
* the event any further. * should check {@link #specChanged()} before processing the event any
* further.
*/ */
@SuppressWarnings("PMD.DataClass") @SuppressWarnings("PMD.DataClass")
public class VmDefChanged extends Event<Void> { public class VmResourceChanged extends Event<Void> {
private final K8sObserver.ResponseType type; private final K8sObserver.ResponseType type;
private final VmDefinition vmDefinition;
private final boolean specChanged; private final boolean specChanged;
private final VmDefinitionModel vmDef; private final boolean podChanged;
/** /**
* Instantiates a new VM changed event. * Instantiates a new VM changed event.
* *
* @param type the type * @param type the type
* @param specChanged the spec part changed
* @param vmDefinition the VM definition * @param vmDefinition the VM definition
* @param specChanged the spec part changed
*/ */
public VmDefChanged(K8sObserver.ResponseType type, boolean specChanged, public VmResourceChanged(K8sObserver.ResponseType type,
VmDefinitionModel vmDefinition) { VmDefinition vmDefinition, boolean specChanged,
boolean podChanged) {
this.type = type; this.type = type;
this.vmDefinition = vmDefinition;
this.specChanged = specChanged; this.specChanged = specChanged;
this.vmDef = vmDefinition; this.podChanged = podChanged;
} }
/** /**
@ -61,6 +65,15 @@ public class VmDefChanged extends Event<Void> {
return type; return type;
} }
/**
* Return the VM definition.
*
* @return the VM definition
*/
public VmDefinition vmDefinition() {
return vmDefinition;
}
/** /**
* Indicates if the "spec" part changed. * Indicates if the "spec" part changed.
*/ */
@ -69,19 +82,17 @@ public class VmDefChanged extends Event<Void> {
} }
/** /**
* Returns the object. * Indicates if the pod status changed.
*
* @return the object.
*/ */
public VmDefinitionModel vmDefinition() { public boolean podChanged() {
return vmDef; return podChanged;
} }
@Override @Override
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
builder.append(Components.objectName(this)).append(" [") builder.append(Components.objectName(this)).append(" [")
.append(vmDef.getMetadata().getName()).append(' ').append(type); .append(vmDefinition.name()).append(' ').append(type);
if (channels() != null) { if (channels() != null) {
builder.append(", channels=").append(Channel.toString(channels())); builder.append(", channels=").append(Channel.toString(channels()));
} }

View file

@ -0,0 +1 @@
/logging.properties

View file

@ -13,15 +13,14 @@ dependencies {
implementation 'commons-cli:commons-cli:1.5.0' implementation 'commons-cli:commons-cli:1.5.0'
implementation 'org.jgrapes:org.jgrapes.core:[1.19.0,2)' implementation 'org.jgrapes:org.jgrapes.util:[1.38.1,2)'
implementation 'org.jgrapes:org.jgrapes.io:[2.7.0,3)' implementation 'org.jgrapes:org.jgrapes.io:[2.12.1,3)'
implementation 'org.jgrapes:org.jgrapes.http:[3.1.0,4)' implementation 'org.jgrapes:org.jgrapes.http:[3.5.0,4)'
implementation 'org.jgrapes:org.jgrapes.util:[1.34.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.base:[1.5.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.base:[2.3.0,3)'
implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.5.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.vuejs:[1.8.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.3.0,2)' implementation 'org.jgrapes:org.jgrapes.webconsole.rbac:[1.4.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.3.0,2)' implementation 'org.jgrapes:org.jgrapes.webconlet.oidclogin:[1.7.0,2)'
implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)' implementation 'org.jgrapes:org.jgrapes.webconlet.markdowndisplay:[1.2.0,2)'
runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)' runtimeOnly 'org.jgrapes:org.jgrapes.webconlet.sysinfo:[1.4.0,2)'
@ -32,8 +31,8 @@ dependencies {
runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)' runtimeOnly 'org.slf4j:slf4j-jdk14:[2.0.7,3)'
runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0' runtimeOnly 'org.apache.logging.log4j:log4j-to-jul:2.20.0'
runtimeOnly project(':org.jdrupes.vmoperator.vmconlet') runtimeOnly project(':org.jdrupes.vmoperator.vmmgmt')
runtimeOnly project(':org.jdrupes.vmoperator.vmviewer') runtimeOnly project(':org.jdrupes.vmoperator.vmaccess')
} }
application { application {
@ -45,75 +44,69 @@ application {
mainClass = 'org.jdrupes.vmoperator.manager.Manager' mainClass = 'org.jdrupes.vmoperator.manager.Manager'
} }
project.ext.gitBranch = grgit.branch.current.name.replace('/', '-')
def registry = "${project.rootProject.properties['docker.registry']}"
def rootVersion = rootProject.version
task buildImage(type: Exec) { task buildImage(type: Exec) {
dependsOn installDist dependsOn installDist
inputs.files 'src/org/jdrupes/vmoperator/manager/Containerfile' inputs.files 'src/org/jdrupes/vmoperator/manager/Containerfile'
commandLine 'podman', 'build', '--pull', commandLine 'podman', 'build', '--pull',
'-t', "${project.name}:${project.version}",\ '-t', "${project.name}:${project.gitBranch}",\
'-f', 'src/org/jdrupes/vmoperator/manager/Containerfile', '.' '-f', 'src/org/jdrupes/vmoperator/manager/Containerfile', '.'
} }
task tagLatestImage(type: Exec) {
dependsOn buildImage
enabled = !project.version.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \
|| project.rootProject.properties['docker.testRegistry'] \
&& project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'tag', "${project.name}:${project.version}",\
"${project.name}:latest"
}
task buildLatestImage {
dependsOn buildImage
dependsOn tagLatestImage
}
task pushImage(type: Exec) { task pushImage(type: Exec) {
dependsOn buildImage dependsOn buildImage
// Don't push without testing first
dependsOn test
commandLine 'podman', 'push', '--tls-verify=false', \ commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}:${project.version}", \ "${project.name}:${project.gitBranch}", \
"${project.rootProject.properties['docker.registry']}" \ "${registry}/${project.name}:${project.gitBranch}"
+ "/${project.name}:${project.version}"
} }
task pushLatestImage(type: Exec) { task tagWithVersion(type: Exec) {
dependsOn buildLatestImage dependsOn pushImage
enabled = !project.version.contains("SNAPSHOT") enabled = !rootVersion.contains("SNAPSHOT")
&& !project.version.contains("alpha") \
&& !project.version.contains("beta") \ commandLine 'podman', 'push', \
"${project.name}:${project.gitBranch}",\
"${registry}/${project.name}:${project.version}"
}
task tagAsLatest(type: Exec) {
dependsOn tagWithVersion
enabled = !rootVersion.contains("SNAPSHOT")
&& !rootVersion.contains("alpha") \
&& !rootVersion.contains("beta") \
|| project.rootProject.properties['docker.testRegistry'] \ || project.rootProject.properties['docker.testRegistry'] \
&& project.rootProject.properties['docker.registry'] \ && project.rootProject.properties['docker.registry'] \
== project.rootProject.properties['docker.testRegistry'] == project.rootProject.properties['docker.testRegistry']
commandLine 'podman', 'push', '--tls-verify=false', \ commandLine 'podman', 'push', \
"localhost/${project.name}:${project.version}", \ "${project.name}:${project.gitBranch}",\
"${project.rootProject.properties['docker.registry']}" \ "${registry}/${project.name}:latest"
+ "/${project.name}:latest" }
task publishImage {
dependsOn pushImage
dependsOn tagWithVersion
dependsOn tagAsLatest
} }
task pushForTest(type: Exec) { task pushForTest(type: Exec) {
dependsOn buildImage dependsOn buildImage
commandLine 'podman', 'push', '--tls-verify=false', \ commandLine 'podman', 'push', '--tls-verify=false', \
"localhost/${project.name}:${project.version}", \ "${project.name}:${project.gitBranch}", \
"${project.rootProject.properties['docker.registry']}" \ "${project.rootProject.properties['docker.testRegistry']}" \
+ "/${project.name}:test" + "/${project.name}:test"
} }
task pushImages {
// Don't push without testing first
dependsOn test
dependsOn pushImage
dependsOn pushLatestImage
}
test { test {
enabled = project.hasProperty("k8s.testCluster") enabled = project.hasProperty("k8s.testCluster")

View file

@ -1,3 +1,3 @@
<footer> <footer>
Copyright &copy; Michael N. Lipp 2023 Copyright &copy; Michael N. Lipp 2023, 2025
</footer> </footer>

View file

@ -1,6 +1,6 @@
# #
# VM-Operator # VM-Operator
# Copyright (C) 2023 Michael N. Lipp # Copyright (C) 2025 Michael N. Lipp
# #
# This program is free software; you can redistribute it and/or modify it # This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by # under the terms of the GNU General Public License as published by
@ -19,10 +19,7 @@
handlers=java.util.logging.ConsoleHandler, \ handlers=java.util.logging.ConsoleHandler, \
org.jgrapes.webconlet.logviewer.LogViewerHandler org.jgrapes.webconlet.logviewer.LogViewerHandler
org.jgrapes.level=FINE org.jdrupes.vmoperator.level=FINE
org.jgrapes.core.handlerTracking.level=FINER
org.jdrupes.vmoperator.manager.level=FINE
java.util.logging.ConsoleHandler.level=ALL java.util.logging.ConsoleHandler.level=ALL
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter

View file

@ -1,19 +1,19 @@
apiVersion: v1 apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
namespace: ${ cr.metadata.namespace.asString } namespace: ${ cr.namespace() }
name: ${ cr.metadata.name.asString } name: ${ cr.name() }
labels: labels:
app.kubernetes.io/name: ${ constants.APP_NAME } app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/instance: ${ cr.name() }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
annotations: annotations:
vmoperator.jdrupes.org/version: ${ managerVersion } vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences: ownerReferences:
- apiVersion: ${ cr.apiVersion.asString } - apiVersion: ${ cr.apiVersion() }
kind: ${ constants.VM_OP_KIND_VM } kind: ${ constants.Crd.KIND_VM }
name: ${ cr.metadata.name.asString } name: ${ cr.name() }
uid: ${ cr.metadata.uid.asString } uid: ${ cr.metadata().getUid() }
controller: false controller: false
data: data:
@ -21,115 +21,118 @@ data:
"/Runner": "/Runner":
# The directory used to store data files. Defaults to (depending on # The directory used to store data files. Defaults to (depending on
# values available): # values available):
# * $XDG_DATA_HOME/vmrunner/${ cr.metadata.name.asString } # * $XDG_DATA_HOME/vmrunner/${ cr.name() }
# * $HOME/.local/share/vmrunner/${ cr.metadata.name.asString } # * $HOME/.local/share/vmrunner/${ cr.name() }
# * ./${ cr.metadata.name.asString } # * ./${ cr.name() }
dataDir: /var/local/vm-data dataDir: /var/local/vm-data
# The directory used to store runtime files. Defaults to (depending on # The directory used to store runtime files. Defaults to (depending on
# values available): # values available):
# * $XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString } # * $XDG_RUNTIME_DIR/vmrunner/${ cr.name() }
# * /tmp/$USER/vmrunner/${ cr.metadata.name.asString } # * /tmp/$USER/vmrunner/${ cr.name() }
# * /tmp/vmrunner/${ cr.metadata.name.asString } # * /tmp/vmrunner/${ cr.name() }
# runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.metadata.name.asString }" # runtimeDir: "$XDG_RUNTIME_DIR/vmrunner/${ cr.name() }"
<#assign spec = cr.spec() />
# The template to use. Resolved relative to /usr/share/vmrunner/templates. # The template to use. Resolved relative to /usr/share/vmrunner/templates.
# template: "Standard-VM-latest.ftl.yaml" # template: "Standard-VM-latest.ftl.yaml"
<#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.source?? > <#if spec.runnerTemplate?? && spec.runnerTemplate.source?? >
template: ${ cr.spec.runnerTemplate.source.asString } template: ${ spec.runnerTemplate.source }
</#if> </#if>
# The template is copied to the data diretory when the VM starts for # The template is copied to the data diretory when the VM starts for
# the first time. Subsequent starts use the copy unless this option is set. # the first time. Subsequent starts use the copy unless this option is set.
<#if cr.spec.runnerTemplate?? && cr.spec.runnerTemplate.update?? > <#if spec.runnerTemplate?? && spec.runnerTemplate.update?? >
updateTemplate: ${ cr.spec.runnerTemplate.update.asBoolean?c } updateTemplate: ${ spec.runnerTemplate.update?c }
</#if> </#if>
# Whether a shutdown initiated by the guest stops the pod deployment # Whether a shutdown initiated by the guest stops the pod deployment
guestShutdownStops: ${ cr.spec.guestShutdownStops!false?c } guestShutdownStops: ${ (spec.guestShutdownStops!false)?c }
# When incremented, the VM is reset. The value has no default value,
# i.e. if you start the VM without a value for this property, and
# decide to trigger a reset later, you have to first set the value
# and then inrement it.
resetCounter: ${ cr.extra().resetCount()?c }
# Forward the cloud-init data if provided # Forward the cloud-init data if provided
<#if cr.spec.cloudInit??> <#if spec.cloudInit??>
cloudInit: cloudInit:
<#if cr.spec.cloudInit.metaData??> metaData: ${ toJson(adjustCloudInitMeta(spec.cloudInit.metaData!{}, cr.metadata())) }
metaData: ${ cr.spec.cloudInit.metaData.toString() } <#if spec.cloudInit.userData??>
<#else> userData: ${ toJson(spec.cloudInit.userData) }
metaData: {}
</#if>
<#if cr.spec.cloudInit.userData??>
userData: ${ cr.spec.cloudInit.userData.toString() }
<#else> <#else>
userData: {} userData: {}
</#if> </#if>
<#if cr.spec.cloudInit.networkConfig??> <#if spec.cloudInit.networkConfig??>
networkConfig: ${ cr.spec.cloudInit.networkConfig.toString() } networkConfig: ${ toJson(spec.cloudInit.networkConfig) }
</#if> </#if>
</#if> </#if>
# Define the VM (required) # Define the VM (required)
vm: vm:
# The VM's name (required) # The VM's name (required)
name: ${ cr.metadata.name.asString } name: ${ cr.name() }
# The machine's uuid. If none is specified, a uuid is generated # The machine's uuid. If none is specified, a uuid is generated
# and stored in the data directory. If the uuid is important # and stored in the data directory. If the uuid is important
# (e.g. because licenses depend on it) it is recommaned to specify # (e.g. because licenses depend on it) it is recommaned to specify
# it here explicitly or to carefully backup the data directory. # it here explicitly or to carefully backup the data directory.
# uuid: "generated uuid" # uuid: "generated uuid"
<#if cr.spec.vm.machineUuid??> <#if spec.vm.machineUuid??>
uuid: "${ cr.spec.vm.machineUuid.asString }" uuid: "${ spec.vm.machineUuid }"
</#if> </#if>
# Whether to provide a software TPM (defaults to false) # Whether to provide a software TPM (defaults to false)
# useTpm: false # useTpm: false
useTpm: ${ cr.spec.vm.useTpm.asBoolean?c } useTpm: ${ spec.vm.useTpm?c }
# How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml): # How to boot (see https://github.com/mnlipp/VM-Operator/blob/main/org.jdrupes.vmoperator.runner.qemu/resources/org/jdrupes/vmoperator/runner/qemu/defaults.yaml):
# * bios # * bios
# * uefi[-4m] # * uefi[-4m]
# * secure[-4m] # * secure[-4m]
firmware: ${ cr.spec.vm.firmware.asString } firmware: ${ spec.vm.firmware }
# Whether to show a boot menu. # Whether to show a boot menu.
# bootMenu: false # bootMenu: false
bootMenu: ${ cr.spec.vm.bootMenu.asBoolean?c } bootMenu: ${ spec.vm.bootMenu?c }
# When terminating, a graceful powerdown is attempted. If it # When terminating, a graceful powerdown is attempted. If it
# doesn't succeed within the given timeout (seconds) SIGTERM # doesn't succeed within the given timeout (seconds) SIGTERM
# is sent to Qemu. # is sent to Qemu.
# powerdownTimeout: 900 # powerdownTimeout: 900
powerdownTimeout: ${ cr.spec.vm.powerdownTimeout.asLong?c } powerdownTimeout: ${ spec.vm.powerdownTimeout?c }
# CPU settings # CPU settings
cpuModel: ${ cr.spec.vm.cpuModel.asString } cpuModel: ${ spec.vm.cpuModel }
# Setting maximumCpus to 1 omits the "-smp" options. The defaults (0) # Setting maximumCpus to 1 omits the "-smp" options. The defaults (0)
# cause the corresponding property to be omitted from the "-smp" option. # cause the corresponding property to be omitted from the "-smp" option.
# If currentCpus is greater than maximumCpus, the latter is adjusted. # If currentCpus is greater than maximumCpus, the latter is adjusted.
<#if cr.spec.vm.maximumCpus?? > <#if spec.vm.maximumCpus?? >
maximumCpus: ${ parseQuantity(cr.spec.vm.maximumCpus.asString)?c } maximumCpus: ${ parseQuantity(spec.vm.maximumCpus)?c }
</#if> </#if>
<#if cr.spec.vm.cpuTopology?? > <#if spec.vm.cpuTopology?? >
sockets: ${ cr.spec.vm.cpuTopology.sockets.asInt?c } sockets: ${ spec.vm.cpuTopology.sockets?c }
diesPerSocket: ${ cr.spec.vm.cpuTopology.diesPerSocket.asInt?c } diesPerSocket: ${ spec.vm.cpuTopology.diesPerSocket?c }
coresPerDie: ${ cr.spec.vm.cpuTopology.coresPerDie.asInt?c } coresPerDie: ${ spec.vm.cpuTopology.coresPerDie?c }
threadsPerCore: ${ cr.spec.vm.cpuTopology.threadsPerCore.asInt?c } threadsPerCore: ${ spec.vm.cpuTopology.threadsPerCore?c }
</#if> </#if>
<#if cr.spec.vm.currentCpus?? > <#if spec.vm.currentCpus?? >
currentCpus: ${ parseQuantity(cr.spec.vm.currentCpus.asString)?c } currentCpus: ${ parseQuantity(spec.vm.currentCpus)?c }
</#if> </#if>
# RAM settings # RAM settings
# Maximum defaults to 1G # Maximum defaults to 1G
maximumRam: "${ formatMemory(parseQuantity(cr.spec.vm.maximumRam.asString)) }" maximumRam: "${ formatMemory(parseQuantity(spec.vm.maximumRam)) }"
<#if cr.spec.vm.currentRam?? > <#if spec.vm.currentRam?? >
currentRam: "${ formatMemory(parseQuantity(cr.spec.vm.currentRam.asString)) }" currentRam: "${ formatMemory(parseQuantity(spec.vm.currentRam)) }"
</#if> </#if>
# RTC settings. # RTC settings.
# rtcBase: utc # rtcBase: utc
# rtcClock: rt # rtcClock: rt
rtcBase: ${ cr.spec.vm.rtcBase.asString } rtcBase: ${ spec.vm.rtcBase }
rtcClock: ${ cr.spec.vm.rtcClock.asString } rtcClock: ${ spec.vm.rtcClock }
# Network settings # Network settings
# Supported types are "tap" and "user" (for debugging). Type "user" # Supported types are "tap" and "user" (for debugging). Type "user"
@ -141,19 +144,19 @@ data:
# mac: (undefined) # mac: (undefined)
network: network:
<#assign nwCounter = 0/> <#assign nwCounter = 0/>
<#list cr.spec.vm.networks.asList() as itf> <#list spec.vm.networks as itf>
<#if itf.tap??> <#if itf.tap??>
- type: tap - type: tap
device: ${ itf.tap.device.asString } device: ${ itf.tap.device }
bridge: ${ itf.tap.bridge.asString } bridge: ${ itf.tap.bridge }
<#if itf.tap.mac??> <#if itf.tap.mac??>
mac: "${ itf.tap.mac.asString }" mac: "${ itf.tap.mac }"
</#if> </#if>
<#elseif itf.user??> <#elseif itf.user??>
- type: user - type: user
device: ${ itf.user.device.asString } device: ${ itf.user.device }
<#if itf.user.net??> <#if itf.user.net??>
net: "${ itf.user.net.asString }" net: "${ itf.user.net }"
</#if> </#if>
</#if> </#if>
<#assign nwCounter += 1/> <#assign nwCounter += 1/>
@ -169,11 +172,11 @@ data:
# file: (undefined) # file: (undefined)
drives: drives:
<#assign drvCounter = 0/> <#assign drvCounter = 0/>
<#list cr.spec.vm.disks.asList() as disk> <#list spec.vm.disks as disk>
<#if disk.volumeClaimTemplate?? <#if disk.volumeClaimTemplate??
&& disk.volumeClaimTemplate.metadata?? && disk.volumeClaimTemplate.metadata??
&& disk.volumeClaimTemplate.metadata.name??> && disk.volumeClaimTemplate.metadata.name??>
<#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk"> <#assign diskName = disk.volumeClaimTemplate.metadata.name + "-disk">
<#else> <#else>
<#assign diskName = "disk-" + drvCounter> <#assign diskName = "disk-" + drvCounter>
</#if> </#if>
@ -181,30 +184,36 @@ data:
- type: raw - type: raw
resource: /dev/${ diskName } resource: /dev/${ diskName }
<#if disk.bootindex??> <#if disk.bootindex??>
bootindex: ${ disk.bootindex.asInt?c } bootindex: ${ disk.bootindex?c }
</#if> </#if>
<#assign drvCounter = drvCounter + 1/> <#assign drvCounter = drvCounter + 1/>
</#if> </#if>
<#if disk.cdrom??> <#if disk.cdrom??>
- type: ide-cd - type: ide-cd
file: "${ disk.cdrom.image.asString }" file: "${ imageLocation(disk.cdrom.image) }"
<#if disk.bootindex??> <#if disk.bootindex??>
bootindex: ${ disk.bootindex.asInt?c } bootindex: ${ disk.bootindex?c }
</#if> </#if>
</#if> </#if>
</#list> </#list>
display: display:
<#if cr.spec.vm.display.spice??> <#if spec.vm.display.outputs?? >
outputs: ${ spec.vm.display.outputs?c }
</#if>
<#if loginRequestedFor?? >
loggedInUser: "${ loginRequestedFor }"
</#if>
<#if spec.vm.display.spice??>
spice: spice:
port: ${ cr.spec.vm.display.spice.port.asInt?c } port: ${ spec.vm.display.spice.port?c }
<#if cr.spec.vm.display.spice.ticket??> <#if spec.vm.display.spice.ticket??>
ticket: "${ cr.spec.vm.display.spice.ticket.asString }" ticket: "${ spec.vm.display.spice.ticket }"
</#if> </#if>
<#if cr.spec.vm.display.spice.streamingVideo??> <#if spec.vm.display.spice.streamingVideo??>
streaming-video: "${ cr.spec.vm.display.spice.streamingVideo.asString }" streaming-video: "${ spec.vm.display.spice.streamingVideo }"
</#if> </#if>
usbRedirects: ${ cr.spec.vm.display.spice.usbRedirects.asInt?c } usbRedirects: ${ spec.vm.display.spice.usbRedirects?c }
</#if> </#if>
logging.properties: | logging.properties: |

View file

@ -0,0 +1,18 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
namespace: ${ cr.namespace() }
name: ${ runnerDataPvcName }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.name() }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
spec:
accessModes:
- ReadWriteOnce
<#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??>
storageClassName: ${ reconciler.runnerDataPvc.storageClassName }
</#if>
resources:
requests:
storage: 1Mi

View file

@ -0,0 +1,16 @@
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
namespace: ${ cr.namespace() }
name: ${ disk.generatedPvcName }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.name() }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
<#if disk.volumeClaimTemplate.metadata??
&& disk.volumeClaimTemplate.metadata.annotations??>
annotations:
${ toJson(disk.volumeClaimTemplate.metadata.annotations) }
</#if>
spec:
${ toJson(disk.volumeClaimTemplate.spec) }

View file

@ -1,26 +1,26 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
namespace: ${ cr.metadata.namespace.asString } namespace: ${ cr.namespace() }
name: ${ cr.metadata.name.asString } name: ${ cr.name() }
labels: labels:
app.kubernetes.io/name: ${ constants.APP_NAME } app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/instance: ${ cr.name() }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME } app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
annotations: annotations:
vmoperator.jdrupes.org/version: ${ managerVersion } vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences: ownerReferences:
- apiVersion: ${ cr.apiVersion.asString } - apiVersion: ${ cr.apiVersion() }
kind: ${ constants.VM_OP_KIND_VM } kind: ${ constants.Crd.KIND_VM }
name: ${ cr.metadata.name.asString } name: ${ cr.name() }
uid: ${ cr.metadata.uid.asString } uid: ${ cr.metadata().getUid() }
controller: false controller: false
spec: spec:
type: LoadBalancer type: LoadBalancer
ports: ports:
- name: spice - name: spice
port: ${ cr.spec.vm.display.spice.port.asInt?c } port: ${ cr.spec().vm.display.spice.port?c }
selector: selector:
app.kubernetes.io/name: ${ constants.APP_NAME } app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString } app.kubernetes.io/instance: ${ cr.name() }

View file

@ -0,0 +1,135 @@
kind: Pod
apiVersion: v1
metadata:
namespace: ${ cr.namespace() }
name: ${ cr.name() }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.name() }
app.kubernetes.io/component: ${ constants.APP_NAME }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
annotations:
# Triggers update of config map mounted in pod
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
vmrunner.jdrupes.org/cmVersion: "${ configMapResourceVersion }"
vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences:
- apiVersion: ${ cr.apiVersion() }
kind: ${ constants.Crd.KIND_VM }
name: ${ cr.name() }
uid: ${ cr.metadata().getUid() }
blockOwnerDeletion: true
controller: false
<#assign spec = cr.spec() />
spec:
containers:
- name: ${ cr.name() }
<#assign image = spec.image>
<#if image.source??>
image: ${ image.source }
<#else>
image: ${ image.repository }/${ image.path }<#if image.version??>:${ image.version }</#if>
</#if>
<#if image.pullPolicy??>
imagePullPolicy: ${ image.pullPolicy }
</#if>
<#if spec.vm.display.spice??>
ports:
<#if spec.vm.display.spice??>
- name: spice
containerPort: ${ spec.vm.display.spice.port?c }
protocol: TCP
</#if>
</#if>
volumeMounts:
# Not needed because pod is priviledged:
# - mountPath: /dev/kvm
# name: dev-kvm
# - mountPath: /dev/net/tun
# name: dev-tun
# - mountPath: /sys/fs/cgroup
# name: cgroup
- name: config
mountPath: /etc/opt/vmrunner
- name: runner-data
mountPath: /var/local/vm-data
- name: vmop-image-repository
mountPath: ${ constants.IMAGE_REPO_PATH }
volumeDevices:
<#list spec.vm.disks as disk>
<#if disk.volumeClaimTemplate??>
- name: ${ disk.generatedDiskName }
devicePath: /dev/${ disk.generatedDiskName }
</#if>
</#list>
securityContext:
privileged: true
<#if spec.resources??>
resources: ${ toJson(spec.resources) }
<#else>
<#if spec.vm.currentCpus?? || spec.vm.currentRam?? >
resources:
requests:
<#if spec.vm.currentCpus?? >
<#assign factor = 2.0 />
<#if reconciler.cpuOvercommit??>
<#assign factor = reconciler.cpuOvercommit * 1.0 />
</#if>
cpu: ${ (parseQuantity(spec.vm.currentCpus) / factor)?c }
</#if>
<#if spec.vm.currentRam?? >
<#assign factor = 1.25 />
<#if reconciler.ramOvercommit??>
<#assign factor = reconciler.ramOvercommit * 1.0 />
</#if>
memory: ${ (parseQuantity(spec.vm.currentRam) / factor)?floor?c }
</#if>
</#if>
</#if>
volumes:
# Not needed because pod is priviledged:
# - name: dev-kvm
# hostPath:
# path: /dev/kvm
# type: CharDevice
# - hostPath:
# path: /dev/net/tun
# type: CharDevice
# name: dev-tun
# - name: cgroup
# hostPath:
# path: /sys/fs/cgroup
- name: config
projected:
sources:
- configMap:
name: ${ cr.name() }
<#if displaySecret??>
- secret:
name: ${ displaySecret }
</#if>
- name: vmop-image-repository
persistentVolumeClaim:
claimName: vmop-image-repository
- name: runner-data
persistentVolumeClaim:
claimName: ${ runnerDataPvcName }
<#list spec.vm.disks as disk>
<#if disk.volumeClaimTemplate??>
- name: ${ disk.generatedDiskName }
persistentVolumeClaim:
claimName: ${ disk.generatedPvcName }
</#if>
</#list>
hostNetwork: true
terminationGracePeriodSeconds: ${ (spec.vm.powerdownTimeout + 5)?c }
<#if spec.nodeName??>
nodeName: ${ spec.nodeName }
</#if>
<#if spec.nodeSelector??>
nodeSelector: ${ toJson(spec.nodeSelector) }
</#if>
<#if spec.affinity??>
affinity: ${ toJson(spec.affinity) }
</#if>
serviceAccountName: vm-runner

View file

@ -1,194 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
namespace: ${ cr.metadata.namespace.asString }
name: ${ cr.metadata.name.asString }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
annotations:
vmoperator.jdrupes.org/version: ${ managerVersion }
ownerReferences:
- apiVersion: ${ cr.apiVersion.asString }
kind: ${ constants.VM_OP_KIND_VM }
name: ${ cr.metadata.name.asString }
uid: ${ cr.metadata.uid.asString }
blockOwnerDeletion: true
controller: false
spec:
selector:
matchLabels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
replicas: ${ (cr.spec.vm.state.asString == "Running")?then(1, 0) }
updateStrategy:
type: OnDelete
template:
metadata:
namespace: ${ cr.metadata.namespace.asString }
name: ${ cr.metadata.name.asString }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
app.kubernetes.io/component: ${ constants.APP_NAME }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
annotations:
# Triggers update of config map mounted in pod
# See https://ahmet.im/blog/kubernetes-secret-volumes-delay/
vmrunner.jdrupes.org/cmVersion: "${ cm.metadata.resourceVersion.asString }"
vmoperator.jdrupes.org/version: ${ managerVersion }
spec:
containers:
- name: ${ cr.metadata.name.asString }
<#assign image = cr.spec.image>
<#if image.source??>
image: ${ image.source.asString }
<#else>
image: ${ image.repository.asString }/${ image.path.asString }<#if image.version??>:${ image.version.asString }</#if>
</#if>
<#if image.pullPolicy??>
imagePullPolicy: ${ image.pullPolicy.asString }
</#if>
<#if cr.spec.vm.display.spice??>
ports:
<#if cr.spec.vm.display.spice??>
- name: spice
containerPort: ${ cr.spec.vm.display.spice.port.asInt?c }
protocol: TCP
</#if>
</#if>
volumeMounts:
# Not needed because pod is priviledged:
# - mountPath: /dev/kvm
# name: dev-kvm
# - mountPath: /dev/net/tun
# name: dev-tun
# - mountPath: /sys/fs/cgroup
# name: cgroup
- name: config
mountPath: /etc/opt/vmrunner
- name: runner-data
mountPath: /var/local/vm-data
- name: vmop-image-repository
mountPath: ${ constants.IMAGE_REPO_PATH }
volumeDevices:
<#assign diskCounter = 0/>
<#list cr.spec.vm.disks.asList() as disk>
<#if disk.volumeClaimTemplate??>
<#if disk.volumeClaimTemplate.metadata??
&& disk.volumeClaimTemplate.metadata.name??>
<#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk">
<#else>
<#assign diskName = "disk-" + diskCounter>
</#if>
- name: ${ diskName }
devicePath: /dev/${ diskName }
<#assign diskCounter = diskCounter + 1/>
</#if>
</#list>
securityContext:
privileged: true
<#if cr.spec.resources??>
resources: ${ cr.spec.resources.toString() }
<#else>
<#if cr.spec.vm.currentCpus?? || cr.spec.vm.currentRam?? >
resources:
requests:
<#if cr.spec.vm.currentCpus?? >
<#assign factor = 2.0 />
<#if reconciler.cpuOvercommit??>
<#assign factor = reconciler.cpuOvercommit * 1.0 />
</#if>
cpu: ${ (parseQuantity(cr.spec.vm.currentCpus.asString) / factor)?c }
</#if>
<#if cr.spec.vm.currentRam?? >
<#assign factor = 1.25 />
<#if reconciler.ramOvercommit??>
<#assign factor = reconciler.ramOvercommit * 1.0 />
</#if>
memory: ${ (parseQuantity(cr.spec.vm.currentRam.asString) / factor)?floor?c }
</#if>
</#if>
</#if>
volumes:
# Not needed because pod is priviledged:
# - name: dev-kvm
# hostPath:
# path: /dev/kvm
# type: CharDevice
# - hostPath:
# path: /dev/net/tun
# type: CharDevice
# name: dev-tun
# - name: cgroup
# hostPath:
# path: /sys/fs/cgroup
- name: config
projected:
sources:
- configMap:
name: ${ cr.metadata.name.asString }
<#if displaySecret??>
- secret:
name: ${ displaySecret }
</#if>
- name: vmop-image-repository
persistentVolumeClaim:
claimName: vmop-image-repository
hostNetwork: true
terminationGracePeriodSeconds: ${ (cr.spec.vm.powerdownTimeout.asInt + 5)?c }
<#if cr.spec.nodeName??>
nodeName: ${ cr.spec.nodeName.asString }
</#if>
<#if cr.spec.nodeSelector??>
nodeSelector: ${ cr.spec.nodeSelector.toString() }
</#if>
<#if cr.spec.affinity??>
affinity: ${ cr.spec.affinity.toString() }
</#if>
serviceAccountName: vm-runner
volumeClaimTemplates:
- metadata:
namespace: ${ cr.metadata.namespace.asString }
name: runner-data
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
spec:
accessModes:
- ReadWriteOnce
<#if reconciler.runnerDataPvc?? && reconciler.runnerDataPvc.storageClassName??>
storageClassName: ${ reconciler.runnerDataPvc.storageClassName }
</#if>
resources:
requests:
storage: 1Mi
<#assign diskCounter = 0/>
<#list cr.spec.vm.disks.asList() as disk>
<#if disk.volumeClaimTemplate??>
<#if disk.volumeClaimTemplate.metadata??
&& disk.volumeClaimTemplate.metadata.name??>
<#assign diskName = disk.volumeClaimTemplate.metadata.name.asString + "-disk">
<#else>
<#assign diskName = "disk-" + diskCounter>
</#if>
- metadata:
namespace: ${ cr.metadata.namespace.asString }
name: ${ diskName }
labels:
app.kubernetes.io/name: ${ constants.APP_NAME }
app.kubernetes.io/instance: ${ cr.metadata.name.asString }
app.kubernetes.io/managed-by: ${ constants.VM_OP_NAME }
<#if disk.volumeClaimTemplate.metadata??
&& disk.volumeClaimTemplate.metadata.annotations??>
annotations:
${ disk.volumeClaimTemplate.metadata.annotations.toString() }
</#if>
spec:
${ disk.volumeClaimTemplate.spec.toString() }
<#assign diskCounter = diskCounter + 1/>
</#if>
</#list>

View file

@ -27,14 +27,11 @@ import io.kubernetes.client.util.generic.options.ListOptions;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import org.jdrupes.vmoperator.common.K8sClient; import org.jdrupes.vmoperator.common.K8sClient;
import org.jdrupes.vmoperator.common.K8sObserver; import org.jdrupes.vmoperator.common.K8sObserver;
import org.jdrupes.vmoperator.common.K8sObserver.ResponseType;
import org.jdrupes.vmoperator.manager.events.ChannelManager;
import org.jdrupes.vmoperator.manager.events.Exit; import org.jdrupes.vmoperator.manager.events.Exit;
import org.jgrapes.core.Channel; import org.jgrapes.core.Channel;
import org.jgrapes.core.Component; import org.jgrapes.core.Component;
@ -45,12 +42,15 @@ import org.jgrapes.core.events.Stop;
import org.jgrapes.util.events.ConfigurationUpdate; import org.jgrapes.util.events.ConfigurationUpdate;
/** /**
* A base class for monitoring VM related resources. * A base class for monitoring VM related resources. When started,
* it creates observers for all versions of the the {@link APIResource}
* configured by {@link #context(APIResource)}. The APIResource is not
* passed to the constructor because in some cases it has to be
* evaluated lazily.
* *
* @param <O> the object type for the context * @param <O> the object type for the context
* @param <L> the object list type for the context * @param <L> the object list type for the context
*/ */
@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis" })
public abstract class AbstractMonitor<O extends KubernetesObject, public abstract class AbstractMonitor<O extends KubernetesObject,
L extends KubernetesListObject, C extends Channel> extends Component { L extends KubernetesListObject, C extends Channel> extends Component {
@ -61,16 +61,17 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
private String namespace; private String namespace;
private ListOptions options = new ListOptions(); private ListOptions options = new ListOptions();
private final AtomicInteger observerCounter = new AtomicInteger(0); private final AtomicInteger observerCounter = new AtomicInteger(0);
private ChannelManager<String, C, ?> channelManager;
private boolean channelManagerMaster;
/** /**
* Initializes the instance. * Initializes the instance.
* *
* @param componentChannel the component channel * @param componentChannel the component channel
* @param objectClass the class of the Kubernetes object to watch
* @param objectListClass the class of the list of Kubernetes objects
* to watch
*/ */
protected AbstractMonitor(Channel componentChannel, Class<O> objectClass, protected AbstractMonitor(Channel componentChannel,
Class<L> objectListClass) { Class<O> objectClass, Class<L> objectListClass) {
super(componentChannel); super(componentChannel);
this.objectClass = objectClass; this.objectClass = objectClass;
this.objectListClass = objectListClass; this.objectListClass = objectListClass;
@ -156,27 +157,6 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
return this; return this;
} }
/**
* Returns the channel manager.
*
* @return the context
*/
public ChannelManager<String, C, ?> channelManager() {
return channelManager;
}
/**
* Sets the channel manager.
*
* @param channelManager the channel manager
* @return the abstract monitor
*/
public AbstractMonitor<O, L, C>
channelManager(ChannelManager<String, C, ?> channelManager) {
this.channelManager = channelManager;
return this;
}
/** /**
* Looks for a key "namespace" in the configuration and, if found, * Looks for a key "namespace" in the configuration and, if found,
* sets the namespace to its value. * sets the namespace to its value.
@ -194,13 +174,12 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
} }
/** /**
* Handle the start event. Configures the namespace invokes * Handle the start event. Configures the namespace, invokes
* {@link #prepareMonitoring()} and starts the observers. * {@link #prepareMonitoring()} and starts the observers.
* *
* @param event the event * @param event the event
*/ */
@Handler(priority = 10) @Handler(priority = 10)
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
public void onStart(Start event) { public void onStart(Start event) {
try { try {
// Get namespace // Get namespace
@ -218,8 +197,6 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
assert client != null; assert client != null;
assert context != null; assert context != null;
assert namespace != null; assert namespace != null;
logger.fine(() -> "Observing " + K8s.toString(context)
+ " objects in " + namespace);
// Monitor all versions // Monitor all versions
for (var version : context.getVersions()) { for (var version : context.getVersions()) {
@ -238,13 +215,7 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
observerCounter.incrementAndGet(); observerCounter.incrementAndGet();
new K8sObserver<>(objectClass, objectListClass, client, new K8sObserver<>(objectClass, objectListClass, client,
K8s.preferred(context, version), namespace, options) K8s.preferred(context, version), namespace, options)
.handler((c, r) -> { .handler(this::handleChange).onTerminated((o, t) -> {
handleChange(c, r);
if (ResponseType.valueOf(r.type) == ResponseType.DELETED
&& channelManagerMaster) {
channelManager.remove(r.object.getMetadata().getName());
}
}).onTerminated((o, t) -> {
if (observerCounter.decrementAndGet() == 0) { if (observerCounter.decrementAndGet() == 0) {
unregisterAsGenerator(); unregisterAsGenerator();
} }
@ -257,7 +228,8 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
/** /**
* Invoked by {@link #onStart(Start)} after the namespace has * Invoked by {@link #onStart(Start)} after the namespace has
* been configured and before starting the observer. * been configured and before starting the observer. This is
* the last opportunity to invoke {@link #context(APIResource)}.
* *
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws ApiException the api exception * @throws ApiException the api exception
@ -268,20 +240,12 @@ public abstract class AbstractMonitor<O extends KubernetesObject,
} }
/** /**
* Handle an observed change. * Handle an observed change. The method is invoked by the observer
* thread(s). It is the responsibility of the implementing class to
* fire derived events on the appropriate event pipeline.
* *
* @param client the client * @param client the client
* @param change the change * @param change the change
*/ */
protected abstract void handleChange(K8sClient client, Response<O> change); protected abstract void handleChange(K8sClient client, Response<O> change);
/**
* Returns the {@link Channel} for the given name.
*
* @param name the name
* @return the channel used for events related to the specified object
*/
protected Optional<C> channel(String name) {
return channelManager.getChannel(name);
}
} }

View file

@ -18,11 +18,18 @@
package org.jdrupes.vmoperator.manager; package org.jdrupes.vmoperator.manager;
import com.google.gson.JsonObject;
import freemarker.template.AdapterTemplateModel;
import freemarker.template.Configuration; import freemarker.template.Configuration;
import freemarker.template.TemplateException; import freemarker.template.TemplateException;
import freemarker.template.TemplateMethodModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.utility.DeepUnwrap;
import io.kubernetes.client.custom.V1Patch; import io.kubernetes.client.custom.V1Patch;
import io.kubernetes.client.openapi.ApiClient; import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException; import io.kubernetes.client.openapi.ApiException;
import io.kubernetes.client.openapi.models.V1ObjectMeta;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi;
import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
import io.kubernetes.client.util.generic.dynamic.Dynamics; import io.kubernetes.client.util.generic.dynamic.Dynamics;
@ -30,13 +37,18 @@ import io.kubernetes.client.util.generic.options.ListOptions;
import io.kubernetes.client.util.generic.options.PatchOptions; import io.kubernetes.client.util.generic.options.PatchOptions;
import java.io.IOException; import java.io.IOException;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.logging.Logger; import java.util.logging.Logger;
import org.jdrupes.vmoperator.common.K8s; import org.jdrupes.vmoperator.common.K8s;
import static org.jdrupes.vmoperator.manager.Constants.APP_NAME; import static org.jdrupes.vmoperator.manager.Constants.APP_NAME;
import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME; import static org.jdrupes.vmoperator.manager.Constants.VM_OP_NAME;
import org.jdrupes.vmoperator.manager.events.VmChannel; import org.jdrupes.vmoperator.manager.events.VmChannel;
import org.jdrupes.vmoperator.manager.events.VmDefChanged; import org.jdrupes.vmoperator.util.DataPath;
import org.jdrupes.vmoperator.util.GsonPtr;
import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor; import org.yaml.snakeyaml.constructor.SafeConstructor;
@ -44,7 +56,6 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Delegee for reconciling the config map * Delegee for reconciling the config map
*/ */
@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
/* default */ class ConfigMapReconciler { /* default */ class ConfigMapReconciler {
protected final Logger logger = Logger.getLogger(getClass().getName()); protected final Logger logger = Logger.getLogger(getClass().getName());
@ -62,34 +73,72 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
/** /**
* Reconcile. * Reconcile.
* *
* @param event the event
* @param model the model * @param model the model
* @param channel the channel * @param channel the channel
* @return the dynamic kubernetes object * @param modelChanged the model has changed
* @throws IOException Signals that an I/O exception has occurred. * @throws IOException Signals that an I/O exception has occurred.
* @throws TemplateException the template exception * @throws TemplateException the template exception
* @throws ApiException the api exception * @throws ApiException the API exception
*/ */
public DynamicKubernetesObject reconcile(VmDefChanged event, public void reconcile(Map<String, Object> model, VmChannel channel,
Map<String, Object> model, VmChannel channel) boolean modelChanged)
throws IOException, TemplateException, ApiException { throws IOException, TemplateException, ApiException {
// Get API // Check if an update is needed
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1", var prevData = channel.associated(PrevData.class)
"configmaps", channel.client()); .orElseGet(() -> new PrevData(null, new HashMap<>()));
Object newInputs = model.get("loginRequestedFor");
if (!modelChanged && Objects.equals(prevData.inputs, newInputs)) {
// Make added data available in new model
model.putAll(prevData.added);
return;
}
prevData = new PrevData(newInputs, prevData.added);
channel.setAssociated(PrevData.class, prevData);
// Combine template and data and parse result // Combine template and data and parse result
logger.fine(() -> "Create/update configmap "
+ DataPath.<String> get(model, "cr", "name").orElse("unknown"));
model.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
prevData.added.put("adjustCloudInitMeta", adjustCloudInitMetaModel);
var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml"); var fmTemplate = fmConfig.getTemplate("runnerConfig.ftl.yaml");
StringWriter out = new StringWriter(); StringWriter out = new StringWriter();
fmTemplate.process(model, out); fmTemplate.process(model, out);
// Avoid Yaml.load due to // Avoid Yaml.load due to
// https://github.com/kubernetes-client/java/issues/2741 // https://github.com/kubernetes-client/java/issues/2741
var mapDef = Dynamics.newFromYaml( var newCm = Dynamics.newFromYaml(
new Yaml(new SafeConstructor(new LoaderOptions())), out.toString()); new Yaml(new SafeConstructor(new LoaderOptions())), out.toString());
// Maybe override logging.properties from reconciler configuration.
DataPath.<String> get(model, "reconciler", "loggingProperties")
.ifPresent(props -> {
GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data")
.get().addProperty("logging.properties", props);
});
// Maybe override logging.properties from VM definition.
DataPath.<String> get(model, "cr", "spec", "loggingProperties")
.ifPresent(props -> {
GsonPtr.to(newCm.getRaw()).getAs(JsonObject.class, "data")
.get().addProperty("logging.properties", props);
});
// Get API and update
DynamicKubernetesApi cmApi = new DynamicKubernetesApi("", "v1",
"configmaps", channel.client());
// Apply and maybe force pod update // Apply and maybe force pod update
var newState = K8s.apply(cmApi, mapDef, out.toString()); var updatedCm = K8s.apply(cmApi, newCm, newCm.getRaw().toString());
maybeForceUpdate(channel.client(), newState); maybeForceUpdate(channel.client(), updatedCm);
return newState; model.put("configMapResourceVersion",
updatedCm.getMetadata().getResourceVersion());
prevData.added.put("configMapResourceVersion",
updatedCm.getMetadata().getResourceVersion());
}
/**
* Key for association.
*/
private record PrevData(Object inputs, Map<String, Object> added) {
} }
/** /**
@ -135,4 +184,27 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
} }
} }
private final TemplateMethodModelEx adjustCloudInitMetaModel
= new TemplateMethodModelEx() {
@Override
public Object exec(@SuppressWarnings("rawtypes") List arguments)
throws TemplateModelException {
@SuppressWarnings("unchecked")
var res = new HashMap<>((Map<String, Object>) DeepUnwrap
.unwrap((TemplateModel) arguments.get(0)));
var metadata
= (V1ObjectMeta) ((AdapterTemplateModel) arguments.get(1))
.getAdaptedObject(Object.class);
if (!res.containsKey("instance-id")) {
res.put("instance-id",
Optional.ofNullable(metadata.getGeneration())
.map(s -> "v" + s).orElse("v1"));
}
if (!res.containsKey("local-hostname")) {
res.put("local-hostname", metadata.getName());
}
return res;
}
};
} }

View file

@ -21,18 +21,8 @@ package org.jdrupes.vmoperator.manager;
/** /**
* Some constants. * Some constants.
*/ */
@SuppressWarnings("PMD.DataClass")
public class Constants extends org.jdrupes.vmoperator.common.Constants { public class Constants extends org.jdrupes.vmoperator.common.Constants {
/** The Constant COMP_DISPLAY_SECRET. */
public static final String COMP_DISPLAY_SECRET = "display-secret";
/** The Constant DATA_DISPLAY_PASSWORD. */
public static final String DATA_DISPLAY_PASSWORD = "display-password";
/** The Constant DATA_PASSWORD_EXPIRY. */
public static final String DATA_PASSWORD_EXPIRY = "password-expiry";
/** The Constant STATE_RUNNING. */ /** The Constant STATE_RUNNING. */
public static final String STATE_RUNNING = "Running"; public static final String STATE_RUNNING = "Running";

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