Compare commits

...

394 commits

Author SHA1 Message Date
ansgarz
da73a87bca fix schedule in scheduleMonthlyReboot 2024-12-20 13:28:39 +01:00
ansgarz
d50f61a4d6 ddd refactoring ScheduledJobs.kt 2024-12-20 13:02:26 +01:00
ansgarz
62fa879fce [skip ci] empty lines before and after version 2024-12-20 12:49:24 +01:00
ansgarz
2d26194369 update version.txt 2024-12-20 09:36:08 +01:00
ansgarz
9004283854 bump version to: 0.39.5-SNAPSHOT 2024-12-18 21:37:22 +01:00
ansgarz
c9d778e1c5 release: 0.39.4 2024-12-18 21:37:22 +01:00
ansgarz
482ec00bdd fix addKnownHost for host with non-default port, i.e. different from 22, remove date from version.txt 2024-12-18 21:37:10 +01:00
ansgarz
dc4dd03f28 update version.txt 2024-12-11 22:20:20 +01:00
ansgarz
333c9bf50e refactor server only modules 2024-12-11 22:08:42 +01:00
ansgarz
bb383ca274 bump version to: 0.39.4-SNAPSHOT 2024-12-11 22:02:36 +01:00
ansgarz
9fb74fabae release: 0.39.3 2024-12-11 22:02:36 +01:00
ansgarz
8d9de5517a add version info output 2024-12-11 22:02:11 +01:00
ansgarz
0af6f8ebc2 bump version to: 0.39.3-SNAPSHOT 2024-12-07 13:05:21 +01:00
ansgarz
b08779daf8 release: 0.39.2 2024-12-07 13:05:21 +01:00
ansgarz
ec018b1e00 [skip ci] add hetzner_csi to README.md 2024-12-07 12:59:47 +01:00
ansgarz
c99277ddf6 [skip ci] fix cron day for Tuesday 2024-12-07 12:04:52 +01:00
ansgarz
ad7845d910 [skip ci] add cmt 2024-12-07 11:56:40 +01:00
ansgarz
1826bcc920 [skip ci] correct help text for binaries 2024-12-07 11:47:04 +01:00
ansgarz
8598357ec6 [skip ci] correct tags 2024-12-07 11:46:34 +01:00
ansgarz
e5f644bacc bump version to: 0.39.2-SNAPSHOT 2024-11-29 11:11:14 +01:00
ansgarz
eb07670363 release: 0.39.1 2024-11-29 11:11:14 +01:00
ansgarz
4e46d84662 set monthly reboot default for k3s server to true 2024-11-29 11:10:45 +01:00
ansgarz
4f77c2f909 refactor scheduleMonthlyReboot to domain 2024-11-29 11:00:29 +01:00
ansgarz
cd99efad5b bump version to: 0.39.1-SNAPSHOT 2024-11-25 21:41:28 +01:00
ansgarz
6b37c0f6e5 release: 0.39.0 2024-11-25 21:41:28 +01:00
ansgarz
b14327efc6 test in pipeline only for tags 2024-11-25 21:40:22 +01:00
ansgarz
58ff04fb35 [skip ci] fix typo 2024-11-25 21:38:19 +01:00
ansgarz
b4c907ec5e [skip ci] add possibility for monthly reboot as "only" without provisioning server 2024-11-25 21:37:23 +01:00
ansgarz
e6db481ac9 add possibility for monthly reboot of host system of k3s server 2024-11-23 18:48:25 +01:00
ansgarz
1118bc7d07 add createCronJob 2024-11-22 11:27:04 +01:00
ansgarz
c0985afacc [skip ci] correct task name binariesInstall 2024-10-25 09:13:52 +02:00
ansgarz
e8d4826708 version bump: 0.38.6-SNAPSHOT 2024-10-21 19:08:24 +02:00
ansgarz
ca2176f67a release: 0.38.5 2024-10-21 19:08:24 +02:00
ansgarz
445d12c849 refactorings & mark tests as ExtensiveContainerTests & add EnvSecretSource.kt 2024-10-21 19:08:09 +02:00
ansgarz
9e1023b4b8 [skip ci] reduce size native-images 2024-10-09 22:02:23 +02:00
ansgarz
f0e7d6518f snapshot: 0.38.5-SNAPSHOT 2024-10-09 18:31:28 +02:00
ansgarz
0d00f6f8c0 release: 0.38.4 2024-10-09 18:31:26 +02:00
ansgarz
c649010491 [skip ci] fix binary creation with native-image 2024-10-09 18:31:06 +02:00
ansgarz
659521e944 snapshot: 0.38.4-SNAPSHOT 2024-10-08 17:17:33 +02:00
ansgarz
ffee8aaff4 release: 0.38.3 2024-10-08 17:17:31 +02:00
ansgarz
b5a7b12794 improve serialization by using serializer<T>() 2024-10-08 17:16:37 +02:00
ansgarz
7f8d4c8b97 [skip ci] remove reflection dependency 2024-10-05 17:37:43 +02:00
ansgarz
e16c445b44 bump version to: 0.38.3-SNAPSHOT 2024-10-04 23:57:15 +02:00
ansgarz
9a68de9135 release: 0.38.2 2024-10-04 23:57:15 +02:00
ansgarz
98644199b5 snapshot: 0.38.2-SNAPSHOT 2024-10-04 23:47:36 +02:00
ansgarz
3f181e07b4 release: 0.38.1 2024-10-04 23:47:34 +02:00
ansgarz
ce93292336 snapshot: 0.38.1-SNAPSHOT 2024-10-04 22:30:10 +02:00
ansgarz
10633eb856 release: version = 0.38.0 2024-10-04 22:30:08 +02:00
ansgarz
da912dacf3 [skip ci] remove s3 verification from syspec & aws sdk s3 dependency 2024-10-04 22:29:26 +02:00
ansgarz
01ef388663 snapshot: 0.37.2-SNAPSHOT 2024-10-04 15:04:49 +02:00
ansgarz
4cf36605c1 release: version = 0.37.1 2024-10-04 15:04:48 +02:00
ansgarz
9affdbe04f [skip ci] add gradle task installbinaries 2024-10-04 15:04:13 +02:00
ansgarz
b546b94410 bump version to: 0.37.1-SNAPSHOT 2024-09-30 22:08:56 +02:00
ansgarz
2ee577a36f release: 0.37.0 2024-09-30 22:08:56 +02:00
ansgarz
74f56aed9c update some deps 2024-09-30 22:06:46 +02:00
78b238928b [skip ci] remove upower and dependencies to improve vm performance 2024-09-20 14:39:49 +02:00
ansgarz
bef0fff652 bump version to: 0.36.1-SNAPSHOT 2024-09-12 22:00:42 +02:00
ansgarz
8f27fde09c release: 0.36.0 2024-09-12 22:00:42 +02:00
ansgarz
9630e23ede add outTrimmed & useHomeDirAsWorkingDir for LocalProcessor 2024-09-12 20:33:10 +02:00
ansgarz
e8c0c97dbe [skip ci] fix test_incl_containtertests.run.xml 2024-09-12 20:12:25 +02:00
ansgarz
72af2db838 various refactorings 2024-09-06 22:43:55 +02:00
ansgarz
bb9146b542 [skip ci] add doc about modules 2024-09-06 22:30:01 +02:00
ansgarz
343c339a5a bump version to: 0.35.3-SNAPSHOT 2024-09-05 09:27:45 +02:00
ansgarz
44f788eb08 release: 0.35.2 2024-09-05 09:27:45 +02:00
ansgarz
41ab214a43 chg UbuntuProv to open class with public constructor 2024-09-05 09:27:20 +02:00
ansgarz
34c5101689 [skip ci] update ForDevelopers.md 2024-08-29 19:18:26 +02:00
ansgarz
30d12734fb refactor unsafe operators & method installHugoByDeb 2024-08-27 09:14:18 +02:00
ansgarz
d31ffd07b7 refactor installGraalVM 2024-08-20 17:30:33 +02:00
ansgarz
09c6de5318 add test installK9s 2024-08-20 17:15:51 +02:00
ansgarz
6f5560274d [skip ci] update README.md and docs 2024-08-16 17:31:12 +02:00
ansgarz
de7c1225b9 move VSCode installation from IDE desktop to office desktop 2024-08-16 15:41:54 +02:00
ansgarz
e14db18eb7 bump version to: 0.35.2-SNAPSHOT 2024-08-16 11:56:38 +02:00
ansgarz
2ebe84d42a release: 0.35.1 2024-08-16 11:56:38 +02:00
ansgarz
469f864339 refactorings, reformats, minor fixes, update docs 2024-08-16 11:53:46 +02:00
277302d0ee add k9s to server 2024-08-09 17:58:18 +02:00
43b7e83187 Merge branch 'main' of ssh://repo.prod.meissa.de:2222/meissa/provs 2024-08-06 14:39:22 +02:00
90d5a96ce8 [Skip-CI] Fix mastodon add website link 2024-08-06 14:39:13 +02:00
4befbd1017 [skip ci] Fix in logic 2024-07-31 10:11:58 +02:00
4bd18fcdf8 [skip ci] Added GraalVM installation to DevOps section, Test DevOpsKtTest/installGraalVM integration prooven 2024-07-29 19:48:09 +02:00
ansgarz
1b7e2824ce [skip ci] update README.md 2024-07-12 20:05:19 +02:00
ansgarz
fd0440fc2f [skip ci] improve test_install_and_configure_Gopass_and_GopassBridgeJsonApi 2024-07-12 19:55:20 +02:00
ansgarz
3217fa95bd [skip ci] update doc 2024-07-12 19:15:23 +02:00
ansgarz
ff331a45ee update gopassBridgeJsonApi version 1.15.5 to 1.15.13 2024-07-12 10:38:24 +02:00
ansgarz
812ae47d80 update gopass version 1.15.5 to 1.15.13 2024-07-12 10:08:25 +02:00
ansgarz
6e58053e1b remove openjdk-8-jdk and openjdk-11-jdk from IDE installation 2024-07-12 09:54:56 +02:00
ansgarz
4fff5257e0 [skip ci] tmp remove nextcloud-client installation 2024-07-05 22:46:04 +02:00
ansgarz
4e50537b39 fix tests 2024-07-05 22:23:59 +02:00
ansgarz
582a830a80 [skip ci] add cmts 2024-07-05 22:02:36 +02:00
ansgarz
d1693268f3 fix test_reopeing_ssh_session_succeeds 2024-07-05 21:59:48 +02:00
ansgarz
b24e4ba36c fix sha256sum for installKubectl 2024-07-05 21:45:02 +02:00
bom
ccea3c1c53 Add traefik annotation 2024-06-28 10:06:54 +02:00
a336838af8 Merge branch 'main' of ssh://repo.prod.meissa.de:2222/meissa/provs 2024-05-30 13:20:52 +02:00
6a0027fb64 Install in IDE 2024-05-30 13:20:42 +02:00
494e1bd8d6 Finish hugo install and test 2024-05-30 13:20:23 +02:00
49ec8462f0 Add Hugo install and test 2024-05-30 13:19:43 +02:00
ansgarz
7307e39ff6 update images for integration tests to ubuntu 22.04 2024-05-24 11:34:45 +02:00
ansgarz
8f90fa9d86 update images for integration tests to ubuntu 22.04 2024-05-24 11:23:47 +02:00
ansgarz
726cd5c01a [skip ci] adjust module names for tests 2024-05-24 11:04:21 +02:00
ansgarz
e2b7732a5e add installation of adduser for integration tests 2024-05-24 10:49:13 +02:00
bom
b8b7091ec2 Update deprecated ingress.class 2024-05-17 13:15:34 +02:00
bom
e568b5aa82 Server: Fix hetzner csi resource paths 2024-05-10 14:46:29 +02:00
bom
80476532d7 Server: Remove obsolete resources 2024-05-10 14:15:59 +02:00
bom
5bd824dee5 Server: Add support for hetzner csi with encryption 2024-05-10 13:44:20 +02:00
f0fa8d5ca5 [Skip-CI] Added doc, "Howto update gradle wrapper" 2024-03-15 16:16:15 +01:00
ansgarz
9d3f43975b update Kotlin version 2024-03-08 23:02:15 +01:00
989f80c41f bump version to: 0.35.1-SNAPSHOT 2024-03-08 11:14:14 +01:00
965f2e3101 release: 0.35.0 2024-03-08 11:14:14 +01:00
a20b1a9144 refactor kubeconform, installpath, check, test 2024-03-08 11:12:02 +01:00
ansgarz
771d7f7b89 bump version to: 0.34.1-SNAPSHOT 2024-03-05 23:34:15 +01:00
ansgarz
055d302022 release: 0.34.0 2024-03-05 23:34:15 +01:00
ansgarz
d187a8423c add kubeconform 2024-03-05 23:33:54 +01:00
7a506e07d4 Merge remote-tracking branch 'origin/main' 2024-02-28 10:12:07 +01:00
497fd9a45d fix scripts 2024-02-28 10:11:58 +01:00
ansgarz
c0e64096a8 bump version to: 0.33.2-SNAPSHOT 2024-02-24 20:29:03 +01:00
ansgarz
ba0f58b02d release: 0.33.1 2024-02-24 20:29:03 +01:00
ansgarz
028543df53 update logback 2024-02-24 20:28:39 +01:00
ansgarz
dd9e7b71b2 bump version to: 0.33.1-SNAPSHOT 2024-02-24 19:33:59 +01:00
ansgarz
8c6f25598d release: 0.33.0 2024-02-24 19:33:59 +01:00
ansgarz
d4115992b0 bump version to: 0.32.3-SNAPSHOT 2024-02-24 18:57:32 +01:00
ansgarz
4cf3ab358c release: 0.32.2 2024-02-24 18:57:32 +01:00
ansgarz
d0e88c3bf4 bump version to: 0.32.2-SNAPSHOT 2024-02-24 18:51:08 +01:00
ansgarz
f9c25a0e1a release: 0.32.1 2024-02-24 18:51:08 +01:00
ansgarz
8191096794 add release_main_branch to build.py 2024-02-24 18:50:40 +01:00
ansgarz
1d64b4400e bump version to: 0.32.1-SNAPSHOT 2024-02-24 18:43:38 +01:00
ansgarz
d43057bf9d release: 0.32.0 2024-02-24 18:43:38 +01:00
ansgarz
bc38779b25 bump version to: 0.31.2-SNAPSHOT 2024-02-24 18:31:40 +01:00
ansgarz
80ae171052 release: 0.31.1 2024-02-24 18:31:40 +01:00
ansgarz
2bb986f80f make deprovisionK3sInfra idempotent 2024-02-24 18:31:07 +01:00
bom
bcc89ef408 bump version to: 0.31.1-SNAPSHOT 2024-02-16 12:08:59 +01:00
bom
20db8c4aca release: 0.31.0 2024-02-16 12:08:59 +01:00
ansgarz
e85fa88bc4 update traefik.yaml 2024-02-16 11:58:26 +01:00
ansgarz
7f2ebcd6e9 update traefik.yaml 2024-02-16 11:32:32 +01:00
ansgarz
06a7cab974 bump version to: 0.30.1-SNAPSHOT 2024-02-16 10:40:07 +01:00
ansgarz
1a1d7c2f6f release: 0.30.0 2024-02-16 10:40:07 +01:00
ansgarz
eccf61b3d6 remove k3d 2024-02-16 10:37:57 +01:00
ansgarz
02ce6336a2 update k3s-install.sh 2024-02-16 10:33:43 +01:00
bom
507475f40e Update k3s version 2024-02-16 09:59:09 +01:00
33b38081d2 Add pytest to python provisioning 2024-02-16 09:59:09 +01:00
ansgarz
38ff640b00 bump version to: 0.29.14-SNAPSHOT 2024-02-02 12:03:20 +01:00
ansgarz
fa9c570186 release: 0.29.13 2024-02-02 12:03:20 +01:00
ansgarz
2cb39a82b8 fix missing function run in build.py 2024-02-02 12:03:04 +01:00
ansgarz
a4c561649a fix workaround for pyb / gopass prompt 2024-02-02 11:59:11 +01:00
ansgarz
1ae1a931b5 bump version to: 0.29.13-SNAPSHOT 2024-02-02 11:57:53 +01:00
ansgarz
3be59e887d release: 0.29.12 2024-02-02 11:57:53 +01:00
ansgarz
88e2cb5962 add info & workaround for pyb / gopass prompt 2024-02-02 11:57:20 +01:00
ansgarz
e039a68241 pin image: domaindrivenarchitecture/ddadevops-kotlin:4.10.7 & cleanup 2024-01-12 10:24:26 +01:00
ansgarz
2fa6e106c7 bump version to: 0.29.12-SNAPSHOT 2024-01-12 09:56:02 +01:00
ansgarz
97b5fdceb9 release: 0.29.11 2024-01-12 09:56:02 +01:00
ansgarz
6b89e4e928 bump version to: 0.29.11-SNAPSHOT 2023-12-15 10:41:01 +01:00
ansgarz
6cf5e75284 release: 0.29.10 2023-12-15 10:41:01 +01:00
ansgarz
099a6f1cee [skip ci] re-order steps 2023-12-15 10:38:52 +01:00
ansgarz
948516aee7 set RELEASE_ARTIFACT_TOKEN generic 2023-12-15 10:37:17 +01:00
ansgarz
647cfe5335 update .gitlab-ci.yml pin docker 2023-12-15 10:25:15 +01:00
ansgarz
73f4d31459 update .gitlab-ci.yml using docker:latest add - "export RELEASE_ARTIFACT_TOKEN" to stage package 2023-12-15 10:20:21 +01:00
ansgarz
42db8d8c92 update .gitlab-ci.yml 2023-12-15 10:07:45 +01:00
ansgarz
5b6d94851e bump version to: 0.29.10-SNAPSHOT 2023-12-15 09:55:07 +01:00
ansgarz
b268843ad4 release: 0.29.9 2023-12-15 09:55:07 +01:00
ansgarz
790a5e7957 update .gitlab-ci.yml 2023-12-15 09:54:52 +01:00
ansgarz
6b851cf783 bump version to: 0.29.9-SNAPSHOT 2023-12-15 09:42:33 +01:00
ansgarz
84132db8f3 release: 0.29.8 2023-12-15 09:42:33 +01:00
ansgarz
35f81320c1 update .gitlab-ci.yml 2023-12-15 09:42:16 +01:00
ansgarz
14787b6f0f bump version to: 0.29.8-SNAPSHOT 2023-12-15 08:58:42 +01:00
ansgarz
56334208be release: 0.29.7 2023-12-15 08:58:42 +01:00
ansgarz
52a6582abe add RELEASE_ARTIFACT_TOKEN to build 2023-12-15 08:58:15 +01:00
ansgarz
ac903c9f37 bump version to: 0.29.7-SNAPSHOT 2023-12-13 19:34:40 +01:00
ansgarz
804fe0c83c release: 0.29.6 2023-12-13 19:34:40 +01:00
ansgarz
40e2e3cd97 Revert "remove ci workaround manually updating ddadevopsbuild"
This reverts commit fc9cb72f1b.
2023-12-13 19:34:23 +01:00
ansgarz
727b53aff9 bump version to: 0.29.6-SNAPSHOT 2023-12-13 19:31:24 +01:00
ansgarz
1d12ea9c99 release: 0.29.5 2023-12-13 19:31:24 +01:00
ansgarz
fc9cb72f1b remove ci workaround manually updating ddadevopsbuild 2023-12-13 19:30:43 +01:00
ansgarz
d4f08cedc2 bump version to: 0.29.5-SNAPSHOT 2023-11-25 18:39:46 +01:00
ansgarz
873a10c76f release: 0.29.4 2023-11-25 18:39:46 +01:00
ansgarz
03a05a990a rename RELEASE_TOKEN to MEISSA_PUBLISH_PACKAGE_TOKEN 2023-11-25 18:39:27 +01:00
ansgarz
7f47c07b4d bump version to: 0.29.4-SNAPSHOT 2023-11-25 17:49:54 +01:00
ansgarz
828b2684c7 release: 0.29.3 2023-11-25 17:49:54 +01:00
ansgarz
ebea6dfad3 [skip ci] re-enable test in ci 2023-11-25 17:49:36 +01:00
ansgarz
717ddc01ae set RELEASE_ARTIFACT_TOKEN in .gitlab-ci.yml 2023-11-25 17:41:46 +01:00
ansgarz
37d2f4ff71 bump version to: 0.29.3-SNAPSHOT 2023-11-25 13:18:23 +01:00
ansgarz
d66a79b299 release: 0.29.2 2023-11-25 13:18:23 +01:00
ansgarz
d12633e43f enforce upgrade ddadevops in gitlab ci 2023-11-25 13:18:11 +01:00
ansgarz
9878aef9ae bump version to: 0.29.2-SNAPSHOT 2023-11-25 13:05:45 +01:00
ansgarz
7107fa7e5d release: 0.29.1 2023-11-25 13:05:45 +01:00
ansgarz
5cc9b32bf4 add tmp upgrade ddadevops to gitlab ci 2023-11-25 13:05:17 +01:00
ansgarz
c22f943dee bump version to: 0.29.1-SNAPSHOT 2023-11-25 12:09:27 +01:00
ansgarz
7ffff3ab13 release: 0.29.0 2023-11-25 12:09:27 +01:00
ansgarz
9eef8e9f04 [skip ci] update comments 2023-11-25 12:07:50 +01:00
ansgarz
35e849783b replace gradle release task by pyb publish_release in gitlab ci 2023-11-25 12:04:32 +01:00
ansgarz
5ed6187172 temporarily disable test on gitlab ci 2023-11-25 11:41:24 +01:00
ansgarz
997e6d8407 fix build.py 2023-11-25 11:39:13 +01:00
ansgarz
f6ba3c9117 bump version to: 0.28.4-SNAPSHOT 2023-11-25 11:25:16 +01:00
ansgarz
3903cf4a71 release: 0.28.3 2023-11-25 11:25:16 +01:00
ansgarz
4eafbce5f4 bump version to: 0.28.3-SNAPSHOT 2023-11-25 11:21:38 +01:00
ansgarz
98aa1306bf release: 0.28.2 2023-11-25 11:21:38 +01:00
ansgarz
48e9b74b37 update build.py 2023-11-24 23:10:02 +01:00
ansgarz
e060d584e9 version bump 2023-11-15 18:30:16 +01:00
ansgarz
a29d024ea3 Version 0.28.1 2023-11-15 18:28:31 +01:00
ansgarz
42c92915f8 move task package from build.gradle to build.py 2023-11-15 18:06:42 +01:00
ansgarz
59163710af fix tests 2023-11-09 09:41:11 +01:00
ansgarz
e373d327f3 remove ci debugging code 2023-11-08 18:22:09 +01:00
fe6e48f6dd [skip-ci]install asciinema with pip 2023-11-07 09:44:22 +01:00
07f7b5a6de improve doc 2023-10-20 14:53:07 +02:00
ansgarz
05450fed46 add python package inflection 2023-09-09 13:05:39 +02:00
ansgarz
419bdcd5fc [skip ci] chg docs 2023-09-09 13:01:28 +02:00
ansgarz
5572aa87ba [skip ci] chg docs 2023-09-09 12:47:03 +02:00
ansgarz
a457c1d05e [skip ci] chg/add cmts git clone 2023-08-31 23:28:52 +02:00
e56abd0c47 [skip-ci] Update traefik version as old version was not installable anymore 2023-08-29 14:58:24 +02:00
ansgarz
57adb756ad [skip ci] 0.28.1-SNAPSHOT 2023-08-27 13:01:21 +02:00
ansgarz
1ead864760 0.28.0 2023-08-27 12:59:44 +02:00
ansgarz
efb8fc8f8d [skip ci] 0.27.1-SNAPSHOT 2023-08-27 12:56:30 +02:00
ansgarz
7bcba91fd9 [skip ci] 0.27.0 2023-08-27 12:55:37 +02:00
ansgarz
2fff923539 add port to KnownHost 2023-08-27 12:54:21 +02:00
ansgarz
111d9951ed [skip ci] rename isHostKnown -> isKnownHost 2023-08-27 12:51:57 +02:00
ansgarz
87b56fb0d2 remove old metallb-0.10.2-manifest.yaml 2023-08-27 10:22:50 +02:00
ansgarz
17d3eb3491 further refactor onlyModules 2023-08-25 13:15:51 +02:00
ansgarz
3b18318921 set version k3s and kubectl to 1.27.0 and add test 2023-08-21 22:45:52 +02:00
ansgarz
11b13feb86 0.26.2 2023-08-20 15:15:40 +02:00
ansgarz
9ceb74515d refactor & fix execution of onlyModules, add tests 2023-08-20 10:51:13 +02:00
ansgarz
f4da33dcb5 [skip ci] 0.26.2-SNAPSHOT 2023-08-19 12:08:55 +02:00
ansgarz
77e063842d 0.26.1 2023-08-19 11:53:47 +02:00
ansgarz
e86efbc888 [skip ci] 0.26.0 2023-08-18 23:26:25 +02:00
ansgarz
4452cf5d01 refactor addKnownHost 2023-08-18 23:25:21 +02:00
ansgarz
2071128371 [skip ci] reformat & remove code 2023-08-17 21:20:34 +02:00
ansgarz
eff4836d08 0.25.1-SNAPSHOT 2023-08-15 22:31:12 +02:00
ansgarz
d484ae5fc6 0.25.0 2023-08-15 21:42:56 +02:00
zwa
3f4d5bb4d6 refactor-git-trust (#4)
Co-authored-by: Michael Jerger <michael.jerger@meissa-gmbh.de>
Co-authored-by: ansgarz <ansgar.zwick@meissa.de>
Reviewed-on: #4
2023-08-15 19:40:53 +00:00
c1267ac17e use pyb 2023-08-08 09:43:28 +02:00
e4e635d45b introduce new kind of ci 2023-08-08 09:22:36 +02:00
3a82fc707b introduce new kind of ci 2023-08-08 09:20:41 +02:00
a8b8c4e4db introduce new kind of ci 2023-08-08 09:18:10 +02:00
c1ceff0f94 we can handle also thunderbird this way 2023-08-01 09:03:38 +02:00
ansgarz
e8fcdae778 improve installPpaFirefox to set priority for ppa above default (snap) installation and allow unattended upgrades 2023-07-30 19:45:35 +02:00
ansgarz
32840cdd46 rename isntallFirefox to installPpaFirefox 2023-07-30 10:21:12 +02:00
9d66a17a1b [Skip-CI] Add Development and mirrors section 2023-07-28 14:31:59 +02:00
ansgarz
66470ed57a [skip ci] capitalize 2 files 2023-07-14 11:04:48 +02:00
ansgarz
cd2ab71e6c [skip ci] remove deprecated package mfa from python installations 2023-07-14 10:38:10 +02:00
bom
9f92454e29 version bump 2023-07-07 14:36:27 +02:00
bom
39c11c4a18 release 2023-07-07 14:35:33 +02:00
bom
97109f01ab Install python linters 2023-07-07 14:29:30 +02:00
bom
d9f3169708 Add missing pip packages 2023-07-07 14:25:57 +02:00
Clemens
15466c9706 Added NextcloudClientTest 2023-07-07 08:40:52 +02:00
ansgarz
02e9e7e404 [skip ci] 0.24.1-SNAPSHOT 2023-06-30 14:51:09 +02:00
ansgarz
c5762d6b06 re-add phase package 2023-06-30 14:39:32 +02:00
ansgarz
343b3e0b5d release-0.23.0 2023-06-30 14:31:14 +02:00
ansgarz
5ae9cad15f [skip ci] add release to gitlab 2023-06-30 14:30:26 +02:00
ansgarz
54f7b1c2da publish also to gitlab 2023-06-30 13:56:31 +02:00
ansgarz
c51d597e56 add debugging info 2023-06-30 13:04:23 +02:00
ansgarz
2f7eb1926e add debugging info for publishing 2023-06-30 12:44:56 +02:00
ansgarz
700132a163 remove install MS Teams as teams now only available per PWA and not as package anymore 2023-06-30 11:19:07 +02:00
ansgarz
a71adfbdc4 [skip ci] rm not-working release sh script 2023-06-19 21:45:24 +02:00
ansgarz
7a0d88ea74 update versions 2023-06-18 23:31:06 +02:00
ansgarz
e08d0d10a6 0.22.14-SNAPSHOT 2023-06-18 23:15:39 +02:00
ansgarz
fc7500b41d fix file path 2023-06-18 22:59:21 +02:00
ansgarz
685a9dc5cc add curl to ci 2023-06-18 22:24:58 +02:00
ansgarz
14efdae873 add gradle task for creating release 2023-06-18 21:56:47 +02:00
ansgarz
18a0830c33 correct pipeline 2023-06-18 16:34:54 +02:00
ansgarz
54ff18600e fix missing curl 2023-06-18 15:18:12 +02:00
ansgarz
0437c92de4 replace bash by sh in ci 2023-06-18 14:35:11 +02:00
ansgarz
3ca4a393bc add chmod for script 2023-06-18 13:44:49 +02:00
ansgarz
83afc52889 add releasing to meissa repo 2023-06-18 13:09:33 +02:00
ansgarz
4fd428109a rename token key 2023-06-18 12:48:14 +02:00
ansgarz
71c7df921a add create-release-in-repo.sh 2023-06-18 12:40:00 +02:00
ansgarz
6e609479c9 publish lib to meissa repo 2023-06-13 18:49:48 +02:00
az
bf28d6306e add git init of gopass root dir 2023-06-05 22:33:48 +02:00
az
90086e9c80 [skip ci] fix typo 2023-06-05 19:35:25 +02:00
ansgarz
fbcdddf031 [skip ci] make apt-get update optional success 2023-06-04 10:56:57 +02:00
az
e230ed2176 [skip ci] 0.22.3-SNAPSHOT 2023-06-03 09:51:27 +02:00
az
59f3e7cfad release-0.22.2 2023-06-03 09:50:01 +02:00
az
0c648eeac7 make gopass mount store idem-potent 2023-06-02 19:36:18 +02:00
az
59c5c8ba5a [skip ci] update version gopass-bridge 2023-06-02 19:21:06 +02:00
az
4063f56834 [skip ci] 0.22.2-SNAPSHOT 2023-06-01 18:58:04 +02:00
6633299f5b Update gopass/gopass-jsonapi Version 1.15.5; add check for SHA256sum to gopass-jsonapi download; change .config/gopass/config.yml to .config/gopass/config, adapt tests 2023-06-01 17:57:04 +02:00
az
5cf5b87c91 [skip ci] 0.22.1-SNAPSHOT 2023-05-26 12:48:39 +02:00
az
e0131c2caf release-0.22.0 2023-05-26 12:25:01 +02:00
az
1c0c038969 [skip ci] fix tests 2023-05-26 12:23:37 +02:00
az
48232826b1 [skip ci] 0.21.3-SNAPSHOT 2023-05-26 12:11:23 +02:00
az
723e9001fa release-0.21.2 2023-05-26 12:08:48 +02:00
az
bce6873851 [skip ci] suppress progress for getting secrets from file or gopass 2023-05-26 12:08:13 +02:00
az
51968263ff Revert "[skip-ci]Update gopass/gopass-jsonapi Version 1.15.5; add check for SHA256sum to gopass-jsonapi download"
This reverts commit 0c24b09489.
2023-05-26 12:00:22 +02:00
az
04865c6869 [skip ci] 0.21.2-SNAPSHOT 2023-05-26 10:17:52 +02:00
az
fc18e1d932 release-0.21.1 2023-05-26 10:03:47 +02:00
az
6bfd9588d8 [skip ci] remove joker 2023-05-26 10:02:04 +02:00
0c24b09489 [skip-ci]Update gopass/gopass-jsonapi Version 1.15.5; add check for SHA256sum to gopass-jsonapi download 2023-05-25 15:24:23 +02:00
az
5f77b462a3 [skip ci] 0.21.1-SNAPSHOT 2023-05-23 09:59:13 +02:00
az
35f3aee26e release-0.21.0 2023-05-23 09:58:01 +02:00
az
72e5519cef [skip ci] add possibility to install python packages in venv (important e.g. for remote installation) 2023-05-23 09:56:31 +02:00
az
5bd1432465 [skip ci] refactor arrayList to list 2023-05-23 08:47:28 +02:00
az
a763927f7d [skip ci] remove outdated clojure linter 2023-05-23 08:34:58 +02:00
az
26904c0b57 [skip ci] mark optional of apt update during installKubectl as it might show false positive error when other packages are not able to update 2023-05-22 09:45:25 +02:00
az
8323b84bbe use default config file if no config was specified and default config file exists 2023-05-12 14:13:23 +02:00
aef96be7f3 Add description Go/forgejo Installation and Testing 2023-05-05 11:00:12 +02:00
3998124f95 Update Joker Version to latest 1.1.0
Update Terraform Version to latest 1.4.6
Adapt syspec-ide-config.yaml out to 1.4.6
2023-05-04 23:30:58 +02:00
az
14c667caf3 [skip ci] 0.20.2-SNAPSHOT 2023-04-30 20:39:10 +02:00
az
626d0bc631 release-0.20.1 2023-04-30 20:37:01 +02:00
az
6e92f5bdc5 [skip ci] release-0.20.0 2023-04-30 20:31:19 +02:00
az
0aad3b4636 [skip ci] update run configs 2023-04-30 20:30:26 +02:00
az
479bc8cd8d fix installation gopass bridge and gopass-jsonapi 2023-04-30 20:25:30 +02:00
az
9a018ae3aa [skip ci] fix firefox installation test 2023-04-30 16:29:53 +02:00
az
3291fc2ccd [skip ci] fix firefox installation by allowing downgrades after having removed snap ff 2023-04-30 15:31:31 +02:00
az
c9aea931b6 [skip ci] fix apt upgrade with apt-get 2023-04-29 21:01:58 +02:00
az
49a4a8311f [skip ci] 0.19.1-SNAPSHOT 2023-04-29 19:42:28 +02:00
az
3b1dfdf345 release-0.19.0 2023-04-29 19:39:43 +02:00
az
611b2c0e6e [skip ci] re-open ssh session by RemoteUbuntuProcessor.kt if required 2023-04-29 19:38:09 +02:00
az
f4156fd9ec add warning if not session (but e.g. task) used on top-level 2023-04-28 13:53:19 +02:00
az
99c02e8a48 [skip ci] add checkPackageInstalled and refactor FirefoxKtTest.kt 2023-04-21 19:16:18 +02:00
az
2393844ef4 add upgrade to firefox installation and refactor FirefoxKtTest 2023-04-21 15:57:50 +02:00
az
d59154ddf6 [skip ci] add task to remove snap firefox 2023-04-20 23:15:28 +02:00
az
477b8fd65c [skip ci] add cmt 2023-04-20 18:17:16 +02:00
az
4cda6a9517 [skip ci] 0.18.6-SNAPSHOT 2023-04-20 09:50:32 +02:00
az
bed536c1ad release-0.18.5 2023-04-20 09:49:10 +02:00
az
87df8b9dc3 fix gopassBridge by gopassInitStoreFolder 2023-04-20 09:47:14 +02:00
az
c725cc0202 [skip ci] 0.18.5-SNAPSHOT 2023-04-13 22:33:43 +02:00
az
9c5fef3686 release-0.18.4 2023-04-13 22:32:15 +02:00
az
d03b6ff4b7 add ensureSudoWithoutPassword to provs-server 2023-04-13 22:30:38 +02:00
az
eba6037fcc introduce method session &refactor Application.kt 2023-04-13 18:22:17 +02:00
az
c78cf8e3bf lift ensureSudoWithoutPassword into task to avoid disconnect 2023-04-12 17:05:54 +02:00
az
2a5f0d95fb Revert "[skip ci] Revert "[skip ci] refactor for ssh does not need to reconnect after user is sudoer without pw required""
This reverts commit 9334f0ae92.
2023-04-12 16:54:11 +02:00
az
9334f0ae92 [skip ci] Revert "[skip ci] refactor for ssh does not need to reconnect after user is sudoer without pw required"
This reverts commit cdb4281c72.
2023-04-12 09:20:12 +02:00
az
cdb4281c72 [skip ci] refactor for ssh does not need to reconnect after user is sudoer without pw required 2023-04-11 18:16:25 +02:00
az
b36f2f965a Merge branch 'master' into MEIS-2538--make-sudo-in-application 2023-04-11 18:01:28 +02:00
az
8e2c5e13a6 [skip ci] add installation of python packages for pybuilder 2023-04-06 17:58:19 +02:00
az
54b4d3075c [skip ci] remove redundant firefox installation 2023-04-05 21:02:10 +02:00
az
abd8c34d2c [skip ci] 0.18.4-SNAPSHOT 2023-04-05 18:56:18 +02:00
az
a46cc9d2ae release-0.18.3 2023-04-05 18:55:08 +02:00
az
332978cfa1 [skip ci] add comment 2023-04-05 18:54:08 +02:00
az
29b8a99655 [skip ci] remove unnecessary output line in case of <<returned result>> 2023-04-05 18:49:08 +02:00
az
3aeeacfebf [skip ci] add tests test_verifySpecConfig_fails & test_verifySpecConfig_succeeds 2023-04-03 17:39:00 +02:00
az
b00783dd73 [skip ci] add newline before eof 2023-04-02 10:05:32 +02:00
az
727b950525 [skip ci] 0.18.3-SNAPSHOT 2023-04-02 10:04:31 +02:00
az
29e0af0c85 release-0.18.2 2023-04-02 10:00:52 +02:00
az
2667a7c64f [skip ci] add meld to office desktop 2023-04-02 09:59:51 +02:00
az
c9a7eb4142 add failure result to output if not yet included 2023-04-01 11:56:36 +02:00
az
075fe6cae1 pinning version of kubectl 2023-03-31 20:31:18 +02:00
az
44deb79865 [skip ci] update rsa fingerprint github 2023-03-31 20:30:54 +02:00
az
587e978d63 [skip ci] fix test_configureSsh 2023-03-26 21:56:59 +02:00
az
e35caca49a [skip ci] disable very long running tests for desktop setup and remove 1 test 2023-03-26 19:49:16 +02:00
az
0d66421506 [skip ci] recreate defaultTestContainer if not running 2023-03-26 19:32:31 +02:00
az
64471e9e3f [skip ci] remove redundant tag 2023-03-26 19:30:28 +02:00
az
1497d390f6 [skip ci] refactor ensureSudoWithoutPassword to application layer 2023-03-20 18:22:47 +01:00
az
8a4bbe97f9 [skip ci] 0.18.2-SNAPSHOT 2023-03-06 17:18:57 +01:00
az
4d80748526 release-0.18.1 2023-03-06 17:11:33 +01:00
ansgarz
c82abbb3db Merge branch 'local-sudoer-without-pw' into 'master'
Local sudoer without pw

See merge request domaindrivenarchitecture/provs!6
2023-02-26 19:57:21 +00:00
az
d353dd1fc2 [skip ci] add CreateProvInstanceSequence.md 2023-02-26 20:54:59 +01:00
az
eda6e6b218 [skip ci] correct DesktopCliParsingSequence.md 2023-02-26 20:16:50 +01:00
az
52641f8665 Merge branch 'master' into local-sudoer-without-pw 2023-02-26 20:03:58 +01:00
az
804bfd0040 refactor CliUtils.kt 2023-02-26 20:01:47 +01:00
az
df2a47bb6a [skip ci] improve error message when failing ssh connection 2023-02-26 19:49:45 +01:00
az
a06d47ff30 [skip ci] remove sudo without password check from UbuntuProv 2023-02-26 19:48:25 +01:00
az
8bb2e6e950 [skip ci] simplify and rename retrievePassword 2023-02-26 19:39:54 +01:00
az
082c0827e3 [skip ci] make makeUserSudoerWithoutPasswordRequired taskWithResult 2023-02-26 19:32:44 +01:00
az
bf36a6283c [skip ci] set ssh connection timeout 2023-02-26 19:28:38 +01:00
az
c72e40fb65 [skip ci] enlarge password prompt window 2023-02-26 19:25:14 +01:00
az
482280574b [skip ci] rename makeUserSudoerWithoutPasswordRequired 2023-02-26 19:18:25 +01:00
az
10a750fbf9 [skip ci] remove parameter remoteHostSetSudoWithoutPasswordRequired 2023-02-26 19:15:23 +01:00
az
d6d42c0733 [skip ci] add DesktopCliParsingSequence.md 2023-02-24 17:07:59 +01:00
az
0f7e3790ca [skip ci] update docs 2023-02-24 15:59:48 +01:00
az
3e54b0a63a [skip ci] remove CliApplication.md 2023-02-24 15:08:38 +01:00
az
ddd3ed220e [skip ci] correct ProvisionDesktopSequence.md 2023-02-23 23:04:12 +01:00
az
61320b5dbe exclude NonCi tests from ci 2023-02-23 22:39:34 +01:00
az
f672624928 Merge branch 'master' into local-sudoer-without-pw 2023-02-23 21:10:42 +01:00
az
1d42089ca3 remove unsafe-operators 2023-02-16 18:53:58 +01:00
az
836dc3ca2a [skip ci] remove unsafe-operator 2023-02-16 18:39:04 +01:00
az
3a38109c1d [skip ci] remove unsafe-operator 2023-02-16 18:37:53 +01:00
az
113313f60a [skip ci] remove unsafe-operator 2023-02-16 18:30:22 +01:00
az
93425a0f83 [skip ci] improve layout 2023-02-16 18:25:18 +01:00
az
eb04e84007 remove unsafe-operator and nullable parameter 2023-02-16 18:24:07 +01:00
az
05e648abe6 remove unsafe-operator 2023-02-16 18:04:49 +01:00
az
592dc53cc7 [skip ci] rename parameter sudo in createUser 2023-02-15 19:02:32 +01:00
az
1cfe32bd08 rename and fix currentUserCanSudoWithoutPassword 2023-02-15 18:54:18 +01:00
az
b5d64095f4 Merge branch 'master' into local-sudoer-without-pw 2023-02-15 18:42:15 +01:00
a1192f4382 Fix DefaultConfigFileRepo Test 2023-02-10 12:49:16 +01:00
69fe472971 Fix CliArgumentParserTest 2023-02-10 10:18:45 +01:00
31bea54d3b Merge branch 'refactoring-application-validierung' into 'master'
Validate Application File

See merge request domaindrivenarchitecture/provs!5
2023-02-10 08:47:52 +00:00
18c081893a Validate Application File 2023-02-10 08:47:52 +00:00
656b6530b7 Merge branch 'application-file-check' into 'master'
Check the application.yaml for errors

See merge request domaindrivenarchitecture/provs!4
2023-02-07 08:41:04 +00:00
d7ad34bb83 Check the application.yaml for errors 2023-02-07 08:41:04 +00:00
az
64788a5d6c [skip ci] add duplicates strategy gradle 2023-02-05 15:59:05 +01:00
az
1fecfedeca [skip ci] correct getConfig_fails_due_to_missing_file 2023-02-05 15:58:03 +01:00
az
ee1fe720f6 Merge branch 'master' into local-sudoer-without-pw 2023-02-05 14:31:08 +01:00
az
c754fa74d1 [skip ci] 0.18.1-SNAPSHOT 2023-02-05 14:28:34 +01:00
az
94761d9d04 release-0.18.0 2023-02-05 14:27:12 +01:00
az
2e87791ec7 [skip ci] make getConfig public 2023-02-05 14:22:24 +01:00
az
b0b63b1b4e [skip ci] remove comment 2023-02-02 21:16:32 +01:00
860dcb2fd8 Read password from stdin
Add warning to password prompt.
2023-02-01 11:36:53 +01:00
az
b25729e910 [skip ci] add local check for sudo 2023-02-01 09:41:06 +01:00
az
c7ea95fbd4 [skip ci] fix DesktopServiceKtTest.kt 2023-01-31 19:08:48 +01:00
az
921fda2d91 [skip ci] update DesktopServiceKtTest.kt 2023-01-31 19:06:09 +01:00
Mattis Böckle
305b93f2f8 Merge branch 'metallb-update' into 'master'
Update metallb version

See merge request domaindrivenarchitecture/provs!3
2023-01-25 12:15:49 +00:00
bom
5a40884543 Add Layer 2 advertisement for IPAddressPools 2023-01-20 10:50:53 +01:00
252bf48d3d [Skip-CI] WIP start test-container with correct user 2023-01-18 09:33:28 +01:00
az
6537058ecf branch initial commit 2023-01-18 09:10:19 +01:00
bom
4db72d4962 Update metallb configs to use CRDs 2023-01-06 11:13:15 +01:00
bom
48136fdc14 Remove metallb namespace file
This is already included in the manifest
2023-01-06 09:45:09 +01:00
Clemens
8a4b1b5212 use new metallb version 2022-12-16 15:15:13 +01:00
18f40610c9 [Skip-CI] Add prometheus and native manifest 2022-12-15 12:38:51 +01:00
160 changed files with 8365 additions and 2093 deletions

1
.gitignore vendored
View file

@ -9,3 +9,4 @@
/server-config.yaml
/desktop-config.yaml
/syspec-config.yaml
/.kotlin/

View file

@ -1,157 +1,112 @@
image: openjdk:11-jdk-slim
stages:
- build
- test
- package
- publish
- release
before_script:
- echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x gradlew
- echo "------ commit tag ---------------"
- echo $CI_COMMIT_TAG
- echo $CI_COMMIT_REF_NAME
.kotlin-job: &kotlin
image: domaindrivenarchitecture/ddadevops-kotlin:4.10.7
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .gradle/wrapper
- .gradle/caches
before_script:
- echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x gradlew
- export RELEASE_ARTIFACT_TOKEN=$MEISSA_REPO_BUERO_RW
- echo "------ commit info ---------------"
- echo $CI_COMMIT_TAG
- echo $CI_COMMIT_REF_NAME
- echo "----------------------------------"
.tag_only: &tag_only
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
cache:
paths:
- .gradle/wrapper
- .gradle/caches
build:
<<: *kotlin
stage: build
script:
- echo "---------- build stage ----------"
- ./gradlew assemble
- pyb build
artifacts:
paths:
- build/libs/*.jar
expire_in: 1 week
variables:
DOCKER_TLS_CERTDIR: "/certs"
test:
<<: *tag_only
stage: test
image: docker:latest
image: docker:24.0.5
services:
- docker:dind
- docker:24.0.5-dind
dependencies:
- build
before_script:
- echo "---------- BEFORE -------------"
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
script:
- echo "---------- TEST -------------"
- apk update && apk add bash openjdk11
- apk update && apk add bash openjdk11 git
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
- docker build --pull -t "$CI_REGISTRY_IMAGE" .
- docker run --privileged -dit --name provs_test -v /var/run/docker.sock:/var/run/docker.sock $CI_REGISTRY_IMAGE
- docker inspect -f '{{.State.Running}}' provs_test
- ./gradlew -x assemble test -Dtestdockerwithoutsudo=true -DexcludeTags=extensivecontainertest
- ./gradlew -x assemble test -Dtestdockerwithoutsudo=true -DexcludeTags=extensivecontainertest,nonci
artifacts:
when: on_failure
paths:
- build/reports/tests/test
- build/reports/*
reports:
junit: build/test-results/test/TEST-*.xml
.fatjars:
stage: package
rules:
# Do no allow manually triggered pipelines to prevent duplicates!
# Instead rerun the pipeline created with the last push
- if: $CI_PIPELINE_SOURCE != "push"
when: never
# Only execute when a valid version tag like v1.0, 2.3 or similar is given
# Required is always one point like 1.0
- if: $CI_COMMIT_TAG =~ /^v[0-9]+[.][0-9]+([.][0-9]+)?$/
before_script:
- echo $CI_JOB_ID
# Writing FATJAR_JOB_ID variable to environment file, as variable is needed in the release stage.
- echo FATJAR_JOB_ID=$CI_JOB_ID >> generate_executables.env
script:
- echo "---------- create fatjar ----------"
- ./gradlew fatJarLatest
- ./gradlew fatJarK3s
artifacts:
paths:
- 'build/libs/provs.jar'
- 'build/libs/provs-server.jar'
reports:
# To ensure we've access to this file in the next stage
dotenv: generate_executables.env
expire_in: 6 months
uberjar:
package:
<<: *kotlin
<<: *tag_only
stage: package
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
script:
- ./gradlew -x assemble -x test -x jar uberjarDesktop
- ./gradlew -x assemble -x test -x jar uberjarServer
- ./gradlew -x assemble -x test -x jar uberjarSyspec
- cd build/libs/
- find . -type f -exec sha256sum {} \; | sort > sha256sum.lst
- find . -type f -exec sha512sum {} \; | sort > sha512sum.lst
artifacts:
paths:
- 'build/libs/provs-desktop.jar'
- 'build/libs/provs-server.jar'
- 'build/libs/provs-syspec.jar'
- 'build/libs/sha256sum.lst'
- 'build/libs/sha512sum.lst'
expire_in: never
publish-snapshot-lib:
stage: publish
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG !~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
script:
- ./gradlew -x assemble -x test jar
- ./gradlew -x assemble -x test publish
- pyb package
artifacts:
paths:
- build/libs/*.jar
- build/libs/*.lst
publish-released-lib:
stage: publish
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
script:
- ./gradlew -x assemble -x test jar
- ./gradlew -x assemble -x test publish
artifacts:
paths:
- build/libs/*.jar
release:
image: registry.gitlab.com/gitlab-org/release-cli:latest
publish-maven-package-to-gitlab:
<<: *kotlin
<<: *tag_only
stage: publish
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
artifacts:
paths:
- 'build/libs/provs-desktop.jar'
- 'build/libs/provs-server.jar'
- 'build/libs/provs-syspec.jar'
- 'build/libs/sha256sum.lst'
- 'build/libs/sha512sum.lst'
script:
- apk --no-cache add curl
- |
release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"provs-desktop.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-desktop.jar\"}" \
--assets-link "{\"name\":\"provs-server.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-server.jar\"}" \
--assets-link "{\"name\":\"provs-syspec.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-syspec.jar\"}" \
--assets-link "{\"name\":\"sha256sum.lst\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/sha256sum.lst\"}" \
--assets-link "{\"name\":\"sha512sum.lst\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/sha512sum.lst\"}" \
- ./gradlew -x assemble -x test publishLibraryPublicationToGitlabRepository
publish-maven-package-to-meissa:
<<: *kotlin
<<: *tag_only
stage: publish
allow_failure: true
script:
- ./gradlew -x assemble -x test publishLibraryPublicationToMeissaRepository
release-to-meissa:
<<: *kotlin
<<: *tag_only
stage: release
allow_failure: true
script:
- pyb publish_release
after_script:
- echo "---------- End CI ----------"

View file

@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="provs-desktop" type="JetRunConfigurationType">
<configuration default="false" name="provision-basic-desktop" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="org.domaindrivenarchitecture.provs.desktop.application.ApplicationKt" />
<module name="provs.main" />
<option name="PROGRAM_PARAMETERS" value="basic local -o provsbinaries" />
<option name="PROGRAM_PARAMETERS" value="basic user@192.168.56.146 -p" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="test_incl_extensive_container_tests" type="JUnit" factoryName="JUnit">
<module name="provs.test" />
<module name="org.domaindrivenarchitecture.provs.provs.test" />
<option name="PACKAGE_NAME" value="org" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />

View file

@ -1,9 +1,11 @@
FROM ubuntu:latest
# image for usage in ci pipeline
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install apt-utils sudo
RUN useradd -m testuser && echo "testuser:testuser" | chpasswd && adduser testuser sudo
RUN useradd -m testuser && echo "testuser:testuserpw" | chpasswd && usermod -aG sudo testuser
RUN echo "testuser ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/testuser
USER testuser

View file

@ -1,20 +1,20 @@
# provs
[![pipeline status](https://gitlab.com/domaindrivenarchitecture/provs/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/provs/-/commits/master)
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa-gmbh.de/img/community/Mastodon_Logotype.svg" width=20 alt="team@social.meissa-gmbh.de"> team@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@team) | [Website & Blog](https://domaindrivenarchitecture.org)
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa.de/images/parts/contact/mastodon36_hue9b2464f10b18e134322af482b9c915e_5501_filter_14705073121015236177.png" width=20 alt="M"> meissa@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@meissa) | [Blog](https://domaindrivenarchitecture.org) | [Website](https://meissa.de)
## Purpose
provs provides cli-based tools for
* provisioning a desktop (various kinds)
* provisioning desktop software for different desktop types:
* basic
* office
* IDE
* provisioning a k3s server
* performing system checks
Tasks can be run locally or remotely.
## Status
under development - though we already set up a few IDEs and servers with provs.
## Try out
### Prerequisites
@ -28,8 +28,9 @@ under development - though we already set up a few IDEs and servers with provs.
* Download the latest `provs-desktop.jar`,`provs-server.jar` and/or `provs-syspec.jar` from: https://gitlab.com/domaindrivenarchitecture/provs/-/releases
* Preferably into `/usr/local/bin` or any other folder where executables can be found by the system
* Make the jar-file executable e.g. by `chmod +x provs-desktop.jar`
* Check with `provs-desktop.jar -h` to show help information
#### Build the binaries
###### Build the binaries
Instead of downloading the binaries you can build them yourself
@ -60,7 +61,6 @@ After having installed `provs-desktop.jar` (see prerequisites) execute:
* `-o` for only executing one action, e.g.
* `-o verify` for verifying your installation
* `-o firefox` to install firefox from apt on ubuntu
* `-o teams` to install MS-Teams
#### Example
@ -90,6 +90,39 @@ node:
echo: true # for demo reasons only - deploys an echo app
```
#### Monthly reboot
A monthly reboot can be deployed as only-module (i.e. without a full server deployment) by:
```bash
provs-server k3s <user>@<server> -o monthly_reboot
# Example
provs-server k3s root@myserver.org -o monthly_reboot
```
#### Hetzner CSI driver
To add the hetzner csi driver and encrypted volumes to your k3s installation add the following to the config:
```yaml
hetzner:
hcloudApiToken:
source: "PLAIN" # PLAIN, GOPASS or PROMPT
parameter: "mypassword" # the api key for the hetzner cloud
encryptionPassphrase:
source: "PLAIN" # PLAIN, GOPASS or PROMPT
parameter: "mypassword" # the encryption passphrase for created volumes
```
or as only-module (i.e. without a full server deployment) by:
```bash
provs-server k3s <user>@<server> -o hetzner_csi
# Example
provs-server k3s root@myserver.org -o hetzner_csi
```
#### Grafana agent
To add a grafana agent to your k3s installation add the following to the config:
```yaml
@ -107,6 +140,12 @@ To provision the grafana agent only to an existing k8s system, ensure that the c
provs-server.jar k3s myuser@myhost.com -o grafana
```
To provision the grafana agent only to an existing k8s system, ensure that the config (as above) is available and execute:
```bash
provs-server.jar k3s myuser@myhost.com -o grafana
```
Reprovisioning the server can easily be done using the -r or --reprovision option.
```bash
@ -145,3 +184,27 @@ Or to get help for subcommands e.g.
provs-desktop.jar ide -h
provs-server.jar k3s -h
```
## Development & mirrors
Development happens at: https://repo.prod.meissa.de/meissa/provs
Mirrors are:
* https://gitlab.com/domaindrivenarchitecture/provs (CI issues and PR)
* https://github.com/DomainDrivenArchitecture/provs
For more details about our repository model see: https://repo.prod.meissa.de/meissa/federate-your-repos
## Developer information
For using provs framework, add the required dependency to your project, then you can implement your own tasks e.g. by:
```kotlin
import org.domaindrivenarchitecture.provs.framework.core.Prov
fun Prov.myCustomTask() = task {
cmd("echo \"Hello world!\"")
}
```
See also [ForDevelopers.md](doc/ForDevelopers.md)

View file

@ -1,24 +1,24 @@
buildscript {
ext.kotlin_version = "1.7.0"
ext.kotlin_version_no = "1.8.20"
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
repositories { mavenCentral() }
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
repositories {
mavenCentral()
}
}
apply plugin: "org.jetbrains.kotlin.jvm"
apply plugin: "java-library"
apply plugin: "java-test-fixtures"
plugins {
id "org.jetbrains.kotlin.jvm" version "$kotlin_version_no"
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version_no"
id "java"
id "java-test-fixtures"
}
apply plugin: "maven-publish"
apply plugin: "kotlinx-serialization"
version = "0.39.5-SNAPSHOT"
group = "org.domaindrivenarchitecture.provs"
version = "0.17.1-SNAPSHOT"
repositories {
mavenCentral()
@ -26,12 +26,15 @@ repositories {
java {
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
withSourcesJar()
withJavadocJar()
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
test {
// set properties for the tests
def propertiesForTests = ["testdockerwithoutsudo"]
@ -57,39 +60,28 @@ compileJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestFixturesJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestJava.options.debugOptions.debugLevel = "source,lines,vars"
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
java {
withSourcesJar()
withJavadocJar()
}
dependencies {
api("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
api("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version_no")
api("org.jetbrains.kotlinx:kotlinx-cli:0.3.4")
api('com.charleskorn.kaml:kaml:0.43.0')
api('com.charleskorn.kaml:kaml:0.54.0')
api("org.slf4j:slf4j-api:1.7.36")
api('ch.qos.logback:logback-classic:1.2.11')
api('ch.qos.logback:logback-core:1.2.11')
api('ch.qos.logback:logback-classic:1.4.14')
api('ch.qos.logback:logback-core:1.4.14')
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
implementation("com.hierynomus:sshj:0.32.0")
implementation("com.hierynomus:sshj:0.38.0")
implementation("aws.sdk.kotlin:s3:0.17.1-beta")
testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.8.2")
testFixturesApi('io.mockk:mockk:1.12.3')
testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.8.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
}
task uberjarDesktop(type: Jar) {
tasks.register('uberjarDesktop', Jar) {
from sourceSets.main.output
@ -110,7 +102,7 @@ task uberjarDesktop(type: Jar) {
}
task uberjarServer(type: Jar) {
tasks.register('uberjarServer', Jar) {
from sourceSets.main.output
@ -131,7 +123,7 @@ task uberjarServer(type: Jar) {
}
task uberjarSyspec(type: Jar) {
tasks.register('uberjarSyspec', Jar) {
from sourceSets.main.output
@ -155,7 +147,7 @@ def projectRoot = rootProject.projectDir
// copy jar to /usr/local/bin and make it executable
// Remark: to be able to use it you must have jarwrapper installed (sudo apt install jarwrapper)
task installlocally {
tasks.register('installlocally') {
dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
doLast {
exec { commandLine("sh", "-c", "sudo apt-get update & sudo apt-get install jarwrapper") }
@ -168,34 +160,94 @@ task installlocally {
}
}
task sourceJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
archiveClassifier.set("sources")
// create binaries and install into /usr/local/bin
// PREREQUISITE: graalvm / native-image must be installed - see https://www.graalvm.org/
// NOTE: May take up tp 10 Min altogether!
tasks.register('binariesInstall') {
dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
doLast {
println "Building binaries ..."
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-desktop.jar") }
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-server.jar") }
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-syspec.jar") }
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-desktop /usr/local/bin/") }
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-server /usr/local/bin/") }
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-syspec /usr/local/bin/") }
}
}
// publish to repo.prod.meissa.de with task "publishLibraryPublicationToMeissaRepository" -- (using pattern "publishLibraryPublicationTo<MAVEN REPOSITORY NAME>Repository")
publishing {
publications {
library(MavenPublication) {
groupId 'org.domaindrivenarchitecture'
artifactId 'provs'
from components.java
}
}
repositories {
if (System.getenv("CI_JOB_TOKEN") != null) {
// see https://docs.gitlab.com/ee/user/packages/maven_repository/index.html
maven {
url "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
name "GitLab"
credentials(HttpHeaderCredentials) {
name = "Job-Token"
value = System.getenv("CI_JOB_TOKEN")
}
authentication {
header(HttpHeaderAuthentication)
maven {
name = "gitlab"
url = "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
credentials(HttpHeaderCredentials) {
name = "Job-Token"
value = System.getenv("CI_JOB_TOKEN")
}
authentication {
header(HttpHeaderAuthentication)
}
}
maven {
name = "meissa"
url = uri("https://repo.prod.meissa.de/api/packages/meissa/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
def publishPackageTokenName = "MEISSA_PUBLISH_PACKAGE_TOKEN"
if (System.getenv("CI_JOB_TOKEN") != null) {
def tokenFromEnv = System.getenv(publishPackageTokenName)
if (tokenFromEnv == null) {
println "Error: $publishPackageTokenName not found"
} else {
value = "token " + tokenFromEnv
println "$publishPackageTokenName found - "
}
} else {
// use project-property (define e.g. in "~/.gradle/gradle.properties") when not running in ci
// you can create a token in gitea "Profile and Settings ... > Settings > Applications", Token Name, Select scopes (write:package) > "Generate Token"
if (!project.hasProperty(publishPackageTokenName)) {
// if token is missing, provide a dummy in order to avoid error "Could not get unknown property 'MEISSA_PUBLISH_PACKAGE_TOKEN' for Credentials [header: Authorization]" for other gradle tasks
ext.MEISSA_PUBLISH_PACKAGE_TOKEN = "Token $publishPackageTokenName not provided in file \".gradle/gradle.properties\""
println "Error: Token $publishPackageTokenName not found"
} else {
value = "token " + project.property(publishPackageTokenName)
}
}
}
} else {
mavenLocal()
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
// create version file to allow Kotlin code to print own version - see https://stackoverflow.com/questions/33020069/how-to-get-version-attribute-from-a-gradle-build-to-be-included-in-runtime-swing
tasks.register('createVersion') {
dependsOn processResources
doLast {
def version = project.version.toString()
def fileName = "src/main/resources/version.txt"
def file = new File(fileName)
file.write(version)
println "Created file: " + fileName
}
}
classes {
dependsOn createVersion
}

141
build.py Normal file
View file

@ -0,0 +1,141 @@
import os
from subprocess import run
from pybuilder.core import task, init
from ddadevops import *
name = "provs"
PROJECT_ROOT_PATH = "."
version = "0.38.3-dev"
@init
def initialize0(project):
"""
workaround to avoid prompt for gopass if no artifacts need to be uploaded
usage: with option "-E ng" , e.g. "pyb -E artifacts patch_local"
"""
os.environ["RELEASE_ARTIFACT_TOKEN"] = "dummy" # avoids prompt for RELEASE_ARTIFACT_TOKEN
@init(environments=["artifacts"])
def initialize1(project):
"""
prompt for gopass if artifacts need to be uploaded
usage: with option "-E artifacts" , e.g. "pyb -E artifacts dev"
"""
del os.environ["RELEASE_ARTIFACT_TOKEN"]
@init
def initialize2(project):
input = {
"name": name,
"module": "notused",
"stage": "notused",
"project_root_path": PROJECT_ROOT_PATH,
"build_types": [],
"mixin_types": ["RELEASE"],
"release_main_branch": "main",
"release_primary_build_file": "build.gradle",
"release_secondary_build_files": ["build.py"],
# release artifacts
"release_artifact_server_url": "https://repo.prod.meissa.de",
"release_organisation": "meissa",
"release_repository_name": name,
"release_artifacts": [
"build/libs/provs-server.jar",
"build/libs/provs-desktop.jar",
"build/libs/provs-syspec.jar",
"build/libs/sha256sum.lst",
"build/libs/sha512sum.lst",
],
}
build = ReleaseMixin(project, input)
build.initialize_build_dir()
@task
def dev(project):
"""
to avoid gopass prompt set RELEASE_ARTIFACT_TOKEN e.g.:
RELEASE_ARTIFACT_TOKEN=xxx pyb dev
"""
run("./gradlew assemble", shell=True)
@task
def build(project):
run("./gradlew assemble", shell=True)
@task
def patch(project):
"""
updates version to next patch level, creates a tag, creates new SNAPSHOT version,
commits primary build file (build.gradle) and pushes to remote
"""
increase_version_number(project, "PATCH")
release(project)
@task
def minor(project):
"""
updates version to next minor level, creates a tag, creates new SNAPSHOT version,
commits primary build file (build.gradle) and pushes to remote
"""
increase_version_number(project, "MINOR")
release(project)
@task
def major(project):
"""
updates version to next major level, creates a tag, creates new SNAPSHOT version,
commits primary build file (build.gradle) and pushes to remote
"""
increase_version_number(project, "MAJOR")
release(project)
@task
def tag(project):
build = get_devops_build(project)
build.tag_bump_and_push_release()
@task
def release(project):
build = get_devops_build(project)
build.prepare_release()
tag(project)
@task
def package(project):
run("./gradlew assemble -x test jar", shell=True)
run("./gradlew assemble -x test uberjarDesktop", shell=True)
run("./gradlew assemble -x test uberjarServer", shell=True)
run("./gradlew assemble -x test uberjarSyspec", shell=True)
run("cd build/libs/ && find . -type f -exec sha256sum {} \; | sort > sha256sum.lst", shell=True)
run("cd build/libs/ && find . -type f -exec sha512sum {} \; | sort > sha512sum.lst", shell=True)
@task
def publish_release(project):
""" creates a release in repo.meissa and uploads artifacts (jar-files and checksum files) """
build = get_devops_build(project)
build.publish_artifacts()
@task
def inst(project):
run("./gradlew inst", shell=True)
def increase_version_number(project, release_type):
build = get_devops_build(project)
build.update_release_type(release_type)

View file

@ -1,63 +0,0 @@
```plantuml
@startuml
autonumber
skinparam sequenceBox {
borderColor White
}
participant User
box "application" #LightBlue
participant CliWorkplace
participant CliWorkplaceParser
participant CliWorkplaceCommand
participant Application
end box
box #White
participant CliUtils
participant "Prov (local or remote...)" as ProvInstance
end box
box "domain" #LightGreen
participant ProvisionWorkplace
end box
box "infrastructure" #CornSilk
participant ConfigRepository
participant "Infrastructure functions" as Infrastructure_functions
end box
User -> CliWorkplace ++ : main(args...)
CliWorkplace -> CliWorkplaceParser : parseWorkplaceArguments
CliWorkplace -> CliWorkplaceCommand : isValid ?
CliWorkplace -> ConfigRepository : getConfig
CliWorkplace -> CliUtils : createProvInstance
ProvInstance <- CliUtils : create
CliWorkplace -> Application : provision ( config )
Application -> ProvInstance : provisionWorkplace ( type, ssh, ...)
ProvInstance -> ProvisionWorkplace : provisionWorkplace
ProvisionWorkplace -> Infrastructure_functions: Various calls like:
ProvisionWorkplace -> Infrastructure_functions: install ssh, gpg, git ...
ProvisionWorkplace -> Infrastructure_functions: installVirtualBoxGuestAdditions
ProvisionWorkplace -> Infrastructure_functions: configureNoSwappiness, ...
@enduml
```

View file

@ -0,0 +1,55 @@
```plantuml
@startuml
autonumber
skinparam sequenceBox {
borderColor White
}
participant Cli
participant Application
participant CliArgumentsParser
participant CliTargetCommand
participant CliUtils
participant "CliUtils\ncreateLocalProv" as CliUtilsL
participant "CliUtils\ncreateRemoteProv" as CliUtilsR
participant Prov
participant PromptSecretSource
participant User
Cli -> Application ++ : main(args...)
Application -> CliArgumentsParser : parseCommand
CliArgumentsParser -> CliTargetCommand : create()
Application -> CliUtils : createProvInstance( targetCliCommand )
alt target.isValidLocal
CliUtils -> CliUtilsL : createLocalProv
CliUtilsL -> Prov : createLocalInstance
alt userCannotSudoWithoutPw
CliUtilsL -> PromptSecretSource : getPassword
CliUtilsL -> User : makeUserSudoWithoutPw
CliUtilsL --> CliUtils : provInstance
CliUtils --> Application : provInstance
end
else target.isValidRemote
CliUtils -> CliUtilsR : createRemoteProv
CliUtilsR -> Prov : createRemoteInstance
alt userCannotSudoWithoutPw
CliUtilsR -> PromptSecretSource : getPassword
CliUtilsR -> User : makeUserSudoWithoutPw
CliUtilsR -> Prov : createRemoteInstance\n[new ssh-client is required]
CliUtilsR --> CliUtils : provInstance
CliUtils --> Application : provInstance
end
end
Application -> DesktopService1 : provisionDesktopCommand ( provInstance, desktopCliCommand )
'DesktopService1 -> DesktopService2 : provisionDesktop( config )
'DesktopService1 -> ConfigRepository : getConfig
@enduml
```

View file

@ -0,0 +1,38 @@
```plantuml
@startuml
autonumber
skinparam sequenceBox {
borderColor White
}
participant User
User -> Application ++ : main(args...)
Application -> CliArgumentsParser : create
CliArgumentsParser -> ArgParser : subcommands
Application -> CliArgumentsParser : parseCommand
CliArgumentsParser -> ArgParser : super.parse
CliArgumentsParser -> CliTargetCommand : create()
CliTargetCommand -> CliTargetCommand : parseRemoteTarget
alt passwordInteractive == true
CliTargetCommand -> PromptSecretSource : prompt-for-password
end
CliArgumentsParser -> DesktopCliCommand : create(desktopType, cliTargetCmd, ...)
CliArgumentsParser --> Application: desktopCliCommand
Application -> DesktopCliCommand : isValid ?
Application -> CliUtils : createProvInstance
alt target.isValidLocal
CliUtils -> CliUtils : createLocalProv
else target.isValidRemote
CliUtils -> CliUtils : createRemote
end
Application -> DesktopService1 : provisionDesktopCommand ( provInstance, desktopCliCommand )
DesktopService1 -> DesktopService2 : provisionDesktop( config )
DesktopService1 -> ConfigRepository : getConfig
@enduml
```

View file

@ -1,22 +1,8 @@
# Information for developers
This page provides information for developers.
## Create a provs jar-file
# Tasks
* Clone this repo
* Build the jar-file by `./gradlew uberjarDesktop`
* In folder build/libs you'll find the file `provs-desktop.jar`
This uberjar is a Java jar-file including all required dependencies.
## Task
```kotlin
fun Prov.provisionK8s() = task { /* ... code and subtasks come here ... */ }
```
If you're having a deeper look into the provs code, you'll see regularly a task definition like this and might wonder ...
### What is a task ?
## What is a task ?
A task is the **basic execution unit** in provs. When executed, each task produces exactly one result (line) with either success or failure.
@ -26,9 +12,108 @@ The success or failure is computed automatically in the following way:
* a task defined with **optional** (i.e. `= optional { /* ... */ }` always returns success (even if there are failing subtasks)
* **requireLast** defines a task which must provide an explicit result and solely this result counts for success calculation
## Task declaration
### Recommended way
A task can be declared by
```kotlin
fun Prov.myCustomTask() = task { /* ... code and subtasks come here ... */ }
// e.g.
fun Prov.myEchoTask() = task {
cmd("echo hello world!")
}
```
The task will succeed if all sub-tasks (called tasks during execution) have succeeded resp. if no sub-task was called.
### Alternative ways
The following ways are equivalent but are more verbose:
```kotlin
// Redundant declaration of the return type (ProvResult), which is already declared by task
fun Prov.myCustomTask(): ProvResult = task { /* ... code and subtasks come here ... */ }
// Redundant parentheses behind task
fun Prov.myCustomTask() = task() { /* ... code and subtasks come here ... */ }
// Redundant definition of the task name, but could be used to output a different task name
fun Prov.myCustomTask() = task("myCustomTask") { /* ... code and subtasks come here ... */ }
// Functionally equal, but with additional curly brackets
fun Prov.myCustomTask() { task { /* ... code and subtasks come here ... */ } }
```
Btw, the following lines and WILL NOT work as expected.
Due to too much lamda nesting, the code within the task is NOT executed:
```kotlin
fun Prov.myCustomTask() = { task { /* ... code and subtasks come here ... */ } }
fun Prov.myCustomTask() {{ task { /* ... code and subtasks come here ... */ } }}
```
### Add custom results
If you want to add a result explicitly, you can use method `addResultToEval`.
This maxy be used e.g. to add explicitly an error line, like in:
```kotlin
fun Prov.myCustomTask() = task {
/* some other code ... */
addResultToEval(ProvResult(false, err = "my error msg"))
/* some other code ... */
}
```
or alternatively you can use `taskWithResult`.
#### TaskWithResult
In case you want to include the return value (of type `ProvResult`) of a task to be added to the evaluation,
you can use `taskWithResult` instead of `task` and return the value, e.g. like
```kotlin
fun Prov.myEchoTask() = taskWithResult {
cmd("echo hello world!")
// ...
ProvResult(false, "Error: ... error message ...") // will be the returned as return value and included in the evaluation
}
```
IMPORTANT: the value you want to return must be placed at the end of the lambda code (as usual in functional programming)!
The following will NOT work as expected:
```kotlin
fun Prov.myEchoTask() = taskWithResult {
ProvResult(false, "Error: ... error message ...") // will be ignored
// the result from the call below (i.e. from task "cmd") will be returned by myEchoTask,
// which is redundant as its result is already included in the evaluation anyway.
cmd("echo hello world!")
}
```
### Task output
If a task is run e.g. with `local().myEchoTask()`, it will produce output like
```
> Success -- myEchoTask
---> Success -- cmd [/bin/bash, -c, echo hello world!]
```
## Call hierarchy
Find below an example of a sequence diagram when provisioning a desktop workplace:
In the following link you can find an example of a sequence diagram when provisioning a desktop:
![img.png](resources/provision-workplace-sequence.diagram.png)
[ProvisionDesktopSequence.md](ProvisionDesktopSequence.md)
## Create a provs jar-file
* Clone this repo
* Build the jar-file by `./gradlew uberjarDesktop`
* In folder build/libs you'll find the file `provs-desktop.jar`
This uberjar is a Java jar-file including all required dependencies.

45
doc/GoForgejo_install.md Normal file
View file

@ -0,0 +1,45 @@
# Go / forgejo Installation and Testing
## go install/update
#### remove old version
sudo rm -rf ~/go
### download latest version and configure
curl -OL https://go.dev/dl/go1.21.3.linux-amd64.tar.gz
# extract latest version to ~/go
tar -C ~ -xzf go*.linux-amd64.tar.gz
# append path
```
(meissa) jem@meissa-ide-2023:~$ cat .bashrc.d/go.sh
PATH=$PATH:$HOME/go/bin
export PATH
```
## VScode optional - TODO!?!
"Go for VS Code v0.39.1"
## Testing forgejo
full:
make test
require node:
make test-frontend
require go:
make test-backend
#nvm - required to build forgejo frontend
sudo apt remove nodejs
sudo apt autoremove
adapt version to latest:
curl o https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
nvm install
optional:
nvm alias default "latest"
## forgejo build
TAGS="bindata" make build
-> include make frontend & make backend //see details Makefile

20
doc/Modularization.md Normal file
View file

@ -0,0 +1,20 @@
# Modules
## Modules and their possible relations
![modularization.png](resources/modularization.png)
#### Modules
A,B,C: Modules with both domain and infrastructure layer code - common type of modules
D: Module with only domain: can sometimes make sense if only domain logic and no infrastructure logic is required
E: Module with only infrastructure: usually utility modules that just provide a collection of infrastructure functionality
#### Interactions
1. Domain calls (a function in) the infrastructure of the same module - common practice within a module
1. Domain calls (a function in) the domain another module - common practice between modules
1. Infrastructure calls infrastructure of another module - usually not recommended
1. Domain calls infrastructure in another module - can make sense in some cases e.g. if module D just needs some low-level function of module D. However where possible calling domain of module C should be preferred
1. Domain calls infrastructure in another module, which only has infrastructure - common practice for calling utility modules, which don't have a domain.

View file

@ -1,35 +0,0 @@
@startuml
autonumber
Application -> Prov: create
activate Prov
Application -> DesktopService.kt: provisionDesktop(prov, ...)
DesktopService.kt -> Install.kt: aptInstall(prov, lambda=cmd "apt install", ..)
Install.kt -> Prov: taskWithResult
activate Prov
Prov -> Prov: evaluate
activate Prov
Prov -> Prov: initProgress (bei level 0)
Prov -> Prov: progress
activate Prov
Prov -> Prov: lambda
activate Prov
Prov -> Processor: exec
deactivate Prov
Prov <-- Prov: ProvResult
deactivate Prov
Prov -> Prov: endProgress (bei level 0)
Prov -> Prov: printResults (bei level 0)
deactivate Prov
deactivate Prov
Install.kt <-- Prov: ProvResult
@enduml

View file

@ -0,0 +1,47 @@
```plantuml
@startuml
autonumber
participant Application
participant DesktopService
participant Install
participant Prov
participant Processor
Application -> Prov: create
activate Prov
Application -> DesktopService: provisionDesktop(prov, ...)
DesktopService -> Install: prov.aptInstall()
Install -> Prov: taskWithResult( lambda = cmd("sudo apt install ...") )
activate Prov
Prov -> Prov: evaluate
activate Prov
Prov -> Prov: initProgress (if level 0)
Prov -> Prov: progress
activate Prov
Prov -> Prov: lambda
activate Prov
Prov -> Processor: exec
Prov <-- Processor: exec
deactivate Prov
deactivate Prov
Prov -> Prov: endProgress (if level 0)
Prov -> Prov: printResults (if level 0)
deactivate Prov
deactivate Prov
Install <-- Prov: ProvResult
DesktopService <-- Install
Application <-- DesktopService
@enduml
```

View file

@ -0,0 +1,53 @@
```plantuml
@startuml
autonumber
skinparam sequenceBox {
borderColor White
}
participant User
box "application" #LightBlue
participant Application
participant CliArgumentsParser
participant DesktopCliCommand
participant ProvWithSudo
end box
box #White
participant CliUtils
participant "Prov (local or remote...)" as ProvInstance
end box
box "domain" #LightGreen
participant "DesktopService"
end box
box "infrastructure" #CornSilk
participant ConfigRepository
participant "Various\ninfrastructure functions" as Infrastructure_functions
end box
User -> Application ++ : main(args...)
Application -> CliArgumentsParser : parseCommand
Application -> DesktopCliCommand : isValid ?
Application -> CliUtils : createProvInstance
ProvInstance <- CliUtils : create
Application -> ProvWithSudo : ensureSudoWithoutPassword
Application -> DesktopService : provisionDesktopCommand ( provInstance, desktopCliCommand )
DesktopService -> ConfigRepository : getConfig
DesktopService -> DesktopService : provisionDesktop( config )
DesktopService -> Infrastructure_functions: Various calls like:
DesktopService -> Infrastructure_functions: install ssh, gpg, git ...
DesktopService -> Infrastructure_functions: installVirtualBoxGuestAdditions
DesktopService -> Infrastructure_functions: configureNoSwappiness, ...
@enduml
```

View file

@ -2,8 +2,6 @@ This repository holds the documentation of the provs framework.
# Design principles
For usage examples it is recommended to have a look at [provs-scripts](https://gitlab.com/domaindrivenarchitecture/provs-scripts) or [provs-ubuntu-extensions](https://gitlab.com/domaindrivenarchitecture/provs-ubuntu-extensions).
## "Implarative"
Configuration management tools are usually classified as either **imperative** or **declarative**.
@ -28,45 +26,3 @@ In the following document we describe how we implement idempotence:
https://gitlab.com/domaindrivenarchitecture/overview/-/blob/master/adr-provs/quasi-idempotence.md
## Architecture
Multiple architectural layers provide different levels of functionality:
![provs layers](resources/provs-architecture-7.png "Provs architecture")
## Module structure
For the modules we use domain-drive design according to:
https://gitlab.com/domaindrivenarchitecture/overview/-/blob/master/adr-provs/ddd-structure.md
## Module dependencies
![resources/prov-module-dependencies-5b.png](resources/prov-module-dependencies-5b.png)
__Explanation__:
Modules:
<ol type="A">
<li>Common module: has both a domain layer and an infrastructure layer</li>
<li>Module with only domain layer: e.g. for very simple logic where no infrastructure layer is needed</li>
<li>Module with only infrastructure layer: these are often _utility modules_, which provide a collection of utility functions</li>
</ol>
Dependencies:
1. Domain layer calls (a function in) the infrastructure layer of the same module
* _Common practice of dependencies within a module_
1. Domain layer calls (a function in) the domain layer another module
* _Common practice of dependencies between modules_
1. Base layer calls domain layer
* _Usually not recommended!_
4. Domain layer calls infrastructure layer in another module
* _This sometimes can make sense, e.g. if module B just needs some low-level function of module D instead of full provisioning.
However, in most cases it is recommended to call the domain layer of module D whenever possible_
5. Domain layer calls infrastructure layer in another module, which only has infrastructure layer
* _Common practice for calling utility modules, which don't have a domain layer._

View file

@ -5,7 +5,7 @@ release-1.2 or release-1.2.3
I.e.: release-X.X.Z where X, Y, Z are the major, minor resp. the patch level of the release. Z can be omitted.
**Note:** Such kind of release tags should only be applied to commits in the master branch.
**Note:** Such kind of release tags should only be applied to commits in the main branch.
```
#adjust [version]

View file

@ -0,0 +1,38 @@
# ADR: We implement domain services static
Domain services can be implemented either as object (and composed like done in spring / example1 ) or with extension
function and composed static (see example2).
## example1
```kotlin
class DesktopServie(val aptApi: AptApi, val prov: Prov) {
fun provisionIdeDesktop(onlyModules: List<String>? = null) {
prov.task {
if (onlyModules == null) {
aptApi.aptInstall(OPEN_VPM)
}
}
}
}
```
## example2
```kotlin
fun Prov.provisionIdeDesktop(onlyModules: List<String>? = null) {
if (onlyModules == null) {
aptInstall(OPEN_VPM)
}
}
```
## Decission
We use extension function and composed static.
## Reason
1. Similar to composed objects we can easily mock `aptInstall` in tests. Both solutions are equivalent.
2. Inheritance in case of composed objects we can solve by static composition.
3. Object composition we can solve by static composition.
There is no reason left to change the current implementd pattern.

77
doc/dev/architecture.md Normal file
View file

@ -0,0 +1,77 @@
## Initialization
```mermaid
sequenceDiagram
actor user
participant app as Application
participant ds as DesktopService
participant gtr as KnownHost
participant pa as CliArgumentsParser
participant cr as DesktopConfigRepository
participant ut as CliUtils
participant su as ProvsWithSudo
user ->> app: main
activate app
app ->> pa: parseCommands
app ->> cr: getConfig(configFileName)
app ->> ut: createProvInstance(cmd.target)
app ->> su: ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
app ->> ds: provisionDesktopCommand(cmd, config)
activate ds
ds ->> gtr: values()
gtr -->> ds: List(KnownHost)
deactivate ds
deactivate app
```
## Domain
```mermaid
classDiagram
namespace configuration {
class TargetCliCommand {
val target: String,
val passwordInteractive: Boolean = false
}
class ConfigFileName {
fileName: String
}
}
namespace desktop {
class DesktopCliCommand {
}
class DesktopConfig {
val ssh: SshKeyPairSource? = null,
val gpg: KeyPairSource? = null,
val gitUserName: String? = null,
val gitEmail: String? = null,
}
class DesktopType {
val name: String
}
class DesktopOnlyModule {
<<enum>>
FIREFOX, VERIFY
}
class KnownHost {
hostName: String,
hostKeys: List<HostKey>
}
}
DesktopCliCommand "1" *-- "1" DesktopType: type
DesktopCliCommand "1" *-- "1" TargetCliCommand: target
DesktopCliCommand "1" *-- "1" ConfigFileName: configFile
DesktopCliCommand "1" *-- "..n" DesktopOnlyModule: onlyModules
```

View file

@ -0,0 +1,10 @@
### Howto update gradle wrapper
1. To *latest* version (be aware for deprecated parts in future versions):
```shell
./gradlew wrapper --gradle-version latest
```
2. To *specific version:
```shell
./gradlew wrapper --gradle-version 8.6
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

View file

@ -3,7 +3,6 @@ package org.domaindrivenarchitecture.provs.configuration.application
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
open class CliTargetParser(name: String) : ArgParser(name) {
val target by argument(
@ -16,14 +15,4 @@ open class CliTargetParser(name: String) : ArgParser(name) {
"p",
"prompt for password for remote target",
).default(false)
}
fun parseTarget(
programName: String = "provs",
args: Array<String>
): TargetCliCommand {
val parser = CliTargetParser(programName)
parser.parse(args)
return TargetCliCommand(parser.target, parser.passwordInteractive)
}

View file

@ -0,0 +1,21 @@
package org.domaindrivenarchitecture.provs.configuration.application
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
fun Prov.ensureSudoWithoutPassword(password: Secret?) {
if (!currentUserCanSudoWithoutPassword()) {
val passwordNonNull = password ?: getPasswordToConfigureSudoWithoutPassword()
val result = makeCurrentUserSudoerWithoutPasswordRequired(passwordNonNull)
check(result.success) {
"Could not make user a sudoer without password required. (E.g. the password provided may be incorrect.)"
}
}
}

View file

@ -7,7 +7,7 @@ import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileReposit
class DefaultConfigFileRepository : ConfigFileRepository {
override fun assertExists(configFileName: ConfigFileName?) {
if (configFileName != null && !checkLocalFile(configFileName.fullqualified())) {
throw RuntimeException("Config file ${configFileName.fileName} not found. Please check if path is correct.")
throw RuntimeException("Config file not found. Please check if path is correct.")
}
}
}

View file

@ -1,9 +1,16 @@
package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.serialization.SerializationException
import org.domaindrivenarchitecture.provs.configuration.application.ensureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopConfig
import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktopCommand
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.printProvsVersion
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import java.io.FileNotFoundException
import java.nio.file.Files
import kotlin.io.path.Path
import kotlin.system.exitProcess
/**
@ -11,25 +18,47 @@ import kotlin.system.exitProcess
*/
fun main(args: Array<String>) {
printProvsVersion()
val cmd = CliArgumentsParser("provs-desktop.jar subcommand target").parseCommand(args)
if (!cmd.isValid()) {
println("Arguments are not valid, pls try option -h for help.")
exitProcess(1)
}
val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true)
val defaultConfigFileName = "desktop-config.yaml"
val config = if ((cmd.configFile == null) && !Files.isRegularFile(Path(defaultConfigFileName))) {
println("ATTENTION: No config provided => using an empty config.")
DesktopConfig()
} else {
val configFileName = cmd.configFile?.fileName ?: defaultConfigFileName
try {
getConfig(configFileName)
} catch (_: SerializationException) {
println(
"Error: File \"${configFileName}\" has an invalid format and or invalid data."
)
null
} catch (_: FileNotFoundException) {
println(
"Error: File\u001b[31m $configFileName \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m $configFileName \u001B[0m " +
"and change the content according to your needs."
)
null
}
}
try {
provisionDesktopCommand(prov, cmd)
} catch (e: SerializationException) {
println(
"Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n"
)
} catch (e: FileNotFoundException) {
println(
"Error: File\u001b[31m ${cmd.configFile?.fileName} \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${cmd.configFile?.fileName} \u001B[0m " +
"and change the content according to your needs.\n"
)
if (config == null) {
println("No suitable config found.")
quit(-1)
}
val prov = createProvInstance(cmd.target)
prov.session {
ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
provisionDesktopCommand(cmd, config)
}
}

View file

@ -1,6 +1,7 @@
package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
@ -10,6 +11,7 @@ import org.domaindrivenarchitecture.provs.desktop.domain.DesktopOnlyModule
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType
@OptIn(ExperimentalCli::class)
open class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<DesktopSubcommand> = listOf(Basic(), Office(), Ide())
@ -23,12 +25,14 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
val module = modules.first { it.parsed }
val targetCliCommand = TargetCliCommand(
target,
passwordInteractive
)
return DesktopCliCommand(
DesktopType.valueOf(module.name.uppercase()),
TargetCliCommand(
target,
passwordInteractive
),
targetCliCommand,
module.configFileName,
module.onlyModules
)
@ -55,7 +59,7 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
override fun execute() {
configFileName = cliConfigFileName?.let { ConfigFileName(it) }
parsed = true
onlyModules = if (only != null) listOf(only!!.name.lowercase()) else null
onlyModules = only?.let { listOf(it.name.lowercase()) }
}
}

View file

@ -1,5 +1,10 @@
package org.domaindrivenarchitecture.provs.desktop.domain
enum class DesktopOnlyModule {
TEAMS, FIREFOX, VERIFY
FIREFOX, VERIFY
;
fun isIn(list: List<String>): Boolean {
return list.any { it.equals(this.name, ignoreCase = true) }
}
}

View file

@ -1,5 +1,7 @@
package org.domaindrivenarchitecture.provs.desktop.domain
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopOnlyModule.FIREFOX
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopOnlyModule.VERIFY
import org.domaindrivenarchitecture.provs.desktop.infrastructure.*
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.git.provisionGit
@ -9,22 +11,26 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.KeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.SshKeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.provisionKeys
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
internal fun provisionDesktopCommand(prov: Prov, cmd: DesktopCliCommand) {
// retrieve config
val conf = if (cmd.configFile != null) getConfig(cmd.configFile.fileName) else DesktopConfig()
internal fun Prov.provisionDesktopCommand(cmd: DesktopCliCommand, conf: DesktopConfig) = task {
prov.provisionDesktop(
cmd.type,
conf.ssh?.keyPair(),
conf.gpg?.keyPair(),
conf.gitUserName,
conf.gitEmail,
cmd.onlyModules
)
validatePrecondition()
val only = cmd.onlyModules
if (only == null) {
provisionDesktop(
cmd.type,
conf.ssh?.keyPair(),
conf.gpg?.keyPair(),
conf.gitUserName,
conf.gitEmail,
)
} else {
provisionOnlyModules(cmd.type, only)
}
}
@ -42,128 +48,122 @@ internal fun Prov.provisionDesktop(
gpg: KeyPair? = null,
gitUserName: String? = null,
gitEmail: String? = null,
onlyModules: List<String>?
) = task {
validatePrecondition()
provisionBasicDesktop(gpg, ssh, gitUserName, gitEmail, onlyModules)
provisionBasicDesktop(gpg, ssh, gitUserName, gitEmail)
if (desktopType == DesktopType.OFFICE) {
provisionOfficeDesktop(onlyModules)
if (onlyModules == null) {
verifyOfficeSetup()
}
provisionOfficeDesktop()
verifyOfficeSetup()
}
if (desktopType == DesktopType.IDE) {
if (onlyModules == null) {
provisionOfficeDesktop()
provisionIdeDesktop()
provisionOfficeDesktop()
provisionIdeDesktop()
verifyIdeSetup()
}
}
internal fun Prov.provisionOnlyModules(
desktopType: DesktopType = DesktopType.BASIC,
onlyModules: List<String>
) = task {
if (FIREFOX.isIn(onlyModules)) {
installPpaFirefox()
}
if (VERIFY.isIn(onlyModules)) {
if (desktopType == DesktopType.OFFICE) {
verifyOfficeSetup()
} else if (desktopType == DesktopType.IDE) {
verifyIdeSetup()
} else {
provisionIdeDesktop(onlyModules)
}
}
}
fun Prov.validatePrecondition() {
if (!currentUserCanSudo()) {
if (!currentUserCanSudoWithoutPassword()) {
throw Exception("Current user ${whoami()} cannot execute sudo without entering a password! This is necessary to execute provisionDesktop")
}
}
fun Prov.provisionIdeDesktop(onlyModules: List<String>? = null) {
if (onlyModules == null) {
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
// DevEnvs
installDocker()
aptInstall(JAVA)
aptInstall(CLOJURE_TOOLS)
installShadowCljs()
installDevOps()
provisionPython()
// IDEs
installVSC("python", "clojure")
installIntelliJ()
} else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) {
verifyIdeSetup()
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
installFirefox()
}
}
@Suppress("unused") // used in other projects
fun Prov.provisionMSDesktop(onlyModules: List<String>?) {
if (onlyModules == null) {
installMsTeams()
} else if (onlyModules.contains(DesktopOnlyModule.TEAMS.name.lowercase())) {
installMsTeams()
}
}
fun Prov.provisionOfficeDesktop(onlyModules: List<String>? = null) {
if (onlyModules == null) {
aptInstall(ZIP_UTILS)
aptInstall(BROWSER)
aptInstall(EMAIL_CLIENT)
installDeltaChat()
aptInstall(OFFICE_SUITE)
installZimWiki()
installNextcloudClient()
// optional as installation of these tools often fail and they are not considered mandatory
optional {
aptInstall(DRAWING_TOOLS)
}
aptInstall(SPELLCHECKING_DE)
} else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) {
verifyOfficeSetup()
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
installFirefox()
}
}
fun Prov.provisionBasicDesktop(
gpg: KeyPair?,
ssh: SshKeyPair?,
gitUserName: String?,
gitEmail: String?,
onlyModules: List<String>?
) {
if (onlyModules == null) {
aptInstall(KEY_MANAGEMENT)
aptInstall(VERSION_MANAGEMENT)
aptInstall(NETWORK_TOOLS)
aptInstall(SCREEN_TOOLS)
aptInstall(KEY_MANAGEMENT_GUI)
aptInstall(PASSWORD_TOOLS)
aptInstall(OS_ANALYSIS)
aptInstall(BASH_UTILS)
aptInstall(CLIP_TOOLS)
aptPurge(
"remove-power-management xfce4-power-manager " +
"xfce4-power-manager-plugins xfce4-power-manager-data"
)
aptPurge("abiword gnumeric")
aptPurge("popularity-contest")
aptInstall(KEY_MANAGEMENT)
aptInstall(VERSION_MANAGEMENT)
aptInstall(NETWORK_TOOLS)
aptInstall(SCREEN_TOOLS)
aptInstall(KEY_MANAGEMENT_GUI)
aptInstall(PASSWORD_TOOLS)
aptInstall(OS_ANALYSIS)
aptInstall(BASH_UTILS)
aptInstall(CLIP_TOOLS)
aptPurge(
"remove-power-management xfce4-power-manager " +
"xfce4-power-manager-plugins xfce4-power-manager-data" +
"upower libimobiledevice6 libplist3 libusbmuxd6 usbmuxd bluez-cups"
)
aptPurge("abiword gnumeric")
aptPurge("popularity-contest")
provisionKeys(gpg, ssh)
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
provisionKeys(gpg, ssh)
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
installFirefox()
installGopass()
installGopassBridgeJsonApi()
downloadGopassBridge()
installRedshift()
installPpaFirefox()
installGopass()
configureGopass(publicGpgKey = gpg?.publicKey)
installGopassJsonApi()
downloadGopassBridge()
configureRedshift()
configureNoSwappiness()
configureBash()
installVirtualBoxGuestAdditions()
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
installFirefox()
installRedshift()
configureRedshift()
configureNoSwappiness()
configureBash()
installVirtualBoxGuestAdditions()
}
fun Prov.provisionOfficeDesktop() {
aptInstall(ZIP_UTILS)
aptInstall(SPELLCHECKING_DE)
aptInstall(BROWSER)
aptInstall(EMAIL_CLIENT)
installDeltaChat()
aptInstall(OFFICE_SUITE)
installZimWiki()
// installNextcloudClient() might not install - might need fix and working test
aptInstall(COMPARE_TOOLS)
// VSCode is also required in office VM (not only in IDE desktop) e.g. as editor
installVSCode("python", "clojure")
// optional, as installation of these tools often fail and as they are not mandatory
optional {
aptInstall(DRAWING_TOOLS)
}
}
fun Prov.provisionIdeDesktop() {
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
// DevEnvs
installDocker()
aptInstall(JAVA)
aptInstall(CLOJURE_TOOLS)
installShadowCljs()
installDevOps()
provisionPython()
installHugoByDeb()
// IDEs
installIntelliJ()
installKubeconform()
}

View file

@ -0,0 +1,47 @@
package org.domaindrivenarchitecture.provs.desktop.domain
typealias HostKey = String
/**
* Represents a known host for ssh connections.
*
* @param hostName domain name or ip
* @param port (optional) to be specified if different from default port 22
* @param hostKeys list of keys, where each should contain separated by space: 1. keytype, 2. key and 3. (optionally) a comment
*
* See: https://man7.org/linux/man-pages/man8/sshd.8.html#SSH_KNOWN_HOSTS_FILE_FORMAT
*/
open class KnownHost(
val hostName: String,
val port: Int? = null,
val hostKeys: List<HostKey>
) {
constructor(hostName: String, hostKeys: List<HostKey>) : this(hostName, null, hostKeys)
companion object {
val GITHUB = KnownHost(
"github.com",
listOf(
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl",
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=",
)
)
val GITLAB = KnownHost(
"gitlab.com",
listOf(
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf",
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9",
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=",
)
)
@JvmStatic
protected val values = listOf(GITHUB, GITLAB)
fun values(): List<KnownHost> {
return values
}
}
}

View file

@ -0,0 +1,11 @@
package org.domaindrivenarchitecture.provs.desktop.domain
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.addKnownHost
fun Prov.addKnownHosts(knownHosts: List<KnownHost> = KnownHost.values()) = task {
for (knownHost in knownHosts) {
addKnownHost(knownHost, verifyKeys = true)
}
}

View file

@ -11,8 +11,8 @@ import java.io.FileWriter
* Returns DesktopConfig; data for config is read from specified file.
* Throws exceptions FileNotFoundException, SerializationException if file is not found resp. cannot be parsed.
*/
internal fun getConfig(filename: String = "desktop-config.yaml"): DesktopConfig = readFromFile(filename).yamlToType()
fun getConfig(filename: String): DesktopConfig = readFromFile(filename).yamlToType()
@Suppress("unused")
internal fun writeConfig(config: DesktopConfig, fileName: String = "desktop-config.yaml") = FileWriter(fileName).use { it.write(config.toYaml()) }
fun writeConfig(config: DesktopConfig, fileName: String = "desktop-config.yaml") = FileWriter(fileName).use { it.write(config.toYaml()) }

View file

@ -7,21 +7,21 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInsta
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
private const val resourcePath = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
private const val RESOURCE_PATH = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
private const val KUBE_CONFIG_CONTEXT_SCRIPT = ".bashrc.d/kubectl.sh"
fun Prov.installDevOps() = task {
installTerraform()
installKubectlAndTools()
installYq()
installAwsCredentials()
installDevOpsFolder()
installGraalVM()
}
fun Prov.installYq(
version: String = "4.13.2",
sha256sum: String = "d7c89543d1437bf80fee6237eadc608d1b121c21a7cbbe79057d5086d74f8d79"
): ProvResult = task {
) = task {
val path = "/usr/bin/"
val filename = "yq"
if (!checkFile(path + filename)) {
@ -38,45 +38,85 @@ fun Prov.installYq(
}
}
fun Prov.installKubectlAndTools(): ProvResult = task {
fun Prov.installKubectlAndTools() = task {
task("installKubectl") {
val kubeConfigFile = ".bashrc.d/kubectl.sh"
if (!checkFile(kubeConfigFile)) {
// prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
cmd("sudo apt-get update")
aptInstall("apt-transport-https ca-certificates curl")
cmd("sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg")
cmd("echo \"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main\" | sudo tee /etc/apt/sources.list.d/kubernetes.list")
// kubectl and bash completion
cmd("sudo apt update")
aptInstall("kubectl")
cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true)
createDir(".bashrc.d")
createFileFromResource(kubeConfigFile, "kubectl.sh", resourcePath)
if (!checkFile(KUBE_CONFIG_CONTEXT_SCRIPT)) {
installKubectl()
configureKubectlBashCompletion()
} else {
ProvResult(true, out = "Kubectl already installed")
}
}
task("installKubeconform") {
installKubeconform()
}
installDevopsScripts()
}
fun Prov.installDevopsScripts() {
fun Prov.installKubeconform() = task {
// check for latest stable release on: https://github.com/yannh/kubeconform/releases
val version = "0.6.4"
val installationPath = "/usr/local/bin/"
val tmpDir = "~/tmp"
val filename = "kubeconform-linux-amd64"
val packedFilename = "$filename.tar.gz"
if ( !chk("kubeconform -v") || "v$version" != cmd("kubeconform -v").out?.trim() ) {
downloadFromURL(
"https://github.com/yannh/kubeconform/releases/download/v$version/$packedFilename",
path = tmpDir,
sha256sum = "2b4ebeaa4d5ac4843cf8f7b7e66a8874252b6b71bc7cbfc4ef1cbf85acec7c07"
)
cmd("sudo tar -xzf $packedFilename -C $installationPath", tmpDir)
} else {
ProvResult(true, out = "Kubeconform $version already installed")
}
}
fun Prov.installKubectl() = task {
// see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
val kubectlVersion = "1.27.4"
val tmpDir = "~/tmp"
// prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
optional {
cmd("sudo apt-get update")
}
aptInstall("apt-transport-https ca-certificates curl")
createDir(tmpDir)
downloadFromURL(
"https://dl.k8s.io/release/v$kubectlVersion/bin/linux/amd64/kubectl",
path = tmpDir,
// from https://dl.k8s.io/v1.27.4/bin/linux/amd64/kubectl.sha256
sha256sum = "4685bfcf732260f72fce58379e812e091557ef1dfc1bc8084226c7891dd6028f"
)
cmd("sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl", dir = tmpDir)
}
fun Prov.configureKubectlBashCompletion() = task {
cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true)
createDir(".bashrc.d")
createFileFromResource(KUBE_CONFIG_CONTEXT_SCRIPT, "kubectl.sh", RESOURCE_PATH)
}
fun Prov.installDevopsScripts() = task {
task("install ssh helper") {
createFileFromResource(
"/usr/local/bin/sshu.sh",
"sshu.sh",
resourcePath,
RESOURCE_PATH,
"555",
sudo = true
)
createFileFromResource(
"/usr/local/bin/ssht.sh",
"ssht.sh",
resourcePath,
RESOURCE_PATH,
"555",
sudo = true
)
@ -87,7 +127,7 @@ fun Prov.installDevopsScripts() {
createFileFromResource(
k3sContextFile,
"k3s-create-context.sh",
resourcePath,
RESOURCE_PATH,
"555",
sudo = true
)
@ -98,13 +138,14 @@ fun Prov.installDevopsScripts() {
createFileFromResource(
k3sConnectFile,
"k3s-connect.sh",
resourcePath,
RESOURCE_PATH,
"555",
sudo = true
)
}
}
fun Prov.installTerraform(): ProvResult = task {
fun Prov.installTerraform() = task {
val dir = "/usr/lib/tfenv/"
if (!checkDir(dir)) {
@ -114,47 +155,6 @@ fun Prov.installTerraform(): ProvResult = task {
cmd("ln -s " + dir + "bin/* /usr/local/bin", sudo = true)
}
cmd("tfenv install", sudo = true)
cmd("tfenv install latest:^1.0.8", sudo = true)
cmd("tfenv use latest:^1.0.8", sudo = true)
}
// -------------------------------------------- AWS credentials file -----------------------------------------------
fun Prov.installAwsCredentials(id: String = "REPLACE_WITH_YOUR_ID", key: String = "REPLACE_WITH_YOUR_KEY"): ProvResult =
task {
val dir = "~/.aws"
if (!checkDir(dir)) {
createDirs(dir)
createFile("~/.aws/config", awsConfig())
createFile("~/.aws/credentials", awsCredentials(id, key))
} else {
ProvResult(true, "aws credential folder already installed")
}
}
fun awsConfig(): String {
return """
[default]
region = eu-central-1
output = json
""".trimIndent()
}
fun awsCredentials(id: String, key: String): String {
return """
[default]
aws_access_key_id = $id
aws_secret_access_key = $key
""".trimIndent()
}
fun Prov.installDevOpsFolder(): ProvResult = task {
val dir = "~/.devops/"
if (!checkDir(dir)) {
createDirs(dir)
}
cmd("tfenv install latest:^1.4.6", sudo = true)
cmd("tfenv use latest:^1.4.6", sudo = true)
}

View file

@ -1,37 +1,61 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.addTextToFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import java.io.File
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstallFromPpa
/**
* Installs non-snap firefox, removing a firefox snap-installation if existing
* Installs ppa firefox (i.e. non-snap), removing snap-firefox if existing.
*/
fun Prov.installFirefox() = task {
fun Prov.installPpaFirefox() = taskWithResult {
// inspired by: https://www.omgubuntu.co.uk/2022/04/how-to-install-firefox-deb-apt-ubuntu-22-04
// inspired by: https://wiki.ubuntuusers.de/Firefox/Installation/PPA/
if (chk("snap list | grep firefox")) {
val unattendeUpgradesForPpaFirefox = "/etc/apt/apt.conf.d/51unattended-upgrades-firefox"
val preCondition = checkFile(unattendeUpgradesForPpaFirefox)
if (preCondition) {
return@taskWithResult ProvResult(true, out = "Firefox already installed with ppa")
}
cmd("sudo apt-get -qy remove firefox", sudo = true)
optional("remove snap firefox") {
cmd("snap remove firefox", sudo = true)
}
aptInstall("software-properties-common")
cmd("add-apt-repository -y ppa:mozillateam/ppa", sudo = true)
addTextToFile(
"\nPackage: *\n" +
"Pin: release o=LP-PPA-mozillateam\n" +
"Pin-Priority: 1001\n",
File("/etc/apt/preferences.d/mozilla-firefox"),
createFile("/etc/apt/preferences.d/mozillateam", mozillaTeamFileContent, sudo = true)
aptInstallFromPpa("mozillateam", "ppa", "firefox")
createFile(
unattendeUpgradesForPpaFirefox,
"Unattended-Upgrade::Allowed-Origins:: \"LP-PPA-mozillateam:\${distro_codename}\";\n",
sudo = true
)
}
addTextToFile(
"""Unattended-Upgrade::Allowed-Origins:: "LP-PPA-mozillateam:${'$'}{distro_codename}";""",
File("/etc/apt/preferences.d/mozilla-firefox"),
sudo = true
)
aptInstall("firefox")
}
private val mozillaTeamFileContent = """
Package: *
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 100
Package: firefox*
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001
Package: firefox*
Pin: release o=Ubuntu
Pin-Priority: -1
Package: thunderbird*
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001
Package: thunderbird*
Pin: release o=Ubuntu
Pin-Priority: -1
""".trimIndent()

View file

@ -2,16 +2,19 @@ package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
fun Prov.installGopass(
version: String = "1.12.7",
version: String = "1.15.13", // NOTE: when adjusting, pls also adjust checksum below and version of gopass bridge json api
enforceVersion: Boolean = false,
sha256sum: String = "0824d5110ff1e68bff1ba10c1be63acb67cb1ad8e3bccddd6b6fc989608beca8" // checksum for sha256sum version 8.30 (e.g. ubuntu 20.04)
// from https://github.com/gopasspw/gopass/releases/tag/v1.15.13
sha256sum: String = "409ed5617e64fa2c781d5e2807ba7fcd65bc383a4e110f410f90b590e51aec55"
) = taskWithResult {
if (isPackageInstalled("gopass") && !enforceVersion) {
@ -34,29 +37,34 @@ fun Prov.installGopass(
if (result.success) {
cmd("sudo dpkg -i $path/gopass_${version}_linux_amd64.deb")
// Cross-check if installation was successful
addResultToEval(ProvResult(checkGopassVersion(version)))
return@taskWithResult ProvResult(checkGopassVersion(version))
} else {
addResultToEval(ProvResult(false, err = "Gopass could not be installed. " + result.err))
return@taskWithResult ProvResult(false, err = "Gopass could not be installed. " + result.err)
}
}
fun Prov.configureGopass(gopassRootFolder: String? = null) = taskWithResult() {
val configFile = ".config/gopass/config.yml"
val defaultRootFolder = userHome() + ".password-store"
val rootFolder = gopassRootFolder ?: defaultRootFolder
fun Prov.configureGopass(gopassRootFolder: String? = null, publicGpgKey: Secret? = null) = taskWithResult {
if (checkFile(configFile)) {
val configFile = ".config/gopass/config"
if ((gopassRootFolder != null) && (!gopassRootFolder.startsWith("/"))) {
return@taskWithResult ProvResult(false, err = "Gopass cannot be initialized with a relative path or path starting with ~ ($gopassRootFolder)")
}
if(!fileContainsText(configFile,"share/gopass/stores/root")){
return@taskWithResult ProvResult(true, out = "Gopass already configured in file $configFile")
}
if ((gopassRootFolder != null) && (!gopassRootFolder.startsWith("/"))) {
return@taskWithResult ProvResult(false, err = "Gopass cannot be initialized with a relative path or path starting with ~")
}
// use default
createDir(rootFolder)
val defaultRootFolder = userHome() + ".password-store"
val gopassRoot = gopassRootFolder ?: defaultRootFolder
// initialize root store
val fingerprint = publicGpgKey?.let { gpgFingerprint(it.plain()) }
gopassInitStoreFolder(gopassRoot, fingerprint)
createDirs(".config/gopass")
createFile(configFile, gopassConfig(rootFolder))
createFile(configFile, gopassConfig(gopassRoot))
// auto-completion
configureBashForUser()
@ -64,31 +72,41 @@ fun Prov.configureGopass(gopassRootFolder: String? = null) = taskWithResult() {
}
fun Prov.gopassMountStore(storeName: String, path: String) = task {
cmd("gopass mounts add $storeName $path")
fun Prov.gopassMountStore(storeName: String, path: String) = taskWithResult {
val mounts = cmdNoEval("gopass mounts").out ?: return@taskWithResult ProvResult(false, err = "could not determine gopass mounts")
if (mounts.contains(storeName)) {
ProvResult(true, out = "Store $storeName already mounted.")
} else {
cmd("gopass mounts add $storeName $path")
}
}
@Suppress("unused")
fun Prov.gopassInitStore(storeName: String, indexOfRecepientKey: Int = 0) = task {
cmd("printf \"$indexOfRecepientKey\\n\" | gopass init --store=$storeName")
fun Prov.gopassInitStoreFolder(path: String, gpgFingerprint: String? = null ) = task {
createFile("$path/.gpg-id", gpgFingerprint ?: "_replace_this_by_a_fingerprint_of_a_public_gpg_key_")
if (!checkDir(".git", path)) {
cmd("git init", path)
}
}
internal fun gopassConfig(gopassRoot: String): String {
return """
autoclip: true
autoimport: true
cliptimeout: 45
exportkeys: true
nocolor: false
nopager: false
notifications: true
parsing: true
path: $gopassRoot
safecontent: false
mounts: {}
""".trimIndent() + "\n"
[core]
parsing = true
exportkeys = true
autoclip = true
showsafecontent = false
nopager = false
cliptimeout = 45
notifications = true
autoimport = true
[age]
usekeychain = false
[mounts]
path = $gopassRoot
"""
.trimIndent() + "\n"
}
@ -105,4 +123,4 @@ internal fun Prov.checkGopassVersion(version: String): Boolean {
internal fun Prov.gopassVersion(): String? {
val result = cmdNoEval("gopass -v")
return if (!result.success) null else result.out
}
}

View file

@ -6,12 +6,11 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
import java.io.File
fun Prov.downloadGopassBridge() = task {
val version = "0.9.0"
val filename = "gopass_bridge-${version}-fx.xpi"
// Attention: when changing the version, you also need to change the number after /file/ in the download url below
val filename = "gopass_bridge-0.9.0-fx.xpi"
val downloadDir = "${userHome()}Downloads/"
createDirs(downloadDir)
@ -19,13 +18,14 @@ fun Prov.downloadGopassBridge() = task {
"-L https://addons.mozilla.org/firefox/downloads/file/3630534/$filename",
downloadDir + filename
)
// needs manual installation with: firefox Downloads/gopass_bridge-0.8.0-fx.xpi
// needs manual installation with: firefox Downloads/gopass_bridge-0.X.0-fx.xpi
}
fun Prov.installGopassBridgeJsonApi() = task {
// see https://github.com/gopasspw/gopass-jsonapi
val gopassJsonApiVersion = "1.11.1"
val requiredGopassVersion = "1.14.4"
fun Prov.installGopassJsonApi() = taskWithResult {
// from https://github.com/gopasspw/gopass-jsonapi/releases/tag/v1.15.13
val sha256sum = "3162ab558301645024325ce2e419c1d67900e1faf95dc1774a36f1ebfc76389f"
val gopassJsonApiVersion = "1.15.13"
val requiredGopassVersion = "1.15.13"
val filename = "gopass-jsonapi_${gopassJsonApiVersion}_linux_amd64.deb"
val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassJsonApiVersion/$filename"
val downloadDir = "${userHome()}Downloads"
@ -36,7 +36,7 @@ fun Prov.installGopassBridgeJsonApi() = task {
if (checkGopassVersion(requiredGopassVersion)) {
aptInstall("git gnupg2") // required dependencies
createDir(downloadDir)
downloadFromURL(downloadUrl, filename, downloadDir)
downloadFromURL(downloadUrl, filename, downloadDir, sha256sum = sha256sum)
cmd("dpkg -i $downloadDir/$filename", sudo = true)
} else {
ProvResult(
@ -46,51 +46,56 @@ fun Prov.installGopassBridgeJsonApi() = task {
)
}
} else {
addResultToEval(
ProvResult(
false,
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
)
ProvResult(
false,
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
)
}
} else {
if (installedJsonApiVersion.startsWith("gopass-jsonapi version $gopassJsonApiVersion")) {
addResultToEval(ProvResult(true, out = "Version $gopassJsonApiVersion of gopass-jsonapi is already installed"))
ProvResult(true, out = "Version $gopassJsonApiVersion of gopass-jsonapi is already installed")
} else {
addResultToEval(
ProvResult(
false,
err = "gopass-jsonapi (version $gopassJsonApiVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
" Upgrading gopass-jsonapi is currently not supported by provs."
)
ProvResult(
false,
err = "gopass-jsonapi (version $gopassJsonApiVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
" Upgrading gopass-jsonapi is currently not supported by provs."
)
}
}
}
fun Prov.configureGopassWrapperShForFirefox() = task {
/**
* Configures apparmor to allow firefox to access to gopass_wrapper.sh in avoid
* the error "An unexpected error occurred - Is your browser correctly set up for gopass? ..."
* when trying to use gopass bridge.
* This error appears in spite of having already set up gopass-jsonapi correctly.
*/
fun Prov.configureApparmorForGopassWrapperShForFirefox() = task {
val appArmorFile = "/etc/apparmor.d/usr.bin.firefox"
val gopassAccessPermission = "owner @{HOME}/.config/gopass/gopass_wrapper.sh Ux,"
val insertAfterText = "# per-user firefox configuration\n"
if (checkFile(appArmorFile)) {
addTextToFile(
"\nowner @{HOME}/.config/gopass/gopass_wrapper.sh Ux\n",
File(appArmorFile),
sudo = true
if (checkFile(appArmorFile) && !fileContainsText(appArmorFile, gopassAccessPermission, true)) {
replaceTextInFile(
appArmorFile, insertAfterText, "$insertAfterText $gopassAccessPermission\n"
)
cmd("systemctl reload apparmor", sudo = true)
}
cmd("systemctl reload apparmor", sudo = true)
}
fun Prov.configureGopassBridgeJsonApi() = task {
fun Prov.configureGopassJsonApi() = taskWithResult {
if (isPackageInstalled("gopass-jsonapi")) {
// configure for firefox and choose default for each:
// "Install for all users? [y/N/q]",
// "In which path should gopass_wrapper.sh be installed? [/home/testuser/.config/gopass]"
// "Wrapper Script for gopass_wrapper.sh ..."
configureGopassWrapperShForFirefox()
// configures gopass-jsonapi for firefox and chooses default for each:
// * "Install for all users? [y/N/q]",
// * "In which path should gopass_wrapper.sh be installed? [/home/<user>/.config/gopass]"
// * "Wrapper Script for gopass_wrapper.sh ..."
//
// I.e. creates file "gopass_wrapper.sh" in "/home/<user>/.config/gopass" as well as
// the manifest file "/home/<user>/.mozilla/native-messaging-hosts/com.justwatch.gopass.json"
cmd("printf \"\\n\\n\\n\" | gopass-jsonapi configure --browser firefox")
configureApparmorForGopassWrapperShForFirefox()
} else {
ProvResult(
false,

View file

@ -0,0 +1,33 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
const val GRAAL_VM_VERSION = "21.0.2"
fun Prov.installGraalVM() = task {
val tmpDir = "~/tmp"
val filename = "graalvm-community-jdk-"
val additionalPartFilename = "_linux-x64_bin"
val packedFilename = "$filename$GRAAL_VM_VERSION$additionalPartFilename.tar.gz"
val extractedFilenameHunch = "graalvm-community-openjdk-"
val installationPath = "/usr/lib/jvm/"
if ( GRAAL_VM_VERSION != graalVMVersion() || !chk("ls -d $installationPath$extractedFilenameHunch$GRAAL_VM_VERSION*")) {
downloadFromURL(
"https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-$GRAAL_VM_VERSION/$packedFilename",
path = tmpDir,
sha256sum = "b048069aaa3a99b84f5b957b162cc181a32a4330cbc35402766363c5be76ae48"
)
createDirs(installationPath, sudo = true)
cmd("sudo tar -C $installationPath -xzf $packedFilename", tmpDir)
val graalInstPath = installationPath + (cmd("ls /usr/lib/jvm/|grep -e graalvm-community-openjdk-$GRAAL_VM_VERSION").out?.replace("\n", ""))
cmd("sudo ln -sf $graalInstPath/lib/svm/bin/native-image /usr/local/bin/native-image")
}
}
fun Prov.graalVMVersion(): String {
return cmdNoEval("/usr/local/bin/native-image --version|awk 'NR==1 {print $2}'").out?.trim() ?: ""
}

View file

@ -0,0 +1,85 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.userHome
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptPurge
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
fun Prov.installHugoByDeb() = task {
val sha256sum = "46692ac9b79d5bc01b0f847f6dcf651d8630476de63e598ef61a8da9461d45cd"
val requiredHugoVersion = "0.125.5"
val filename = "hugo_extended_0.125.5_linux-amd64.deb"
val downloadUrl = "-L https://github.com/gohugoio/hugo/releases/download/v$requiredHugoVersion/$filename"
val downloadDir = "${userHome()}Downloads"
val currentHugoVersion = cmdNoEval("hugo version").out ?: ""
if (needsHugoInstall(currentHugoVersion, requiredHugoVersion)) {
if (isHugoInstalled(currentHugoVersion)) {
if (currentHugoVersion.contains("snap")) {
cmd("snap remove hugo", sudo = true)
} else {
aptPurge("hugo")
}
}
aptInstall("gnupg2")
downloadFromURL(downloadUrl, filename, downloadDir, sha256sum = sha256sum)
cmd("dpkg -i $downloadDir/$filename", sudo = true)
}
}
fun needsHugoInstall(currentHugoVersion: String?, requiredHugoVersion: String) : Boolean {
if (currentHugoVersion == null) {
return true
}
if (!isHugoInstalled(currentHugoVersion)) {
return true
}
if (!isHugoExtended(currentHugoVersion)) {
return true
}
if (isLowerHugoVersion(requiredHugoVersion, currentHugoVersion)) {
return true
}
return false
}
fun isHugoInstalled(hugoVersion: String?) : Boolean {
if (hugoVersion == null) {
return false
}
return hugoVersion.contains("hugo")
}
fun isHugoExtended(hugoVersion: String) : Boolean {
return hugoVersion.contains("extended")
}
fun isLowerHugoVersion(requiredHugoVersion: String, currentHugoVersion: String ) : Boolean {
val reqVersionNo = getHugoVersionNo(requiredHugoVersion)
val currentVersionNo = getHugoVersionNo(currentHugoVersion)
return when {
compareVersions(currentVersionNo, reqVersionNo).contains("lower") -> true
else -> false
}
}
fun compareVersions(firstVersion : List<Int>, secondVersion: List<Int>) : String {
var result = ""
for (i in 0..2) {
when {
firstVersion[i] > secondVersion[i] -> result += " higher"
firstVersion[i] < secondVersion[i] -> result += " lower"
firstVersion[i] == secondVersion[i] -> result += " equal"
}
}
return result
}
fun getHugoVersionNo(hugoVersion: String) : List<Int> {
// hugo v0.126.1-3d40ab+extended linux/amd64 BuildDate=2024-05-15T10:42:34Z VendorInfo=snap:0.126.1
var result = hugoVersion.split(" ")[1]
result = result.split("-")[0].removePrefix("v")
return result.split(".").map { it.toInt() }
}

View file

@ -1,12 +0,0 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
fun Prov.installMsTeams() = task {
aptInstall("curl gnupg2")
cmd("curl -sSL https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -")
cmd("sudo sh -c 'echo \"deb [arch=amd64] https://packages.microsoft.com/repos/ms-teams stable main\" > /etc/apt/sources.list.d/teams.list'")
cmd("sudo apt-get update") // apt needs update
aptInstall("teams")
}

View file

@ -8,7 +8,7 @@ val NETWORK_TOOLS = "curl wget net-tools"
val KEY_MANAGEMENT_GUI = "seahorse"
val BROWSER = "firefox chromium-browser"
val BROWSER = "chromium-browser" // firefox can be installed by installFirefox
val EMAIL_CLIENT = "thunderbird"
@ -30,7 +30,7 @@ val OPENCONNECT = "openconnect network-manager-openconnect network-manager-openc
val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
val JAVA = "openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk jarwrapper"
val JAVA = "openjdk-17-jdk jarwrapper"
val DRAWING_TOOLS = "inkscape dia openboard graphviz"
@ -38,4 +38,6 @@ val CLOJURE_TOOLS = "leiningen"
val PASSWORD_TOOLS = "pwgen"
val SCREEN_TOOLS = "scrcpy"
val SCREEN_TOOLS = "scrcpy"
val COMPARE_TOOLS = "meld"

View file

@ -7,35 +7,62 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInsta
import java.io.File
fun Prov.provisionPython() = task {
fun Prov.provisionPython(venvHome: String? = "~/.venv/meissa") = task {
installPython3()
configureVenv()
installPybuilder()
installRestClient()
installJupyterlab()
if (venvHome != null) { configureVenv(venvHome) }
installPybuilder(venvHome)
installRestClient(venvHome)
installJupyterlab(venvHome)
installLinters(venvHome)
installAsciinema(venvHome)
installPyTest(venvHome)
}
fun Prov.installPython3(): ProvResult = task {
aptInstall("python3-venv python3-pip")
}
fun Prov.configureVenv(): ProvResult = task {
val venvHome = "~/.venv/meissa"
cmd("python3 -m venv " + venvHome)
cmd("source " + venvHome + "/bin/activate")
createSymlink(File(venvHome + "/bin/activate"), File("~/.bashrc.d/venv.sh"))
cmd("pip3 install pip --upgrade")
fun Prov.configureVenv(venvHome: String): ProvResult = task {
cmd("python3 -m venv $venvHome")
createSymlink(File("$venvHome/bin/activate"), File("~/.bashrc.d/venv.sh"))
pipInstall("pip --upgrade", venvHome)
}
fun Prov.installPybuilder(): ProvResult = task {
cmd("pip3 install pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation python_terraform " +
"boto3")
fun Prov.installPybuilder(venvHome: String? = null): ProvResult = task {
pipInstall("pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation" +
" python_terraform dda_python_terraform boto3 pyyaml packaging inflection",
venvHome
)
pipInstall("--upgrade ddadevops", venvHome)
}
fun Prov.installRestClient(): ProvResult = task {
cmd("pip3 install requests")
fun Prov.installRestClient(venvHome: String? = null): ProvResult = task {
pipInstall("requests", venvHome)
}
fun Prov.installJupyterlab(): ProvResult = task {
cmd("pip3 install jupyterlab pandas matplotlib")
fun Prov.installJupyterlab(venvHome: String? = null): ProvResult = task {
pipInstall("jupyterlab pandas matplotlib", venvHome)
}
fun Prov.installLinters(venvHome: String? = null): ProvResult = task {
pipInstall("flake8 mypy pylint", venvHome)
}
fun Prov.installAsciinema(venvHome: String? = null): ProvResult = task {
pipInstall("asciinema", venvHome)
}
fun Prov.installPyTest(venvHome: String? = null): ProvResult = task {
pipInstall("pytest", venvHome)
}
private fun Prov.pipInstall(pkg: String, venvHome: String? = null) {
cmd(activateVenvCommandPrefix(venvHome) + "pip3 install $pkg")
}
private fun activateVenvCommandPrefix(venvHome: String?): String {
return if (venvHome == null) {
""
} else {
"source $venvHome/bin/activate && "
}
}

View file

@ -6,36 +6,33 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInsta
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
fun Prov.installVSC(vararg options: String) = task {
val clojureExtensions =
arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint")
val pythonExtensions = arrayListOf("ms-python.python")
fun Prov.installVSCode(vararg options: String) = task {
val clojureExtensions = setOf("betterthantomorrow.calva", "DavidAnson.vscode-markdownlint")
val pythonExtensions = setOf("ms-python.python")
prerequisitesVSCinstall()
installVSCodePrerequisites()
installVSCPackage()
installVSCodiumPackage()
if (options.contains("clojure")) {
installExtensionsCode(clojureExtensions)
installExtensionsCodium(clojureExtensions)
installVSCodeExtensions(clojureExtensions)
installVSCodiumExtensions(clojureExtensions)
}
if (options.contains("python")) {
installExtensionsCode(pythonExtensions)
installExtensionsCodium(pythonExtensions)
installVSCodeExtensions(pythonExtensions)
installVSCodiumExtensions(pythonExtensions)
}
provisionAdditionalToolsForVSCode()
}
private fun Prov.prerequisitesVSCinstall() = task {
private fun Prov.installVSCodePrerequisites() = task {
aptInstall("curl gpg unzip apt-transport-https")
}
@Suppress("unused") // only required for installation of vscode via apt
private fun Prov.installVscWithApt() = task {
private fun Prov.installVSCodeWithApt() = task {
val packageName = "code"
if (!isPackageInstalled(packageName)) {
// see https://code.visualstudio.com/docs/setup/linux
@ -65,7 +62,7 @@ private fun Prov.installVSCodiumPackage() = task {
}
private fun Prov.installExtensionsCode(extensions: List<String>) = optional {
private fun Prov.installVSCodeExtensions(extensions: Set<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = cmd("code --install-extension $ext")
@ -74,20 +71,11 @@ private fun Prov.installExtensionsCode(extensions: List<String>) = optional {
// Settings can be found at $HOME/.config/Code/User/settings.json
}
private fun Prov.installExtensionsCodium(extensions: List<String>) = optional {
private fun Prov.installVSCodiumExtensions(extensions: Set<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = cmd("codium --install-extension $ext")
res = ProvResult(res.success && cmd("codium --install-extension $ext").success)
}
res
// Settings can be found at $HOME/.config/Code/User/settings.json
}
internal fun Prov.provisionAdditionalToolsForVSCode() = task {
// Joker
val version = "0.18.0"
cmd("curl -Lo joker-${version}-linux-amd64.zip https://github.com/candid82/joker/releases/download/v${version}/joker-${version}-linux-amd64.zip")
cmd("unzip joker-${version}-linux-amd64.zip")
cmd("sudo mv joker /usr/local/bin/")
// Settings can be found at $HOME/.config/VSCodium/User/settings.json
}

View file

@ -69,46 +69,65 @@ open class Prov protected constructor(
private val infoTexts = arrayListOf<String>()
/**
* A task is the base execution unit in provs. In the results overview it is represented by one line resp. result (of either success or failure).
* A session is the top-level execution unit in provs. A session can contain tasks.
* Returns success if no sub-tasks are called or if all subtasks finish with success.
*/
fun session(taskLambda: Prov.() -> ProvResult): ProvResult {
if (level > 0) {
throw IllegalStateException("A session can only be created on the top-level and may not be included in another session or task.")
}
return evaluate(ResultMode.ALL, "session") { taskLambda() }
}
/**
* A task is the fundamental execution unit. In the results overview it is represented by one line with a success or failure result.
* Returns success if all sub-tasks finished with success or if no sub-tasks are called at all.
*/
fun task(name: String? = null, taskLambda: Prov.() -> Unit): ProvResult {
printDeprecationWarningIfLevel0("task")
return evaluate(ResultMode.ALL, name) { taskLambda(); ProvResult(true) }
}
/**
* Same as task but the provided lambda is explicitly required to provide a ProvResult to be returned.
* The returned result is included in the evaluation.
* Same as task above but the lambda parameter must have a ProvResult as return type.
* The returned ProvResult is included in the success resp. failure evaluation,
* i.e. if the returned ProvResult from the lambda fails, the returned ProvResult from
* taskWithResult also fails, else success depends on potentially called sub-tasks.
*/
fun taskWithResult(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("taskWithResult")
return evaluate(ResultMode.ALL, name) { taskLambda() }
}
/**
* defines a task, which returns the returned result, the results of sub-tasks are not considered
* defines a task, which returns the returned result from the lambda, the results of sub-tasks are not considered
*/
fun requireLast(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.LAST) { a() }
fun requireLast(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("requireLast")
return evaluate(ResultMode.LAST, name) { taskLambda() }
}
/**
* defines a task, which always returns success
* Defines a task, which always returns success.
*/
fun optional(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.OPTIONAL) { a() }
fun optional(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("optional")
return evaluate(ResultMode.OPTIONAL, name) { taskLambda() }
}
/**
* defines a task, which exits the overall execution on failure
* Defines a task, which exits the overall execution on failure result of the taskLambda.
*/
fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.FAILEXIT) { a() }
fun exitOnFailure(taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("exitOnFailure")
return evaluate(ResultMode.FAILEXIT) { taskLambda() }
}
/**
* Runs the provided task in the specified (running) container
*/
fun taskInContainer(containerName: String, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("taskInContainer")
runInContainerWithName = containerName
val res = evaluate(ResultMode.ALL) { taskLambda() }
runInContainerWithName = null
@ -254,6 +273,8 @@ open class Prov protected constructor(
previousLevel = -1
exit = false
initProgress()
processor.open()
}
// pre-handling
@ -312,6 +333,15 @@ open class Prov protected constructor(
internalResults[resultIndex].provResult = returnValue
// Add failure result to output if not yet included,
// which is the case if the result was not part of another subtask but created and returned by the lambda itself.
// Success results do not need to be added here as they don't change the overall success evaluation,
// whereas the failure results may have a useful error message, which should be in the output.
// Only direct result objects are added, but not result objects that were passed from a subtask as they are already handled in the subtask.
if (!resultOfTaskLambda.success && (resultIndex < internalResults.size - 1) && (resultOfTaskLambda != internalResults[resultIndex + 1].provResult)) {
internalResults.add(ResultLine(level + 1, "<<returned result>>", resultOfTaskLambda))
}
if (level == 0) {
endProgress()
processor.close()
@ -322,8 +352,12 @@ open class Prov protected constructor(
}
/**
* Returns true if the task at the specified index has no subtasks.
* I.e. if the task is the last one or if level of the next task is the same or less (which means same level or "higher" in the tree)
*/
private fun internalResultIsLeaf(resultIndex: Int): Boolean {
return !(resultIndex < internalResults.size - 1 && internalResults[resultIndex + 1].level > internalResults[resultIndex].level)
return (resultIndex >= internalResults.size - 1 || internalResults[resultIndex].level >= internalResults[resultIndex + 1].level)
}
@ -432,6 +466,11 @@ open class Prov protected constructor(
}
}
fun printDeprecationWarningIfLevel0(methodName: String) {
if (level == 0 && progressType != ProgressType.NONE) {
println("WARNING: method $methodName should not be used at top-level, use method <session> instead.")
}
}
}

View file

@ -8,6 +8,8 @@ data class ProvResult(val success: Boolean,
val exception: Exception? = null,
val exit: String? = null) {
val outTrimmed: String? = out?.trim()
constructor(returnCode : Int) : this(returnCode == 0)
override fun toString(): String {

View file

@ -20,7 +20,7 @@ internal fun getCallingMethodName(): String? {
val offsetVal = 1
val exclude = arrayOf("task", "task\$default", "taskWithResult\$default", "taskWithResult", "def", "def\$default", "record", "invoke", "invoke0", "evaluate", "evaluate\$default", )
// suffixes are also ignored as method names but will be added as suffix in the evaluation results
val suffixes = arrayOf("optional", "requireAll", "requireLast", "inContainer")
val suffixes = arrayOf("optional", "optional\$default", "requireAll", "requireLast", "requireLast\$default", "inContainer")
var suffix = ""
val callingFrame = Thread.currentThread().stackTrace
@ -30,7 +30,7 @@ internal fun getCallingMethodName(): String? {
var inc = 0
while ((method in exclude) or (method in suffixes)) {
if (method in suffixes && suffix == "") {
suffix = method
suffix = method.split("$")[0]
}
inc++
method = callingFrame[i + offsetVal + inc].methodName

View file

@ -2,7 +2,6 @@ package org.domaindrivenarchitecture.provs.framework.core
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import java.io.BufferedReader
import java.io.File
@ -18,15 +17,13 @@ fun writeToFile(fileName: String, text: String) {
}
@OptIn(InternalSerializationApi::class)
inline fun <reified T : Any> String.yamlToType() = Yaml(configuration = YamlConfiguration(strictMode = false)).decodeFromString(
T::class.serializer(),
serializer<T>(),
this
)
@OptIn(InternalSerializationApi::class)
inline fun <reified T : Any> T.toYaml() = Yaml(configuration = YamlConfiguration(strictMode = false, encodeDefaults = false)).encodeToString(
T::class.serializer(),
serializer<T>(),
this
)

View file

@ -6,88 +6,63 @@ import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
import kotlin.system.exitProcess
/**
* Returns a Prov instance according to the targetCommand.
* E.g. it returns a local Prov instance if targetCommand.isValidLocalhost() is true or
* Returns a local Prov instance if targetCommand.isValidLocalhost() is true resp.
* returns a remote Prov instance if targetCommand.isValidRemote() is true.
*
* If the target is remote and if parameter remoteHostSetSudoWithoutPasswordRequired is set to true,
* it will enable sudo without password on the remote machine (in case this was not yet enabled).
*/
fun createProvInstance(
targetCommand: TargetCliCommand,
remoteHostSetSudoWithoutPasswordRequired: Boolean = false
): Prov {
fun createProvInstance(targetCommand: TargetCliCommand): Prov {
if (targetCommand.isValid()) {
val password: Secret? = targetCommand.remoteTarget()?.password
val remoteTarget = targetCommand.remoteTarget()
if (targetCommand.isValidLocalhost()) {
return local()
} else if (targetCommand.isValidRemote() && remoteTarget != null) {
return createProvInstanceRemote(
remoteTarget.host,
remoteTarget.user,
remoteTarget.password == null,
password,
remoteHostSetSudoWithoutPasswordRequired
)
return if (targetCommand.isValidLocalhost()) {
local()
} else if (targetCommand.isValidRemote()) {
createRemoteProvInstance(targetCommand.remoteTarget(), password)
} else {
throw IllegalArgumentException("Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help.")
throw IllegalArgumentException(
"Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help."
)
}
} else {
println("Invalid command line options.\nPlease use option -h for help.")
println("ERROR: Invalid target (${targetCommand.target}). Please use option -h for help.")
System.out.flush()
exitProcess(1)
}
}
private fun createProvInstanceRemote(
host: String,
remoteUser: String,
sshWithKey: Boolean,
password: Secret?,
remoteHostSetSudoWithoutPasswordRequired: Boolean
/**
* Wrapper for exitProcess, which allows e.g. mocking for test purposes
*/
fun quit(status: Int): Nothing {
exitProcess(status)
}
fun printProvsVersion() {
val version = object {}.javaClass.getResource("/version.txt")?.readText()?.trim()
println("\nProvs version: $version\n")
}
internal fun createRemoteProvInstance(
target: TargetCliCommand.RemoteTarget?,
password: Secret? = null
): Prov {
val prov =
if (sshWithKey) {
remote(host, remoteUser)
} else {
require(
password != null,
{ "No password available for provisioning without ssh keys. Either specify provisioning by ssh-keys or provide password." })
remote(host, remoteUser, password)
}
if (!prov.currentUserCanSudo()) {
if (remoteHostSetSudoWithoutPasswordRequired) {
require(
password != null,
{ "User ${prov.whoami()} not able to sudo on remote machine without password and no password available for the user." })
prov.makeUserSudoerWithNoSudoPasswordRequired(password)
// a new session is required after making the user a sudoer without password
return remote(host, remoteUser, password)
} else {
throw IllegalStateException("User ${prov.whoami()} not able to sudo on remote machine without password and option not set to enable user to sudo without password.")
}
return if (target != null) {
remote(target.host, target.user, target.password ?: password)
} else {
throw IllegalArgumentException(
"Error: no valid remote target (host & user) was specified!"
)
}
return prov
}
// todo: consider removal as password can be retrieved by PromptSecretSource
internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? {
var password: Secret? = null
if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) {
password =
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret()
}
return password
internal fun getPasswordToConfigureSudoWithoutPassword(): Secret {
return PromptSecretSource("password to configure sudo without password.").secret()
}

View file

@ -6,7 +6,7 @@ import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.Doc
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
import org.domaindrivenarchitecture.provs.framework.core.platforms.UbuntuProv
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
private const val DOCKER_NOT_SUPPORTED = "docker not yet supported for "
@ -17,7 +17,7 @@ fun Prov.dockerProvideImage(image: DockerImage, skipIfExisting: Boolean = true,
if (this is UbuntuProv) {
return this.dockerProvideImagePlatform(image, skipIfExisting, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}
@ -28,7 +28,7 @@ fun Prov.dockerImageExists(imageName: String, sudo: Boolean = true) : Boolean {
if (this is UbuntuProv) {
return this.dockerImageExistsPlatform(imageName, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}
@ -50,7 +50,7 @@ fun Prov.provideContainer(
if (this is UbuntuProv) {
return this.provideContainerPlatform(containerName, imageName, startMode, sudo, options, command)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}
@ -59,7 +59,7 @@ fun Prov.containerRuns(containerName: String, sudo: Boolean = true) : Boolean {
if (this is UbuntuProv) {
return this.containerRunsPlatform(containerName, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}
@ -72,7 +72,7 @@ fun Prov.runContainer(
if (this is UbuntuProv) {
return this.runContainerPlatform(containerName, imageName, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}
@ -84,16 +84,17 @@ fun Prov.exitAndRmContainer(
if (this is UbuntuProv) {
return this.exitAndRmContainerPlatform(containerName, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}
@Suppress("unused")
fun Prov.containerExec(containerName: String, cmd: String, sudo: Boolean = true): ProvResult {
if (this is UbuntuProv) {
return this.containerExecPlatform(containerName, cmd, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
}
}

View file

@ -17,12 +17,12 @@ class UbuntuPlusUser(private val userName: String = "testuser") : DockerImage {
override fun imageText(): String {
return """
FROM ubuntu:20.04
FROM ubuntu:22.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install sudo
RUN useradd -m $userName && echo "$userName:$userName" | chpasswd && adduser $userName sudo
RUN useradd -m $userName && echo "$userName:$userName" | chpasswd && usermod -aG sudo $userName
RUN echo "$userName ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/$userName
USER $userName

View file

@ -7,19 +7,12 @@ import org.domaindrivenarchitecture.provs.framework.core.processors.Processor
const val SHELL = "/bin/bash"
class UbuntuProv internal constructor(
open class UbuntuProv(
processor: Processor = LocalProcessor(),
name: String? = null,
progressType: ProgressType
progressType: ProgressType = ProgressType.BASIC
) : Prov(processor, name, progressType) {
init {
val user = cmdNoLog("whoami").out?.trim()
if ("root" != user && !cmdNoLog("timeout 1 sudo id").success) {
println("IMPORTANT INFO:\nUser $user cannot sudo without entering a password, i.e. some functions may fail!\nIf you need to run functions with sudo, please ensure $user can sudo without password.")
}
}
override fun cmd(cmd: String, dir: String?, sudo: Boolean): ProvResult = taskWithResult {
exec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo))
}
@ -37,13 +30,15 @@ class UbuntuProv internal constructor(
}
private fun buildCommand(vararg args: String): String {
return if (args.size == 1)
return if (args.size == 1) {
args[0].escapeAndEncloseByDoubleQuoteForShell()
else
if (args.size == 3 && SHELL.equals(args[0]) && "-c".equals(args[1]))
} else {
if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) {
SHELL + " -c " + args[2].escapeAndEncloseByDoubleQuoteForShell()
else
} else {
args.joinToString(separator = " ")
}
}
}
}

View file

@ -5,7 +5,6 @@ import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.escapeAndEncloseByDoubleQuoteForShell
import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL
import org.domaindrivenarchitecture.provs.framework.core.tags.Api
enum class ContainerStartMode {
USE_RUNNING_ELSE_CREATE,
@ -20,26 +19,24 @@ enum class ContainerEndMode {
open class ContainerUbuntuHostProcessor(
private val containerName: String = "default_provs_container",
@Api // suppress false positive warning
private val dockerImage: String = "ubuntu",
@Api // suppress false positive warning
private val startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
dockerImage: String = "ubuntu",
startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
private val endMode: ContainerEndMode = ContainerEndMode.KEEP_RUNNING,
@Api // suppress false positive warning
private val sudo: Boolean = true
sudo: Boolean = true,
options: String = ""
) : Processor {
private val hostShell = "/bin/bash"
private val dockerCmd = if (sudo) "sudo docker " else "docker "
private var localExecution = LocalProcessor()
private var a = Prov.newInstance(name = "LocalProcessor for Docker operations", progressType = ProgressType.NONE)
init {
val r = a.provideContainer(containerName, dockerImage, startMode, sudo)
if (!r.success)
throw RuntimeException("Could not start docker image: " + r.toString(), r.exception)
val result = a.provideContainer(containerName, dockerImage, startMode, sudo, options)
if (!result.success)
throw RuntimeException("Could not start docker image: " + result.toString(), result.exception)
}
private val hostShell = "/bin/bash"
override fun exec(vararg args: String): ProcessResult {
return localExecution.exec(hostShell, "-c", dockerCmd + "exec $containerName " + buildCommand(*args))
}
@ -57,7 +54,7 @@ open class ContainerUbuntuHostProcessor(
return s.escapeAndEncloseByDoubleQuoteForShell()
}
private fun buildCommand(vararg args: String) : String {
private fun buildCommand(vararg args: String): String {
return if (args.size == 1) quoteString(args[0]) else
if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) SHELL + " -c " + quoteString(args[2])
else args.joinToString(separator = " ")

View file

@ -5,13 +5,14 @@ import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
import java.nio.charset.Charset
import java.nio.file.Paths
private fun getOsName(): String {
return System.getProperty("os.name")
}
open class LocalProcessor : Processor {
open class LocalProcessor(val useHomeDirAsWorkingDir: Boolean = true) : Processor {
companion object {
@Suppress("JAVA_CLASS_ON_COMPANION")
@ -26,7 +27,12 @@ open class LocalProcessor : Processor {
private fun workingDir() : String
{
return System.getProperty("user.home") ?: File.separator
return if (useHomeDirAsWorkingDir) {
System.getProperty("user.home") ?: File.separator
} else {
// folder in which program was started
Paths.get("").toAbsolutePath().toString()
}
}
override fun exec(vararg args: String): ProcessResult {

View file

@ -2,10 +2,13 @@ package org.domaindrivenarchitecture.provs.framework.core.processors
interface Processor {
fun open() {
// no action needed for most processors; otherwise, overwrite this method in the implementing class
}
fun exec(vararg args: String): ProcessResult
fun execNoLog(vararg args: String): ProcessResult
fun close() {
// no action needed for most processors; if action is needed when closing, this method must be overwritten in the subclass
// no action needed for most processors; otherwise, overwrite this method in the implementing class
}
}

View file

@ -21,23 +21,28 @@ import java.util.concurrent.TimeUnit
* Executes task on a remote machine.
* Attention: host key is currently not being verified
*/
class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null) : Processor {
class RemoteProcessor(val host: InetAddress, val user: String, val password: Secret? = null) : Processor {
companion object {
@Suppress("JAVA_CLASS_ON_COMPANION")
private val log = LoggerFactory.getLogger(javaClass.enclosingClass)
}
private val ssh = SSHClient()
private var ssh = SSHClient()
init {
override fun open() {
try {
// always create a new instance as old one might be closed
ssh = SSHClient()
log.info("Connecting to $host with user: $user with " + if (password != null) "password" else "ssh-key")
ssh.loadKnownHosts()
// Attention: host key is not verified
ssh.addHostKeyVerifier(PromiscuousVerifier())
ssh.connectTimeout = 30000 // ms
ssh.connect(host)
if (password != null) {
@ -50,8 +55,9 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null)
try {
ssh.disconnect()
} finally {
log.error("Got exception when initializing ssh (Username, password or ssh-key might be wrong): " + e.message)
throw RuntimeException("Error when initializing ssh (Username, password or ssh-key might be wrong) ", e)
val errorMag = "Error when initializing ssh (Host, username, password or ssh-key might be wrong) "
log.error(errorMag + e.message)
throw RuntimeException(errorMag, e)
}
}
}
@ -87,9 +93,9 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null)
var session: Session? = null
try {
session = ssh.startSession()
session = ssh.startSession() ?: throw IllegalStateException("ERROR: Could not start ssh session.")
val cmd: Command = session!!.exec(cmdString)
val cmd: Command = session.exec(cmdString)
val out = BufferedReader(InputStreamReader(cmd.inputStream)).use { it.readText() }
val err = BufferedReader(InputStreamReader(cmd.errorStream)).use { it.readText() }
cmd.join(100, TimeUnit.SECONDS)

View file

@ -201,7 +201,7 @@ fun Prov.fileContentLargeFile(file: String, sudo: Boolean = false, chunkSize: In
// check first chunk
if (resultString == null) {
if (!chunkResult.success) {
return resultString
return null
} else {
resultString = ""
}
@ -251,7 +251,7 @@ fun Prov.replaceTextInFile(file: String, oldText: String, replacement: String) =
}
fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = task {
fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = taskWithResult {
// todo: only use sudo for root or if owner different from current
val content = fileContent(file, true)
if (content != null) {
@ -329,11 +329,15 @@ fun Prov.deleteDir(dir: String, path: String, sudo: Boolean = false): ProvResult
if ("" == path) {
throw RuntimeException("In deleteDir: path must not be empty.")
}
val cmd = "cd $path && rmdir $dir"
return if (!sudo) {
cmd(cmd)
return if (checkDir(dir, path, sudo)) {
val cmd = "cd $path && rmdir $dir"
if (!sudo) {
cmd(cmd)
} else {
cmd(cmd.sudoizeCommand())
}
} else {
cmd(cmd.sudoizeCommand())
ProvResult(true, out = "Dir to delete did not exist: $dir")
}
}
@ -403,7 +407,7 @@ fun Prov.fileSize(filename: String, sudo: Boolean = false): Int? {
private fun ensureValidPosixFilePermission(posixFilePermission: String) {
if (!Regex("^[0-7]{3}$").matches(posixFilePermission)) throw IllegalArgumentException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664")
if (!Regex("^0?[0-7]{3}$").matches(posixFilePermission)) throw IllegalArgumentException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664")
}
/**

View file

@ -3,9 +3,6 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.git.base
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.KNOWN_HOSTS_FILE
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.trustHost
import java.io.File
/**
@ -33,34 +30,14 @@ fun Prov.gitClone(
ProvResult(true, out = "Repo [$pathWithBasename] already exists, but might not be up-to-date.")
}
} else {
// create targetPath (if not yet existing)
// create targetPath if not yet existing
if (!checkDir(targetPath)) {
createDirs(targetPath)
}
// Note that all output of git clone on Linux is shown in stderr (normal progress info AND errors),
// which might be confusing in the logfile.
cmd("cd $targetPath && git clone $repoSource ${targetFolderName ?: ""}")
}
}
fun Prov.trustGithub() = task {
// current fingerprints from https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints
val fingerprints = setOf(
"SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com", // (RSA)
// supported beginning September 14, 2021:
"SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA)
"SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519)
)
trustHost("github.com", fingerprints)
}
fun Prov.trustGitlab() = task {
// entries for known_hosts from https://docs.gitlab.com/ee/user/gitlab_com/
val gitlabFingerprints = """
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
""".trimIndent()
addTextToFile("\n" + gitlabFingerprints + "\n", File(KNOWN_HOSTS_FILE))
}

View file

@ -20,7 +20,10 @@ fun Prov.aptInstall(packages: String, ignoreAlreadyInstalled: Boolean = true): P
if (!allInstalled) {
if (!isPackageInstalled(packages)) {
if (!aptInit) {
cmd("sudo apt-get update")
optional {
// may fail for some packages, but this should in general not be an issue
cmd("sudo apt-get update")
}
cmd("sudo apt-get install -qy apt-utils")
aptInit = true
}
@ -73,7 +76,15 @@ fun Prov.isPackageInstalled(packageName: String): Boolean {
/**
* Removes a package including its configuration and data files
* Returns true if a package is installed else false
*/
fun Prov.checkPackageInstalled(packageName: String): ProvResult = taskWithResult {
cmd("dpkg -s $packageName")
}
/**
* Removes a package including its configuration and data file
*/
@Suppress("unused") // used externally
fun Prov.aptPurge(packageName: String): Boolean {

View file

@ -1,17 +1,14 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base
import org.domaindrivenarchitecture.provs.desktop.domain.KnownHost
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.core.echoCommandForText
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDir
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createSecretFile
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.SshKeyPair
import java.io.File
const val KNOWN_HOSTS_FILE = "~/.ssh/known_hosts"
/**
* Installs ssh keys for active user; ssh filenames depend on the ssh key type, e.g. for public key file: "id_rsa.pub", "id_id_ed25519.pub", etc
*/
@ -23,72 +20,66 @@ fun Prov.configureSshKeys(sshKeys: SshKeyPair) = task {
/**
* Checks if the specified hostname or Ip is in a known_hosts file
*
* @return whether if was found
* Checks if the specified host (domain name or IP) and (optional) port is contained in the known_hosts file
*/
fun Prov.isHostKnown(hostOrIp: String) : Boolean {
return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false
fun Prov.isKnownHost(hostOrIp: String, port: Int? = null): Boolean {
val hostWithPotentialPort = port?.let { formatHostForKnownHostsFile(hostOrIp, port) } ?: hostOrIp
return cmdNoEval("ssh-keygen -F $hostWithPotentialPort").out?.isNotEmpty() ?: false
}
fun formatHostForKnownHostsFile(hostOrIp: String, port: Int? = null): String {
return port?.let { "[$hostOrIp]:$port" } ?: hostOrIp
}
/**
* Adds ssh keys for specified host (which also can be an ip-address) to ssh-file "known_hosts"
* Either add the specified rsaFingerprints or - if null - add automatically retrieved keys.
* Note: adding keys automatically is vulnerable to a man-in-the-middle attack, thus considered insecure and not recommended.
* Adds ssh keys for specified host (which also can be an ip-address) to the ssh-file "known_hosts".
* If parameter verifyKeys is true, the keys are checked against the live keys of the host and added only if valid.
*/
fun Prov.trustHost(host: String, fingerprintsOfKeysToBeAdded: Set<String>?) = taskWithResult {
if (isHostKnown(host)) {
return@taskWithResult ProvResult(true, out = "Host already known")
}
if (!checkFile(KNOWN_HOSTS_FILE)) {
createDir(".ssh")
createFile(KNOWN_HOSTS_FILE, null)
}
if (fingerprintsOfKeysToBeAdded == null) {
// auto add keys
cmd("ssh-keyscan $host >> $KNOWN_HOSTS_FILE")
} else {
// logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt
val actualKeys = findSshKeys(host)
if (actualKeys == null || actualKeys.size == 0) {
return@taskWithResult ProvResult(false, out = "No valid keys found for host: $host")
}
val actualFingerprints = getFingerprintsForKeys(actualKeys)
for (fingerprintToBeAdded in fingerprintsOfKeysToBeAdded) {
var indexOfKeyFound = -1
fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task {
val knownHostsFile = "~/.ssh/known_hosts"
// search for fingerprint in actual fingerprints
for ((i, actualFingerprint) in actualFingerprints.withIndex()) {
if (actualFingerprint.contains(fingerprintToBeAdded)) {
indexOfKeyFound = i
break
if (!checkFile(knownHostsFile)) {
createDir(".ssh")
createFile(knownHostsFile, null)
}
with(knownHost) {
for (key in hostKeys) {
if (!verifyKeys) {
addTextToFile("\n${formatHostForKnownHostsFile(hostName, port)} $key\n", File(knownHostsFile))
} else {
val validKeys = findSshKeys(hostName, port)
if (validKeys?.contains(key) == true) {
val formattedHost = formatHostForKnownHostsFile(hostName, port)
addTextToFile("\n$formattedHost $key\n", File(knownHostsFile))
} else {
addResultToEval(
ProvResult(
false,
err = "The following key of host [$hostName] could not be verified successfully: " + key
)
)
}
}
if (indexOfKeyFound == -1) {
return@taskWithResult ProvResult(
false,
err = "Fingerprint ($fingerprintToBeAdded) could not be found in actual fingerprints: $actualFingerprints"
)
}
cmd(echoCommandForText(actualKeys.get(indexOfKeyFound) + "\n") + " >> $KNOWN_HOSTS_FILE")
}
ProvResult(true)
}
}
/**
* Returns a list of valid ssh keys for the given host (host can also be an ip address)
* Returns a list of valid ssh keys for the given host (host can also be an ip address),
* keys are returned (space-separated) as keytype and key, but WITHOUT the host name.*
* If no port is specified, the keys for the default port (22) are returned.
* If no keytype is specified, keys are returned for all keytypes.
*/
private fun Prov.findSshKeys(host: String): List<String>? {
return cmd("ssh-keyscan $host 2>/dev/null").out?.split("\n")?.filter { x -> "" != x }
fun Prov.findSshKeys(host: String, port: Int? = null, keytype: String? = null): List<String>? {
val portOption = port?.let { " -p $port " } ?: ""
val keytypeOption = keytype?.let { " -t $keytype " } ?: ""
val output = cmd("ssh-keyscan $portOption $keytypeOption $host 2>/dev/null").out?.trim()
return output?.split("\n")?.filter { x -> "" != x }?.map { x -> x.substringAfter(" ") }
}
/**
* Returns a list of fingerprints of the given sshKeys; the returning list has same size and order as the specified list of sshKeys
*/
private fun Prov.getFingerprintsForKeys(sshKeys: List<String>): List<String> {
return sshKeys.map { x -> cmd("echo \"$x\" | ssh-keygen -lf -").out ?: "" }
}
fun main() {
val k = local().findSshKeys("repo.prod.meissa.de", 2222)
println(k)
}

View file

@ -0,0 +1,15 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.domain
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.infrastructure.createCronJob
/**
* Adds a schedule for a monthly reboot of the (Linux) system.
* ATTENTION: Use with care!! System will be shut down, restart might not succeed in all cases.
*/
fun Prov.scheduleMonthlyReboot() = task {
// reboot each first Tuesday a month - see https://blog.healthchecks.io/2022/09/schedule-cron-job-the-funky-way/
val schedule = "0 2 */100,1-7 * TUE"
createCronJob("50_monthly_reboot", schedule, "/sbin/shutdown", "-r now", "root")
}

View file

@ -0,0 +1,29 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
/**
* Creates a cron job.
* @param cronFilename e.g. "90_my_cron"; file is created in folder /etc/cron.d/
* @param schedule in the usual cron-format, examples: "0 * * * *" for each hour, "0 3 1-7 * 1" for the first Monday each month at 3:00, etc
* @param command the executed command
* @param user the user with whom the command will be executed, if null the current user is used
*/
fun Prov.createCronJob(cronFilename: String, schedule: String, command: String, parameter: String, user: String? = null) = task {
val cronUser = user ?: whoami()
// ensure command exists
if (checkFile(command, sudo = true)) {
val cronLine = "$schedule $cronUser $command $parameter\n"
createDirs("/etc/cron.d/", sudo = true)
createFile("/etc/cron.d/$cronFilename", cronLine, "644", sudo = true, overwriteIfExisting = true)
} else {
addResultToEval(ProvResult(false, err = "$command not found."))
}
}

View file

@ -6,24 +6,25 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.
@Serializable
abstract class SecretSource(protected val input: String) {
abstract class SecretSource(protected val parameter: String) {
abstract fun secret() : Secret
abstract fun secretNullable() : Secret?
}
@Serializable
enum class SecretSourceType() {
enum class SecretSourceType {
PLAIN, FILE, PROMPT, PASS, GOPASS;
PLAIN, FILE, PROMPT, PASS, GOPASS, ENV;
fun secret(input: String) : Secret {
fun secret(parameter: String) : Secret {
return when (this) {
PLAIN -> PlainSecretSource(input).secret()
FILE -> FileSecretSource(input).secret()
PLAIN -> PlainSecretSource(parameter).secret()
FILE -> FileSecretSource(parameter).secret()
PROMPT -> PromptSecretSource().secret()
PASS -> PassSecretSource(input).secret()
GOPASS -> GopassSecretSource(input).secret()
PASS -> PassSecretSource(parameter).secret()
GOPASS -> GopassSecretSource(parameter).secret()
ENV -> EnvSecretSource(parameter).secret()
}
}
}

View file

@ -0,0 +1,18 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
/**
* Reads secret from a local environment variable
*/
class EnvSecretSource(varName: String) : SecretSource(varName) {
override fun secret(): Secret {
return secretNullable() ?: throw Exception("Failed to get secret from environment variable: $parameter")
}
override fun secretNullable(): Secret? {
val secret = System.getenv(parameter)
return if (secret == null) null else Secret(secret)
}
}

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
@ -11,12 +12,12 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
class FileSecretSource(fqFileName: String) : SecretSource(fqFileName) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "FileSecretSource")
return p.getSecret("cat " + input) ?: throw Exception("Failed to get secret.")
val p = Prov.newInstance(name = "FileSecretSource", progressType = ProgressType.NONE)
return p.getSecret("cat " + parameter) ?: throw Exception("Failed to get secret.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "FileSecretSource")
return p.getSecret("cat " + input)
val p = Prov.newInstance(name = "FileSecretSource", progressType = ProgressType.NONE)
return p.getSecret("cat " + parameter)
}
}

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
@ -10,10 +11,10 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
*/
class GopassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret {
return secretNullable() ?: throw Exception("Failed to get \"$input\" secret from gopass.")
return secretNullable() ?: throw Exception("Failed to get \"$parameter\" secret from gopass.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "GopassSecretSource for $input")
return p.getSecret("gopass show -f $input", true)
val p = Prov.newInstance(name = "GopassSecretSource for $parameter", progressType = ProgressType.NONE)
return p.getSecret("gopass show -f $parameter", true)
}
}

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
@ -10,11 +11,11 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
*/
class PassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "PassSecretSource")
return p.getSecret("pass " + input) ?: throw Exception("Failed to get secret.")
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
return p.getSecret("pass " + parameter) ?: throw Exception("Failed to get secret.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "PassSecretSource")
return p.getSecret("pass " + input)
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
return p.getSecret("pass " + parameter)
}
}

View file

@ -6,9 +6,9 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
class PlainSecretSource(plainSecret: String) : SecretSource(plainSecret) {
override fun secret(): Secret {
return Secret(input)
return Secret(parameter)
}
override fun secretNullable(): Secret {
return Secret(input)
return Secret(parameter)
}
}

View file

@ -8,7 +8,7 @@ import javax.swing.*
class PasswordPanel : JPanel(FlowLayout()) {
private val passwordField = JPasswordField(20)
private val passwordField = JPasswordField(30)
private var entered = false
val enteredPassword
@ -47,7 +47,7 @@ class PasswordPanel : JPanel(FlowLayout()) {
class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
override fun secret(): Secret {
val password = PasswordPanel.requestPassword(input)
val password = PasswordPanel.requestPassword(parameter)
if (password == null) {
throw IllegalArgumentException("Failed to retrieve secret from prompting.")
} else {
@ -56,7 +56,7 @@ class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text)
}
override fun secretNullable(): Secret? {
val password = PasswordPanel.requestPassword(input)
val password = PasswordPanel.requestPassword(parameter)
return if(password == null) {
null

View file

@ -25,15 +25,15 @@ fun Prov.userExists(userName: String): Boolean {
fun Prov.createUser(
userName: String,
password: Secret? = null,
sudo: Boolean = false,
userCanSudoWithoutPassword: Boolean = false,
copyAuthorizedSshKeysFromCurrentUser: Boolean = false
): ProvResult = task {
if (!userExists(userName)) {
cmd("sudo adduser --gecos \"First Last,RoomNumber,WorkPhone,HomePhone\" --disabled-password --home /home/$userName $userName")
}
password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true)
if (sudo) {
makeUserSudoerWithNoSudoPasswordRequired(userName)
if (userCanSudoWithoutPassword) {
makeUserSudoerWithoutPasswordRequired(userName)
}
val authorizedKeysFile = userHome() + ".ssh/authorized_keys"
if (copyAuthorizedSshKeysFromCurrentUser && checkFile(authorizedKeysFile)) {
@ -85,11 +85,11 @@ fun Prov.deleteUser(userName: String, deleteHomeDir: Boolean = false): ProvResul
* The current (executing) user must already be a sudoer. If he is a sudoer with password required then
* his password must be provided.
*/
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(
fun Prov.makeUserSudoerWithoutPasswordRequired(
userName: String,
password: Secret? = null,
overwriteFile: Boolean = false
): ProvResult = task {
): ProvResult = taskWithResult {
val userSudoFile = "/etc/sudoers.d/$userName"
if (!checkFile(userSudoFile) || overwriteFile) {
val sudoPrefix = if (password == null) "sudo" else "echo ${password.plain()} | sudo -S"
@ -107,11 +107,10 @@ fun Prov.makeUserSudoerWithNoSudoPasswordRequired(
* Makes the current (executing) user be able to sudo without password.
* IMPORTANT: Current user must already by sudoer when calling this function.
*/
@Suppress("unused") // used externally
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = task {
fun Prov.makeCurrentUserSudoerWithoutPasswordRequired(password: Secret) = taskWithResult {
val currentUser = whoami()
if (currentUser != null) {
makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true)
makeUserSudoerWithoutPasswordRequired(currentUser, password, overwriteFile = true)
} else {
ProvResult(false, "Current user could not be determined.")
}
@ -131,11 +130,10 @@ fun Prov.userIsInGroupSudo(userName: String): Boolean {
* Checks if current user can execute sudo commands.
*/
@Suppress("unused")
fun Prov.currentUserCanSudo(): Boolean {
return chk("timeout 1 sudo id")
fun Prov.currentUserCanSudoWithoutPassword(): Boolean {
return chk("timeout 1 sudo -kS id")
}
/**
* Returns username of current user if it can be determined
*/

View file

@ -1,6 +1,9 @@
package org.domaindrivenarchitecture.provs.server.application
import org.domaindrivenarchitecture.provs.configuration.application.ensureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.printProvsVersion
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import org.domaindrivenarchitecture.provs.server.domain.ServerType
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sCliCommand
import org.domaindrivenarchitecture.provs.server.domain.k3s.provisionK3sCommand
@ -15,6 +18,8 @@ import kotlin.system.exitProcess
*/
fun main(args: Array<String>) {
printProvsVersion()
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
// validate subcommand
@ -23,16 +28,18 @@ fun main(args: Array<String>) {
exitProcess(1)
}
val cmd = CliArgumentsParser("provs-server.jar subcommand target").parseCommand(checkedArgs)
val cmd = CliArgumentsParser("provs-server subcommand target").parseCommand(checkedArgs)
// ToDo: exitProcess makes testing harder, find another solution
// validate parsed arguments
if (!cmd.isValidTarget()) {
println("Remote or localhost not valid, please try -h for help.")
exitProcess(1)
quit(1)
}
val prov = createProvInstance(cmd.target)
prov.provisionK3sCommand(cmd as K3sCliCommand)
prov.session {
ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
provisionK3sCommand(cmd as K3sCliCommand)
}
}

View file

@ -1,6 +1,7 @@
package org.domaindrivenarchitecture.provs.server.application
import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
@ -11,6 +12,7 @@ import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sCliCommand
import org.domaindrivenarchitecture.provs.server.domain.k3s.ServerOnlyModule
@OptIn(ExperimentalCli::class)
class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<ServerSubcommand> = listOf(K3s(), K3d())
@ -84,7 +86,7 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) {
override fun execute() {
super.configFileName = cliConfigFileName?.let { ConfigFileName(it) }
super.applicationFileName = cliApplicationFileName?.let { ApplicationFileName(it) }
super.onlyModules = if (only != null) listOf(only!!.name.lowercase()) else null
super.onlyModules = only?.let { listOf(it.name.lowercase()) }
super.reprovision = cliReprovision == true
super.parsed = true
}

View file

@ -0,0 +1,8 @@
package org.domaindrivenarchitecture.provs.server.domain.hetzner_csi
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionHetznerCSIForK8s
fun Prov.provisionHetznerCSI(configResolved: HetznerCSIConfigResolved) = task {
provisionHetznerCSIForK8s(configResolved.hcloudApiToken, configResolved.encryptionPassphrase)
}

View file

@ -0,0 +1,23 @@
package org.domaindrivenarchitecture.provs.server.domain.hetzner_csi
import kotlinx.serialization.Serializable
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSupplier
@Serializable
data class HetznerCSIConfig (
val hcloudApiToken: SecretSupplier,
val encryptionPassphrase: SecretSupplier,
) {
fun resolveSecret(): HetznerCSIConfigResolved = HetznerCSIConfigResolved(this)
}
data class HetznerCSIConfigResolved(val configUnresolved: HetznerCSIConfig) {
val hcloudApiToken: Secret = configUnresolved.hcloudApiToken.secret()
val encryptionPassphrase: Secret = configUnresolved.encryptionPassphrase.secret()
}
@Serializable
data class HetznerCSIConfigHolder(
val hetzner: HetznerCSIConfig
)

View file

@ -0,0 +1,26 @@
package org.domaindrivenarchitecture.provs.server.domain.k3s
data class ApplicationFile(val id: ApplicationFileName, val fileContent: String) {
fun validate() : List<String> {
val output = ArrayList<String>()
val specRegex = "Spec.failed".toRegex()
val javaRegex = "Exception.in.thread".toRegex()
if(fileContent.isEmpty()) {
output.add("fileContent is empty.")
}
val specMatch = specRegex.find(fileContent)
if (specMatch != null) {
output.add(specMatch.value)
}
val javaMatch = javaRegex.find(fileContent)
if (javaMatch != null) {
output.add(javaMatch.value)
}
return output
}
fun isValid() : Boolean {
return validate().isEmpty()
}
}

View file

@ -2,8 +2,8 @@ package org.domaindrivenarchitecture.provs.server.domain.k3s
import java.io.File
data class ApplicationFileName(val fileName: String) {
fun fullqualified() : String {
class ApplicationFileName(val fileName: String) {
fun fullyQualifiedName() : String {
return File(fileName).absoluteFile.absolutePath
}
}
}

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.server.domain.k3s
interface ApplicationFileRepository {
fun assertExists(applicationFileName: ApplicationFileName?)
fun getFile() : ApplicationFile
}

View file

@ -10,7 +10,8 @@ data class K3sConfig(
val loopback: Loopback = Loopback(ipv4 = "192.168.5.1", ipv6 = "fc00::5:1"),
val certmanager: Certmanager? = null,
val echo: Echo? = null,
val reprovision: Reprovision = false
val reprovision: Reprovision = false,
val monthlyReboot: Boolean = true,
) {
fun isDualStack(): Boolean {
return node.ipv6 != null && loopback.ipv6 != null

View file

@ -2,25 +2,46 @@ package org.domaindrivenarchitecture.provs.server.domain.k3s
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.domain.scheduleMonthlyReboot
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfigResolved
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.provisionHetznerCSI
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.GrafanaAgentConfigResolved
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.provisionGrafanaAgent
import org.domaindrivenarchitecture.provs.server.infrastructure.*
import org.domaindrivenarchitecture.provs.server.infrastructure.DefaultApplicationFileRepository
import org.domaindrivenarchitecture.provs.server.infrastructure.deprovisionK3sInfra
import org.domaindrivenarchitecture.provs.server.infrastructure.findHetznerCSIConfig
import org.domaindrivenarchitecture.provs.server.infrastructure.findK8sGrafanaConfig
import org.domaindrivenarchitecture.provs.server.infrastructure.getK3sConfig
import org.domaindrivenarchitecture.provs.server.infrastructure.installK3s
import org.domaindrivenarchitecture.provs.server.infrastructure.installK9s
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionK3sApplication
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionK3sCertManager
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionK3sEcho
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionNetwork
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionServerCliConvenience
import kotlin.system.exitProcess
fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
val grafanaConfigResolved: GrafanaAgentConfigResolved? = findK8sGrafanaConfig(cli.configFileName)?.resolveSecret()
val hcloudConfigResolved: HetznerCSIConfigResolved? = findHetznerCSIConfig(cli.configFileName)?.resolveSecret()
if (cli.onlyModules == null ) {
if (cli.onlyModules == null) {
val k3sConfig: K3sConfig = getK3sConfig(cli.configFileName)
DefaultApplicationFileRepository().assertExists(cli.applicationFileName)
DefaultConfigFileRepository().assertExists(cli.configFileName)
val k3sConfigReprovision = k3sConfig.copy(reprovision = cli.reprovision || k3sConfig.reprovision)
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, cli.applicationFileName)
val applicationFile = cli.applicationFileName?.let { DefaultApplicationFileRepository(cli.applicationFileName).getFile() }
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, hcloudConfigResolved, applicationFile)
} else {
provisionGrafana(cli.onlyModules, grafanaConfigResolved)
cli.onlyModules.forEach { module ->
when (module.uppercase()) {
ServerOnlyModule.MONTHLY_REBOOT.name -> scheduleMonthlyReboot()
ServerOnlyModule.HETZNER_CSI.name -> provisionHetznerCSI(hcloudConfigResolved)
ServerOnlyModule.GRAFANA.name -> provisionGrafana(grafanaConfigResolved)
}
}
}
}
@ -30,7 +51,9 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
fun Prov.provisionK3s(
k3sConfig: K3sConfig,
grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
applicationFileName: ApplicationFileName? = null) = task {
hetznerCSIConfigResolved: HetznerCSIConfigResolved? = null,
applicationFile: ApplicationFile? = null
) = task {
if (k3sConfig.reprovision) {
deprovisionK3sInfra()
@ -52,24 +75,42 @@ fun Prov.provisionK3s(
provisionGrafanaAgent(grafanaConfigResolved)
}
if (applicationFileName != null) {
provisionK3sApplication(applicationFileName)
if (hetznerCSIConfigResolved != null) {
provisionHetznerCSI(hetznerCSIConfigResolved)
}
if (applicationFile != null) {
provisionK3sApplication(applicationFile)
}
if (!k3sConfig.reprovision) {
provisionServerCliConvenience()
}
if (k3sConfig.monthlyReboot) {
scheduleMonthlyReboot()
}
installK9s()
}
private fun Prov.provisionGrafana(
onlyModules: List<String>?,
grafanaConfigResolved: GrafanaAgentConfigResolved?) = task {
grafanaConfigResolved: GrafanaAgentConfigResolved?
) = task {
if (grafanaConfigResolved == null) {
println("ERROR: Could not find grafana config.")
exitProcess(7)
}
provisionGrafanaAgent(grafanaConfigResolved)
}
if (onlyModules != null && onlyModules.contains(ServerOnlyModule.GRAFANA.name.lowercase())) {
if (grafanaConfigResolved == null) {
println("ERROR: Could not find grafana config.")
private fun Prov.provisionHetznerCSI(
hetznerCSIConfigResolved: HetznerCSIConfigResolved?
) = task {
if (hetznerCSIConfigResolved == null) {
println("ERROR: Could not find hetznerCSI config.")
exitProcess(7)
}
provisionGrafanaAgent(grafanaConfigResolved)
}
provisionHetznerCSI(hetznerCSIConfigResolved)
}

View file

@ -1,5 +1,7 @@
package org.domaindrivenarchitecture.provs.server.domain.k3s
enum class ServerOnlyModule {
GRAFANA
GRAFANA,
HETZNER_CSI,
MONTHLY_REBOOT,
}

View file

@ -2,7 +2,6 @@ package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource
private const val resourcePath = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
@ -16,7 +15,8 @@ fun Prov.provisionServerCliConvenience() = task {
fun Prov.provisionKubectlCompletionAndAlias(): ProvResult = task {
cmd("kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null")
cmd("echo 'alias k=kubectl' >> ~/.bashrc")
cmd("echo 'complete -o default -F __start_kubectl k' >>~/.bashrc")
cmd("echo 'alias k9=\"k9s --kubeconfig /etc/kubernetes/admin.conf\"' >> ~/.bashrc")
cmd("echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc")
}
fun Prov.provisionVimrc(): ProvResult = task {

View file

@ -1,14 +1,30 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.getLocalFileContent
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkLocalFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileRepository
class DefaultApplicationFileRepository : ApplicationFileRepository {
override fun assertExists(applicationFileName: ApplicationFileName?) {
if (applicationFileName != null && !checkLocalFile(applicationFileName.fullqualified())) {
throw RuntimeException("Application file ${applicationFileName.fileName} not found. Please check if path is correct.")
class DefaultApplicationFileRepository(val applicationFileName: ApplicationFileName) : ApplicationFileRepository {
private fun assertExists(applicationFileName: String) {
if (!checkLocalFile(applicationFileName)) {
throw RuntimeException("Application file not found. Please check if path is correct.")
}
}
override fun getFile(): ApplicationFile {
assertExists(applicationFileName.fullyQualifiedName())
val applicationFileContents = getLocalFileContent(applicationFileName.fullyQualifiedName())
val applicationFile = ApplicationFile(applicationFileName, applicationFileContents)
return if (applicationFile.isValid()) {
applicationFile
} else {
throw RuntimeException("Application file was invalid.")
}
}
}

View file

@ -0,0 +1,53 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResourceTemplate
import org.domaindrivenarchitecture.provs.server.domain.k3s.FileMode
import java.io.File
private const val hetznerCSIResourceDir = "org/domaindrivenarchitecture/provs/server/infrastructure/hetznerCSI/"
fun Prov.provisionHetznerCSIForK8s(hetznerApiToken: Secret, encryptionPassphrase: Secret) {
// CSI Driver
createFileFromResourceTemplate(
k3sManualManifestsDir + "hcloud-api-token-secret.yaml",
"hcloud-api-token-secret.template.yaml",
resourcePath = hetznerCSIResourceDir,
posixFilePermission = "644",
values = mapOf(
"HETZNER_API_TOKEN" to hetznerApiToken.plain()
))
cmd("kubectl apply -f hcloud-api-token-secret.yaml", k3sManualManifestsDir)
applyHetznerCSIFileFromResource(File(k3sManualManifestsDir, "hcloud-csi.yaml"))
// Encryption
createFileFromResourceTemplate(
k3sManualManifestsDir + "hcloud-encryption-secret.yaml",
"hcloud-encryption-secret.template.yaml",
resourcePath = hetznerCSIResourceDir,
posixFilePermission = "644",
values = mapOf(
"HETZNER_ENCRYPTION_PASSPHRASE" to encryptionPassphrase.plain()
))
cmd("kubectl apply -f hcloud-encryption-secret.yaml", k3sManualManifestsDir)
applyHetznerCSIFileFromResource(File(k3sManualManifestsDir, "hcloud-encrypted-storage-class.yaml"))
}
private fun Prov.createHetznerCSIFileFromResource(
file: File,
posixFilePermission: FileMode? = "644"
) = task {
createFileFromResource(
file.path,
file.name,
hetznerCSIResourceDir,
posixFilePermission,
sudo = true
)
}
private fun Prov.applyHetznerCSIFileFromResource(file: File, posixFilePermission: FileMode? = "644") = task {
createHetznerCSIFileFromResource(file, posixFilePermission)
cmd("kubectl apply -f ${file.path}", sudo = true)
}

View file

@ -0,0 +1,31 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import com.charleskorn.kaml.MissingRequiredPropertyException
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.framework.core.readFromFile
import org.domaindrivenarchitecture.provs.framework.core.toYaml
import org.domaindrivenarchitecture.provs.framework.core.yamlToType
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfig
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfigHolder
import java.io.File
import java.io.FileWriter
private const val DEFAULT_CONFIG_FILE = "server-config.yaml"
fun findHetznerCSIConfig(fileName: ConfigFileName? = null): HetznerCSIConfig? {
val filePath = fileName?.fileName ?: DEFAULT_CONFIG_FILE
return if(File(filePath).exists()) {
try {
readFromFile(filePath).yamlToType<HetznerCSIConfigHolder>().hetzner
} catch (e: MissingRequiredPropertyException) {
if (e.message.contains("Property 'hetzner'")) null else throw e
}
} else {
null
}
}
@Suppress("unused")
internal fun writeConfig(config: HetznerCSIConfigHolder, fileName: String = "hetzner-config.yaml") =
FileWriter(fileName).use { it.write(config.toYaml()) }

View file

@ -1,39 +0,0 @@
package org.domaindrivenarchitecture.provs.server.domain
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.echoCommandForTextWithNewlinesReplaced
import org.domaindrivenarchitecture.provs.framework.core.repeatTaskUntilSuccess
/**
* Runs a k3s server and a k3s agent as containers.
* Copies the kubeconfig from container to the default location: $HOME/.kube/config
*/
fun Prov.installK3sAsContainers(token: String = "12345678901234") = task {
cmd("docker volume create k3s-server")
provideContainer("k3s-server", "rancher/k3s", command = "server --cluster-init", options =
"-d --privileged --tmpfs /run --tmpfs /var/run " +
"-e K3S_TOKEN=$token -e K3S_KUBECONFIG_OUTPUT=./kubeconfig.yaml -e K3S_KUBECONFIG_MODE=666 " +
"-v k3s-server:/var/lib/rancher/k3s:z -p 6443:6443 -p 80:80 -p 443:443 " +
"--ulimit nproc=65535 --ulimit nofile=65535:65535")
// wait for config file
cmd("export timeout=60; while [ ! -f /var/lib/docker/volumes/k3s-server/_data/server/kubeconfig.yaml ]; do if [ \"${'$'}timeout\" == 0 ]; then echo \"ERROR: Timeout while waiting for file.\"; break; fi; sleep 1; ((timeout--)); done")
sh("""
mkdir -p ${'$'}HOME/.kube/
cp /var/lib/docker/volumes/k3s-server/_data/server/kubeconfig.yaml ${'$'}HOME/.kube/config
""".trimIndent())
}
/**
* Apply a config to kubernetes.
* Prerequisite: Kubectl has to be installed
*/
fun Prov.applyK8sConfig(configAsYaml: String, kubectlCommand: String = "kubectl") = task {
repeatTaskUntilSuccess(6, 10) {
cmd(echoCommandForTextWithNewlinesReplaced(configAsYaml) + " | $kubectlCommand apply -f -")
}
}

View file

@ -5,15 +5,14 @@ import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.core.repeatTaskUntilSuccess
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.server.domain.CertmanagerEndpoint
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.domaindrivenarchitecture.provs.server.domain.k3s.Certmanager
import org.domaindrivenarchitecture.provs.server.domain.k3s.FileMode
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sConfig
import org.domaindrivenarchitecture.provs.server.domain.k3s.*
import java.io.File
// ----------------------------------- versions --------------------------------
const val K3S_VERSION = "v1.23.6+k3s1"
// when updating this version, it is recommended to update also file k3s-install.sh as well as traefik.yaml in this repo
// (both files in: src/main/resources/org/domaindrivenarchitecture/provs/server/infrastructure/k3s/)
const val K3S_VERSION = "v1.29.1+k3s2"
// ----------------------------------- directories --------------------------------
const val k3sManualManifestsDir = "/etc/rancher/k3s/manifests/"
@ -25,8 +24,8 @@ private const val k3sResourceDir = "org/domaindrivenarchitecture/provs/server/in
// ----------------------------------- files --------------------------------
private val k3sInstallScript = File( "/usr/local/bin/k3s-install.sh")
private val k3sConfigFile = File( "/etc/rancher/k3s/config.yaml")
private val k3sInstallScript = File("/usr/local/bin/k3s-install.sh")
private val k3sConfigFile = File("/etc/rancher/k3s/config.yaml")
private val k3sKubeConfig = File("/etc/rancher/k3s/k3s.yaml")
private val k3sTraefikWorkaround = File(k3sManualManifestsDir, "traefik.yaml")
@ -34,12 +33,12 @@ private val k3sMiddleWareHttpsRedirect = File(k3sManualManifestsDir, "middleware
private val certManagerDeployment = File(k3sManualManifestsDir, "cert-manager.yaml")
private val certManagerIssuer = File(k3sManualManifestsDir, "le-issuer.yaml")
private val k3sEcho = File(k3sManualManifestsDir, "echo.yaml")
private val k3sEchoWithTls = File(k3sManualManifestsDir, "echo-tls.yaml")
private val k3sEchoNoTls = File(k3sManualManifestsDir, "echo-no-tls.yaml")
private val selfSignedCertificate = File(k3sManualManifestsDir, "selfsigned-certificate.yaml")
private val localPathProvisionerConfig = File(k3sManualManifestsDir, "local-path-provisioner-config.yaml")
// ----------------------------------- public functions --------------------------------
fun Prov.testConfigExists(): Boolean {
@ -52,7 +51,11 @@ fun Prov.deprovisionK3sInfra() = task {
deleteFile(certManagerDeployment.path, sudo = true)
deleteFile(certManagerIssuer.path, sudo = true)
deleteFile(k3sKubeConfig.path, sudo = true)
cmd("k3s-uninstall.sh")
val k3sUninstallScript = "k3s-uninstall.sh"
if (chk("which $k3sUninstallScript")) {
cmd(k3sUninstallScript)
}
}
@ -78,25 +81,35 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
if (k3sConfig.isDualStack()) {
k3sConfigResourceFileName += ".dual.template.yaml"
metallbConfigResourceFileName += ".dual.template.yaml"
k3sConfigMap = k3sConfigMap.plus("node_ipv6" to k3sConfig.node.ipv6!!)
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6!!)
require(k3sConfig.node.ipv6 != null && k3sConfig.loopback.ipv6 != null)
k3sConfigMap = k3sConfigMap
.plus("node_ipv6" to k3sConfig.node.ipv6)
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6)
} else {
k3sConfigResourceFileName += ".ipv4.template.yaml"
metallbConfigResourceFileName += ".ipv4.template.yaml"
}
createK3sFileFromResourceTemplate(k3sConfigFile, k3sConfigMap, alternativeResourceTemplate = File(k3sConfigResourceFileName))
createK3sFileFromResourceTemplate(
k3sConfigFile,
k3sConfigMap,
alternativeResourceTemplate = File(k3sConfigResourceFileName)
)
createK3sFileFromResource(k3sInstallScript, posixFilePermission = "755")
cmd("INSTALL_K3S_VERSION=$K3S_VERSION k3s-install.sh")
// metallb
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-namespace.yaml"))
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.10.2-manifest.yaml"))
applyK3sFileFromResourceTemplate(
File(k3sManualManifestsDir, "metallb-config.yaml"),
k3sConfigMap,
alternativeResourceName = File(metallbConfigResourceFileName)
)
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.13.7-native-manifest.yaml"))
repeatTaskUntilSuccess(10, 10) {
applyK3sFileFromResourceTemplate(
File(k3sManualManifestsDir, "metallb-config.yaml"),
k3sConfigMap,
alternativeResourceName = File(metallbConfigResourceFileName)
)
}
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-l2advertisement.yaml"))
// traefik
if (k3sConfig.isDualStack()) {
@ -110,8 +123,9 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
applyK3sFileFromResource(k3sMiddleWareHttpsRedirect)
}
// other
applyK3sFileFromResource(localPathProvisionerConfig)
// TODO: jem 2022-11-25: Why do we need sudo here??
cmd("kubectl set env deployment -n kube-system local-path-provisioner DEPLOY_DATE=\"$(date)\"", sudo = true)
cmd("ln -sf $k3sKubeConfig " + k8sCredentialsDir + "admin.conf", sudo = true)
@ -137,22 +151,25 @@ fun Prov.provisionK3sCertManager(certmanager: Certmanager) = task {
}
}
fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null) = task {
val endpointName = endpoint?.name?.lowercase()
fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null, withTls: Boolean = false) = task {
if (withTls) {
val endpointName = endpoint?.name?.lowercase()
val issuer = if (endpointName == null) {
createK3sFileFromResourceTemplate(selfSignedCertificate, mapOf("host" to fqdn))
"selfsigned-issuer"
val issuer = if (endpointName == null) {
createK3sFileFromResourceTemplate(selfSignedCertificate, mapOf("host" to fqdn))
"selfsigned-issuer"
} else {
endpointName
}
applyK3sFileFromResourceTemplate(k3sEchoWithTls, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
} else {
endpointName
applyK3sFileFromResource(k3sEchoNoTls)
}
applyK3sFileFromResourceTemplate(k3sEcho, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
}
fun Prov.provisionK3sApplication(applicationFileName: ApplicationFileName) = task {
fun Prov.provisionK3sApplication(applicationFile: ApplicationFile) = task {
copyFileFromLocal(
fullyQualifiedLocalFilename = applicationFileName.fullqualified(),
fullyQualifiedLocalFilename = applicationFile.id.fullyQualifiedName(),
fullyQualifiedFilename = k3sManualManifestsDir + "application.yaml",
posixFilePermission = "644",
sudo = true
@ -216,5 +233,5 @@ private fun File.templateName(): String {
}
internal fun Prov.configureShellAliases() = task {
addTextToFile( "\nalias k=\"sudo kubectl\"\n", File(".bash_aliases",))
addTextToFile("\nalias k=\"sudo kubectl\"\n", File(".bash_aliases"))
}

View file

@ -0,0 +1,15 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
const val K9S_VERSION = "v0.32.5"
fun Prov.installK9s() = task {
if (cmdNoEval("k9s version").out?.contains(K9S_VERSION) != true) {
downloadFromURL("https://github.com/derailed/k9s/releases/download/$K9S_VERSION/k9s_linux_amd64.deb", "k9s_linux_amd64.deb", "/tmp")
cmd("sudo dpkg -i k9s_linux_amd64.deb", "/tmp")
}
}

View file

@ -16,11 +16,12 @@ fun Prov.testNetworkExists(): Boolean {
fun Prov.provisionNetwork(k3sConfig: K3sConfig) = task {
if(!testNetworkExists()) {
if(k3sConfig.isDualStack()) {
require(k3sConfig.loopback.ipv6 != null)
createFileFromResourceTemplate(
loopbackFile,
"99-loopback.dual.template.yaml",
resourcePathNetwork,
mapOf("loopback_ipv4" to k3sConfig.loopback.ipv4, "loopback_ipv6" to k3sConfig.loopback.ipv6!!),
mapOf("loopback_ipv4" to k3sConfig.loopback.ipv4, "loopback_ipv6" to k3sConfig.loopback.ipv6),
"644",
sudo = true
)

View file

@ -1,10 +1,5 @@
package org.domaindrivenarchitecture.provs.syspec.infrastructure
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.ListObjectsRequest
import aws.sdk.kotlin.services.s3.model.ListObjectsResponse
import aws.smithy.kotlin.runtime.time.Instant
import kotlinx.coroutines.runBlocking
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkDir
@ -13,7 +8,6 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackag
import org.domaindrivenarchitecture.provs.syspec.domain.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.time.Duration
import java.util.*
import java.util.concurrent.TimeUnit
@ -29,7 +23,6 @@ fun Prov.verifySpecConfig(conf: SyspecConfig) = task {
conf.netcat?.let { task("NetcatSpecs") { for (spec in conf.netcat) verify(spec) } }
conf.socket?.let { task("SocketSpecs") { for (spec in conf.socket) verify(spec) } }
conf.certificate?.let { task("CertificateFileSpecs") { for (spec in conf.certificate) verify(spec) } }
conf.s3?.let { task("CertificateFileSpecs") { for (spec in conf.s3) verify(spec) } }
}
// ------------------------------- verification functions for individual specs --------------------------------
@ -112,27 +105,6 @@ fun Prov.verify(cert: CertificateFileSpec) {
}
}
fun Prov.verify(s3ObjectSpec: S3ObjectSpec) {
val (bucket, prefix, maxAge) = s3ObjectSpec
val expectedAge = Duration.ofHours(s3ObjectSpec.age)
val latestObject = getS3Objects(bucket, prefix).contents?.maxByOrNull { it.lastModified ?: Instant.fromEpochSeconds(0) }
if (latestObject == null) {
verify(false, "Could not retrieve an s3 object with prefix $prefix")
} else {
// convert to java.time.Instant for easier comparison
val lastModified = java.time.Instant.ofEpochSecond(latestObject.lastModified?.epochSeconds ?: 0)
val actualAge = Duration.between(lastModified, java.time.Instant.now())
verify(
actualAge <= expectedAge,
"Age is ${actualAge.toHours()} h (expected: <= $maxAge) for latest file with prefix \"$prefix\" " +
"--- modified date: $lastModified - size: ${(latestObject.size)} B - key: ${latestObject.key}"
)
}
}
// -------------------------- helper functions ---------------------------------
@ -215,14 +187,3 @@ private fun Prov.verifyCertExpiration(enddate: String?, certName: String, expira
)
}
}
private fun getS3Objects(bucketName: String, prefixIn: String): ListObjectsResponse {
val request = ListObjectsRequest { bucket = bucketName; prefix = prefixIn }
return runBlocking {
S3Client { region = "eu-central-1" }.use { s3 ->
s3.listObjects(request)
}
}
}

View file

@ -8,7 +8,7 @@ function usage() {
function main() {
local cluster_name="${1}";
local domain_name="${2:-meissa-gmbh.de}";
local domain_name="${2:-meissa.de}";
/usr/local/bin/k3s-create-context.sh ${cluster_name} ${domain_name}
kubectl config use-context ${cluster_name}

View file

@ -4,8 +4,9 @@ set -o noglob
function main() {
local cluster_name="${1}"; shift
local domain_name="${1:-meissa.de}"; shift
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.meissa-gmbh.de -L 8002:localhost:8002 -L 6443:192.168.5.1:6443
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.${domain_name} -L 8002:localhost:8002 -L 6443:192.168.5.1:6443
}
main $1

View file

@ -4,8 +4,9 @@ set -o noglob
function main() {
local cluster_name="${1}"; shift
local domain_name="${1:-meissa.de}"; shift
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.meissa-gmbh.de
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.${domain_name}
}
main $1

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: hcloud
namespace: kube-system
stringData:
token: $HETZNER_API_TOKEN

View file

@ -0,0 +1,401 @@
# Version 2.6.0
# Source: hcloud-csi/templates/controller/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: hcloud-csi-controller
namespace: "kube-system"
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
automountServiceAccountToken: true
---
# Source: hcloud-csi/templates/core/storageclass.yaml
kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
name: hcloud-volumes
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: csi.hetzner.cloud
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
reclaimPolicy: "Delete"
---
# Source: hcloud-csi/templates/controller/clusterrole.yaml
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: hcloud-csi-controller
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
rules:
# attacher
- apiGroups: [""]
resources: [persistentvolumes]
verbs: [get, list, watch, update, patch]
- apiGroups: [""]
resources: [nodes]
verbs: [get, list, watch]
- apiGroups: [csi.storage.k8s.io]
resources: [csinodeinfos]
verbs: [get, list, watch]
- apiGroups: [storage.k8s.io]
resources: [csinodes]
verbs: [get, list, watch]
- apiGroups: [storage.k8s.io]
resources: [volumeattachments]
verbs: [get, list, watch, update, patch]
- apiGroups: [storage.k8s.io]
resources: [volumeattachments/status]
verbs: [patch]
# provisioner
- apiGroups: [""]
resources: [secrets]
verbs: [get, list]
- apiGroups: [""]
resources: [persistentvolumes]
verbs: [get, list, watch, create, delete, patch]
- apiGroups: [""]
resources: [persistentvolumeclaims, persistentvolumeclaims/status]
verbs: [get, list, watch, update, patch]
- apiGroups: [storage.k8s.io]
resources: [storageclasses]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [events]
verbs: [list, watch, create, update, patch]
- apiGroups: [snapshot.storage.k8s.io]
resources: [volumesnapshots]
verbs: [get, list]
- apiGroups: [snapshot.storage.k8s.io]
resources: [volumesnapshotcontents]
verbs: [get, list]
# resizer
- apiGroups: [""]
resources: [pods]
verbs: [get, list, watch]
# node
- apiGroups: [""]
resources: [events]
verbs: [get, list, watch, create, update, patch]
---
# Source: hcloud-csi/templates/controller/clusterrolebinding.yaml
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: hcloud-csi-controller
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: hcloud-csi-controller
subjects:
- kind: ServiceAccount
name: hcloud-csi-controller
namespace: "kube-system"
---
# Source: hcloud-csi/templates/controller/service.yaml
apiVersion: v1
kind: Service
metadata:
name: hcloud-csi-controller-metrics
namespace: "kube-system"
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
spec:
ports:
- name: metrics
port: 9189
selector:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
---
# Source: hcloud-csi/templates/node/service.yaml
apiVersion: v1
kind: Service
metadata:
name: hcloud-csi-node-metrics
namespace: "kube-system"
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: node
spec:
ports:
- name: metrics
port: 9189
selector:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: node
---
# Source: hcloud-csi/templates/node/daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: hcloud-csi-node
namespace: "kube-system"
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: node
app: hcloud-csi
spec:
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
app: hcloud-csi
template:
metadata:
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: node
app: hcloud-csi
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: instance.hetzner.cloud/is-root-server
operator: NotIn
values:
- "true"
tolerations:
- effect: NoExecute
operator: Exists
- effect: NoSchedule
operator: Exists
- key: CriticalAddonsOnly
operator: Exists
securityContext:
fsGroup: 1001
initContainers:
containers:
- name: csi-node-driver-registrar
image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.7.0
imagePullPolicy: IfNotPresent
args:
- --kubelet-registration-path=/var/lib/kubelet/plugins/csi.hetzner.cloud/socket
volumeMounts:
- name: plugin-dir
mountPath: /run/csi
- name: registration-dir
mountPath: /registration
resources:
limits: {}
requests: {}
- name: liveness-probe
image: registry.k8s.io/sig-storage/livenessprobe:v2.9.0
imagePullPolicy: IfNotPresent
volumeMounts:
- mountPath: /run/csi
name: plugin-dir
resources:
limits: {}
requests: {}
- name: hcloud-csi-driver
image: docker.io/hetznercloud/hcloud-csi-driver:v2.6.0 # x-release-please-version
imagePullPolicy: IfNotPresent
command: [/bin/hcloud-csi-driver-node]
volumeMounts:
- name: kubelet-dir
mountPath: /var/lib/kubelet
mountPropagation: "Bidirectional"
- name: plugin-dir
mountPath: /run/csi
- name: device-dir
mountPath: /dev
securityContext:
privileged: true
env:
- name: CSI_ENDPOINT
value: unix:///run/csi/socket
- name: METRICS_ENDPOINT
value: "0.0.0.0:9189"
- name: ENABLE_METRICS
value: "true"
ports:
- containerPort: 9189
name: metrics
- name: healthz
protocol: TCP
containerPort: 9808
resources:
limits: {}
requests: {}
livenessProbe:
failureThreshold: 5
initialDelaySeconds: 10
periodSeconds: 2
successThreshold: 1
timeoutSeconds: 3
httpGet:
path: /healthz
port: healthz
volumes:
- name: kubelet-dir
hostPath:
path: /var/lib/kubelet
type: Directory
- name: plugin-dir
hostPath:
path: /var/lib/kubelet/plugins/csi.hetzner.cloud/
type: DirectoryOrCreate
- name: registration-dir
hostPath:
path: /var/lib/kubelet/plugins_registry/
type: Directory
- name: device-dir
hostPath:
path: /dev
type: Directory
---
# Source: hcloud-csi/templates/controller/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hcloud-csi-controller
namespace: "kube-system"
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
app: hcloud-csi-controller
spec:
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app: hcloud-csi-controller
template:
metadata:
labels:
app.kubernetes.io/name: hcloud-csi
app.kubernetes.io/instance: hcloud-csi
app.kubernetes.io/component: controller
app: hcloud-csi-controller
spec:
serviceAccountName: hcloud-csi-controller
securityContext:
fsGroup: 1001
initContainers:
containers:
- name: csi-attacher
image: registry.k8s.io/sig-storage/csi-attacher:v4.1.0
imagePullPolicy: IfNotPresent
resources:
limits: {}
requests: {}
args:
- --default-fstype=ext4
volumeMounts:
- name: socket-dir
mountPath: /run/csi
- name: csi-resizer
image: registry.k8s.io/sig-storage/csi-resizer:v1.7.0
imagePullPolicy: IfNotPresent
resources:
limits: {}
requests: {}
volumeMounts:
- name: socket-dir
mountPath: /run/csi
- name: csi-provisioner
image: registry.k8s.io/sig-storage/csi-provisioner:v3.4.0
imagePullPolicy: IfNotPresent
resources:
limits: {}
requests: {}
args:
- --feature-gates=Topology=true
- --default-fstype=ext4
volumeMounts:
- name: socket-dir
mountPath: /run/csi
- name: liveness-probe
image: registry.k8s.io/sig-storage/livenessprobe:v2.9.0
imagePullPolicy: IfNotPresent
resources:
limits: {}
requests: {}
volumeMounts:
- mountPath: /run/csi
name: socket-dir
- name: hcloud-csi-driver
image: docker.io/hetznercloud/hcloud-csi-driver:v2.6.0 # x-release-please-version
imagePullPolicy: IfNotPresent
command: [/bin/hcloud-csi-driver-controller]
env:
- name: CSI_ENDPOINT
value: unix:///run/csi/socket
- name: METRICS_ENDPOINT
value: "0.0.0.0:9189"
- name: ENABLE_METRICS
value: "true"
- name: KUBE_NODE_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: spec.nodeName
- name: HCLOUD_TOKEN
valueFrom:
secretKeyRef:
name: hcloud
key: token
resources:
limits: {}
requests: {}
ports:
- name: metrics
containerPort: 9189
- name: healthz
protocol: TCP
containerPort: 9808
livenessProbe:
failureThreshold: 5
initialDelaySeconds: 10
periodSeconds: 2
successThreshold: 1
timeoutSeconds: 3
httpGet:
path: /healthz
port: healthz
volumeMounts:
- name: socket-dir
mountPath: /run/csi
volumes:
- name: socket-dir
emptyDir: {}
---
# Source: hcloud-csi/templates/core/csidriver.yaml
apiVersion: storage.k8s.io/v1
kind: CSIDriver
metadata:
name: csi.hetzner.cloud
spec:
attachRequired: true
fsGroupPolicy: File
podInfoOnMount: true
volumeLifecycleModes:
- Persistent

View file

@ -0,0 +1,11 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: hcloud-volumes-encrypted
provisioner: csi.hetzner.cloud
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
csi.storage.k8s.io/node-publish-secret-name: encryption-secret
csi.storage.k8s.io/node-publish-secret-namespace: kube-system

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: encryption-secret
namespace: kube-system
stringData:
encryption-passphrase: $HETZNER_ENCRYPTION_PASSPHRASE

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