Compare commits

...

161 commits

Author SHA1 Message Date
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
92 changed files with 5531 additions and 815 deletions

View file

@ -5,20 +5,23 @@ stages:
- test
- package
- publish
- release
before_script:
- echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x gradlew
- echo "------ commit tag ---------------"
- echo "------ commit info ---------------"
- echo $CI_COMMIT_TAG
- echo $CI_COMMIT_REF_NAME
- echo "----------------------------------"
cache:
paths:
- .gradle/wrapper
- .gradle/caches
build:
stage: build
script:
@ -29,6 +32,7 @@ build:
- build/libs/*.jar
expire_in: 1 week
test:
stage: test
image: docker:latest
@ -46,7 +50,7 @@ test:
- 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:
@ -54,40 +58,11 @@ test:
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:
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 jar
- ./gradlew -x assemble -x test -x jar uberjarDesktop
- ./gradlew -x assemble -x test -x jar uberjarServer
- ./gradlew -x assemble -x test -x jar uberjarSyspec
@ -96,42 +71,38 @@ uberjar:
- 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
- build/libs/*.jar
- build/libs/*.lst
publish-snapshot-lib:
publish-maven-package-to-gitlab:
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
- ./gradlew -x assemble -x test publishLibraryPublicationToGitlabRepository
publish-released-lib:
publish-maven-package-to-meissa:
stage: publish
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
- 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
- apt-get update -y
- apt-get install -y iputils-ping ssh
- ping -c 2 repo.prod.meissa.de
- ssh-keyscan repo.prod.meissa.de
- ./gradlew -x assemble -x test publishLibraryPublicationToMeissaRepository
release:
release-to-gitlab:
image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: publish
stage: release
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
@ -153,5 +124,19 @@ release:
--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\"}" \
release-to-meissa:
stage: release
allow_failure: true
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
script:
- apt-get update
- apt-get -yqq install curl
- ./gradlew createReleaseAndUploadAssets
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

@ -3,7 +3,8 @@ FROM ubuntu:latest
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 && adduser testuser sudo
RUN echo "testuser ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/testuser
USER testuser

View file

@ -60,7 +60,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

View file

@ -1,24 +1,25 @@
buildscript {
ext.kotlin_version = "1.7.0"
ext.kotlin_version = "1.7.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"
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
id "java"
id "java-test-fixtures"
}
apply plugin: "maven-publish"
apply plugin: "kotlinx-serialization"
group = "org.domaindrivenarchitecture.provs"
version = "0.17.1-SNAPSHOT"
version = "0.24.2-SNAPSHOT"
repositories {
mavenCentral()
@ -26,11 +27,19 @@ repositories {
java {
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
withSourcesJar()
withJavadocJar()
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
jar {
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
}
test {
// set properties for the tests
@ -57,12 +66,6 @@ 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 {
@ -71,7 +74,7 @@ dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
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')
@ -89,7 +92,7 @@ dependencies {
}
task uberjarDesktop(type: Jar) {
tasks.register('uberjarDesktop', Jar) {
from sourceSets.main.output
@ -110,7 +113,7 @@ task uberjarDesktop(type: Jar) {
}
task uberjarServer(type: Jar) {
tasks.register('uberjarServer', Jar) {
from sourceSets.main.output
@ -131,7 +134,7 @@ task uberjarServer(type: Jar) {
}
task uberjarSyspec(type: Jar) {
tasks.register('uberjarSyspec', Jar) {
from sourceSets.main.output
@ -155,7 +158,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,24 +171,21 @@ task installlocally {
}
}
task sourceJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
archiveClassifier.set("sources")
}
// 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"
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")
@ -194,8 +194,71 @@ publishing {
header(HttpHeaderAuthentication)
}
}
maven {
name = "meissa"
url = uri("https://repo.prod.meissa.de/api/packages/meissa/maven")
credentials(HttpHeaderCredentials) {
name = "Authorization"
if (System.getenv("CI_JOB_TOKEN") != null) {
def tokenFromEnv = System.getenv("RELEASE_TOKEN")
if (tokenFromEnv == null) {
println "Error: RELEASE_TOKEN not found"
} else {
mavenLocal()
value = "token " + tokenFromEnv
println "RELEASE_TOKEN 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("RELEASE_TOKEN")) {
// if RELEASE_TOKEN is missing, provide a dummy in order to avoid error "Could not get unknown property 'RELEASE_TOKEN' for Credentials [header: Authorization]" for other gradle tasks
ext.RELEASE_TOKEN = "RELEASE_TOKEN not provided in file \".gradle/gradle.properties\""
println "Error: RELEASE_TOKEN not found"
}
value = "token $RELEASE_TOKEN"
}
}
authentication {
header(HttpHeaderAuthentication)
}
}
}
}
tasks.register('createReleaseAndUploadAssets') {
dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
doLast {
def token = project.properties.get("RELEASE_TOKEN") ?: System.getenv("RELEASE_TOKEN")
if (token == null) {
throw new GradleException('No token found.')
}
def output1 = new ByteArrayOutputStream()
exec {
standardOutput = output1
def TAG = project.version
commandLine("sh", "-c", "curl -X 'POST' 'https://repo.prod.meissa.de/api/v1/repos/meissa/provs/releases' -H 'accept: application/json' -H 'Content-Type: application/json' -d '{ \"body\": \"Provides jar-files for release $TAG\\nAttention: The \\\"Source Code\\\"-files below are not up-to-date!\", \"tag_name\": \"$TAG\" }' -H \"Authorization: token $token\"")
}
def matches = output1 =~ /\{"id":(\d+?),/
if (!matches) {
throw new GradleException('id of release could not be parsed in: ' + output1)
}
def releaseId = matches.group(1)
println "Release=$releaseId"
def releaseApiUrl = "https://repo.prod.meissa.de/api/v1/repos/meissa/provs/releases"
exec { commandLine("sh", "-c", "find build/libs/ -type f -exec sha256sum {} \\; | sort > build/libs/sha256sum.lst") }
exec { commandLine("sh", "-c", "find build/libs/ -type f -exec sha512sum {} \\; | sort > build/libs/sha512sum.lst") }
exec { commandLine("sh", "-c", "curl -X 'POST' '$releaseApiUrl/$releaseId/assets' -H 'accept: application/json' -H \"Authorization: token $token\" -H 'Content-Type: multipart/form-data' -F 'attachment=@build/libs/provs-desktop.jar;type=application/x-java-archive'") }
exec { commandLine("sh", "-c", "curl -X 'POST' '$releaseApiUrl/$releaseId/assets' -H 'accept: application/json' -H \"Authorization: token $token\" -H 'Content-Type: multipart/form-data' -F 'attachment=@build/libs/provs-server.jar;type=application/x-java-archive'") }
exec { commandLine("sh", "-c", "curl -X 'POST' '$releaseApiUrl/$releaseId/assets' -H 'accept: application/json' -H \"Authorization: token $token\" -H 'Content-Type: multipart/form-data' -F 'attachment=@build/libs/provs-syspec.jar;type=application/x-java-archive'") }
exec { commandLine("sh", "-c", "curl -X 'POST' '$releaseApiUrl/$releaseId/assets' -H 'accept: application/json' -H \"Authorization: token $token\" -H 'Content-Type: multipart/form-data' -F 'attachment=@build/libs/sha256sum.lst;type=text/plain'") }
exec { commandLine("sh", "-c", "curl -X 'POST' '$releaseApiUrl/$releaseId/assets' -H 'accept: application/json' -H \"Authorization: token $token\" -H 'Content-Type: multipart/form-data' -F 'attachment=@build/libs/sha512sum.lst;type=text/plain'") }
}
}

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

@ -29,6 +29,6 @@ The success or failure is computed automatically in the following way:
## 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)

43
doc/GoForgejo_install.md Normal file
View file

@ -0,0 +1,43 @@
# 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/$(curl 'https://go.dev/VERSION?m=text').linux-amd64.tar.gz
extract latest version to ~/go
tar -C ~ -xzf go*.linux-amd64.tar.gz
APPEND='export PATH=$PATH:$HOME/go/bin'
echo $APPEND >> $HOME/.profile
## VScode optional - TODO!?!
Go extension autoinstall
install gpls, div, etc.
## 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

View file

@ -0,0 +1,49 @@
```plantuml
@startuml
autonumber
skinparam sequenceBox {
borderColor White
}
participant User
box "application" #LightBlue
participant Application
participant CliArgumentsParser
participant DesktopCliCommand
end box
box #White
participant CliUtils
participant "Prov (local or remote...)" as ProvInstance
end box
box "domain" #LightGreen
participant "DesktopService\n.provisionDesktopCommand" as DesktopService1
participant "DesktopService\n.provisionDesktop" as DesktopService2
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 -> DesktopService1 : provisionDesktopCommand ( provInstance, desktopCliCommand )
DesktopService1 -> ConfigRepository : getConfig
DesktopService1 -> DesktopService2 : provisionDesktop( config )
DesktopService2 -> Infrastructure_functions: Various calls like:
DesktopService2 -> Infrastructure_functions: install ssh, gpg, git ...
DesktopService2 -> Infrastructure_functions: installVirtualBoxGuestAdditions
DesktopService2 -> Infrastructure_functions: configureNoSwappiness, ...
@enduml
```

View file

@ -28,45 +28,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._

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(
@ -17,13 +16,3 @@ open class CliTargetParser(name: String) : ArgParser(name) {
"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,15 @@
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.quit
import java.io.FileNotFoundException
import java.nio.file.Files
import kotlin.io.path.Path
import kotlin.system.exitProcess
/**
@ -17,19 +23,39 @@ fun main(args: Array<String>) {
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 {
provisionDesktopCommand(prov, cmd)
getConfig(configFileName)
} catch (e: SerializationException) {
println(
"Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n"
"Error: File \"${configFileName}\" has an invalid format and or invalid data."
)
null
} 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"
"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
}
}
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 }
return DesktopCliCommand(
DesktopType.valueOf(module.name.uppercase()),
TargetCliCommand(
val targetCliCommand = TargetCliCommand(
target,
passwordInteractive
),
)
return DesktopCliCommand(
DesktopType.valueOf(module.name.uppercase()),
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,5 @@
package org.domaindrivenarchitecture.provs.desktop.domain
enum class DesktopOnlyModule {
TEAMS, FIREFOX, VERIFY
FIREFOX, VERIFY
}

View file

@ -9,15 +9,11 @@ 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()
prov.provisionDesktop(
internal fun Prov.provisionDesktopCommand(cmd: DesktopCliCommand, conf: DesktopConfig) = task {
provisionDesktop(
cmd.type,
conf.ssh?.keyPair(),
conf.gpg?.keyPair(),
@ -65,7 +61,7 @@ internal fun Prov.provisionDesktop(
}
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")
}
}
@ -94,31 +90,22 @@ fun Prov.provisionIdeDesktop(onlyModules: List<String>? = null) {
}
}
@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(SPELLCHECKING_DE)
aptInstall(BROWSER)
aptInstall(EMAIL_CLIENT)
installDeltaChat()
aptInstall(OFFICE_SUITE)
installZimWiki()
installNextcloudClient()
aptInstall(COMPARE_TOOLS)
// 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())) {
@ -155,11 +142,13 @@ fun Prov.provisionBasicDesktop(
installFirefox()
installGopass()
installGopassBridgeJsonApi()
configureGopass(publicGpgKey = gpg?.publicKey)
installGopassJsonApi()
downloadGopassBridge()
installRedshift()
installRedshift()
configureRedshift()
configureNoSwappiness()
configureBash()
installVirtualBoxGuestAdditions()

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,7 +7,9 @@ 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()
@ -41,20 +43,9 @@ fun Prov.installYq(
fun Prov.installKubectlAndTools(): ProvResult = 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")
}
@ -63,20 +54,47 @@ fun Prov.installKubectlAndTools(): ProvResult = task {
installDevopsScripts()
}
fun Prov.installKubectl(): ProvResult = task {
// see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
val kubectlVersion = "1.23.0"
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.23.0/bin/linux/amd64/kubectl.sha256
sha256sum = "2d0f5ba6faa787878b642c151ccb2c3390ce4c1e6c8e2b59568b3869ba407c4f"
)
cmd("sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl", dir = tmpDir)
}
fun Prov.configureKubectlBashCompletion(): ProvResult = 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("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 +105,7 @@ fun Prov.installDevopsScripts() {
createFileFromResource(
k3sContextFile,
"k3s-create-context.sh",
resourcePath,
RESOURCE_PATH,
"555",
sudo = true
)
@ -98,12 +116,13 @@ fun Prov.installDevopsScripts() {
createFileFromResource(
k3sConnectFile,
"k3s-connect.sh",
resourcePath,
RESOURCE_PATH,
"555",
sudo = true
)
}
}
fun Prov.installTerraform(): ProvResult = task {
val dir = "/usr/lib/tfenv/"
@ -114,8 +133,8 @@ 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)
cmd("tfenv install latest:^1.4.6", sudo = true)
cmd("tfenv use latest:^1.4.6", sudo = true)
}

View file

@ -13,12 +13,16 @@ fun Prov.installFirefox() = task {
// inspired by: https://www.omgubuntu.co.uk/2022/04/how-to-install-firefox-deb-apt-ubuntu-22-04
task("remove snap firefox") {
if (chk("snap list | grep firefox")) {
cmd("snap remove firefox", sudo = true)
}
}
aptInstall("software-properties-common")
cmd("add-apt-repository -y ppa:mozillateam/ppa", sudo = true)
// set prio in order to use ppa-firefox above snap
addTextToFile(
"\nPackage: *\n" +
"Pin: release o=LP-PPA-mozillateam\n" +
@ -34,4 +38,5 @@ fun Prov.installFirefox() = task {
)
aptInstall("firefox")
cmd("apt-get upgrade -y --allow-downgrades firefox", sudo = true)
}

View file

@ -2,16 +2,18 @@ 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.5",
enforceVersion: Boolean = false,
sha256sum: String = "0824d5110ff1e68bff1ba10c1be63acb67cb1ad8e3bccddd6b6fc989608beca8" // checksum for sha256sum version 8.30 (e.g. ubuntu 20.04)
sha256sum: String = "23ec10015c2643f22cb305859eb36d671094d463d2eb1798cc675e7bb06f4b39"
) = taskWithResult {
if (isPackageInstalled("gopass") && !enforceVersion) {
@ -34,29 +36,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 +71,41 @@ fun Prov.configureGopass(gopassRootFolder: String? = null) = taskWithResult() {
}
fun Prov.gopassMountStore(storeName: String, path: String) = task {
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"
}

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 {
fun Prov.installGopassJsonApi() = taskWithResult {
// see https://github.com/gopasspw/gopass-jsonapi
val gopassJsonApiVersion = "1.11.1"
val requiredGopassVersion = "1.14.4"
val sha256sum = "ec9976e39a468428ae2eb1e2e0b9ceccba7f60d66b8097e2425b0c07f4fed108"
val gopassJsonApiVersion = "1.15.5"
val requiredGopassVersion = "1.15.5"
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\""
)
)
}
} 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."
)
)
}
}
}
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)
}
}
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

@ -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"
@ -39,3 +39,5 @@ val CLOJURE_TOOLS = "leiningen"
val PASSWORD_TOOLS = "pwgen"
val SCREEN_TOOLS = "scrcpy"
val COMPARE_TOOLS = "meld"

View file

@ -7,35 +7,53 @@ 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)
}
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",
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)
}
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

@ -7,9 +7,8 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackag
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")
val clojureExtensions = setOf("betterthantomorrow.calva", "DavidAnson.vscode-markdownlint")
val pythonExtensions = setOf("ms-python.python")
prerequisitesVSCinstall()
@ -24,8 +23,6 @@ fun Prov.installVSC(vararg options: String) = task {
installExtensionsCode(pythonExtensions)
installExtensionsCodium(pythonExtensions)
}
provisionAdditionalToolsForVSCode()
}
@ -65,7 +62,7 @@ private fun Prov.installVSCodiumPackage() = task {
}
private fun Prov.installExtensionsCode(extensions: List<String>) = optional {
private fun Prov.installExtensionsCode(extensions: Set<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = cmd("code --install-extension $ext")
@ -74,7 +71,7 @@ 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.installExtensionsCodium(extensions: Set<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = cmd("codium --install-extension $ext")
@ -82,12 +79,3 @@ private fun Prov.installExtensionsCodium(extensions: List<String>) = optional {
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/")
}

View file

@ -68,11 +68,23 @@ open class Prov protected constructor(
private val internalResults = arrayListOf<ResultLine>()
private val infoTexts = arrayListOf<String>()
/**
* 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 base execution unit in provs. In the results overview it is represented by one line resp. result (of either success or failure).
* Returns success if no sub-tasks are called or if all subtasks finish with success.
*/
fun task(name: String? = null, taskLambda: Prov.() -> Unit): ProvResult {
printDeprecationWarningIfLevel0("task")
return evaluate(ResultMode.ALL, name) { taskLambda(); ProvResult(true) }
}
@ -81,27 +93,31 @@ open class Prov protected constructor(
* The returned result is included in the evaluation.
*/
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
*/
fun requireLast(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.LAST) { a() }
fun requireLast(name: String? = null, a: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("requireLast")
return evaluate(ResultMode.LAST, name) { a() }
}
/**
* defines a task, which always returns success
*/
fun optional(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.OPTIONAL) { a() }
fun optional(name: String? = null, a: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("optional")
return evaluate(ResultMode.OPTIONAL, name) { a() }
}
/**
* defines a task, which exits the overall execution on failure
*/
fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("exitOnFailure")
return evaluate(ResultMode.FAILEXIT) { a() }
}
@ -109,6 +125,7 @@ open class Prov protected constructor(
* 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 +271,8 @@ open class Prov protected constructor(
previousLevel = -1
exit = false
initProgress()
processor.open()
}
// pre-handling
@ -312,6 +331,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 +350,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 +464,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

@ -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

@ -6,88 +6,57 @@ 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
internal fun createRemoteProvInstance(
target: TargetCliCommand.RemoteTarget?,
password: Secret? = null
): Prov {
val prov =
if (sshWithKey) {
remote(host, remoteUser)
return if (target != null) {
remote(target.host, target.user, target.password ?: password)
} 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)
throw IllegalArgumentException(
"Error: no valid remote target (host & user) was specified!"
)
}
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 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()
}
/**
* Wrapper for exitProcess, which allows e.g. mocking for test purposes
*/
fun quit(status: Int): Nothing {
exitProcess(status)
}

View file

@ -13,13 +13,6 @@ class UbuntuProv internal constructor(
progressType: ProgressType
) : 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))
}

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

@ -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)
}
}
}

View file

@ -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) {

View file

@ -45,8 +45,7 @@ fun Prov.gitClone(
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:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s github.com", // (RSA)
"SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA)
"SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519)
)

View file

@ -20,7 +20,10 @@ fun Prov.aptInstall(packages: String, ignoreAlreadyInstalled: Boolean = true): P
if (!allInstalled) {
if (!isPackageInstalled(packages)) {
if (!aptInit) {
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,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")
val p = Prov.newInstance(name = "FileSecretSource", progressType = ProgressType.NONE)
return p.getSecret("cat " + input) ?: throw Exception("Failed to get secret.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "FileSecretSource")
val p = Prov.newInstance(name = "FileSecretSource", progressType = ProgressType.NONE)
return p.getSecret("cat " + input)
}
}

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
@ -13,7 +14,7 @@ class GopassSecretSource(path: String) : SecretSource(path) {
return secretNullable() ?: throw Exception("Failed to get \"$input\" secret from gopass.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "GopassSecretSource for $input")
val p = Prov.newInstance(name = "GopassSecretSource for $input", progressType = ProgressType.NONE)
return p.getSecret("gopass show -f $input", 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")
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
return p.getSecret("pass " + input) ?: throw Exception("Failed to get secret.")
}
override fun secretNullable(): Secret? {
val p = Prov.newInstance(name = "PassSecretSource")
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
return p.getSecret("pass " + input)
}
}

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

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,8 @@
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.quit
import org.domaindrivenarchitecture.provs.server.domain.ServerType
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sCliCommand
import org.domaindrivenarchitecture.provs.server.domain.k3s.provisionK3sCommand
@ -25,14 +27,16 @@ fun main(args: Array<String>) {
val cmd = CliArgumentsParser("provs-server.jar 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,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

@ -12,13 +12,13 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
val grafanaConfigResolved: GrafanaAgentConfigResolved? = findK8sGrafanaConfig(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, applicationFile)
} else {
provisionGrafana(cli.onlyModules, grafanaConfigResolved)
}
@ -30,7 +30,8 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
fun Prov.provisionK3s(
k3sConfig: K3sConfig,
grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
applicationFileName: ApplicationFileName? = null) = task {
applicationFile: ApplicationFile? = null
) = task {
if (k3sConfig.reprovision) {
deprovisionK3sInfra()
@ -52,8 +53,8 @@ fun Prov.provisionK3s(
provisionGrafanaAgent(grafanaConfigResolved)
}
if (applicationFileName != null) {
provisionK3sApplication(applicationFileName)
if (applicationFile != null) {
provisionK3sApplication(applicationFile)
}
if (!k3sConfig.reprovision) {
@ -63,7 +64,8 @@ fun Prov.provisionK3s(
private fun Prov.provisionGrafana(
onlyModules: List<String>?,
grafanaConfigResolved: GrafanaAgentConfigResolved?) = task {
grafanaConfigResolved: GrafanaAgentConfigResolved?
) = task {
if (onlyModules != null && onlyModules.contains(ServerOnlyModule.GRAFANA.name.lowercase())) {
if (grafanaConfigResolved == null) {

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

@ -5,10 +5,7 @@ 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 --------------------------------
@ -25,8 +22,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")
@ -78,25 +75,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"))
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.13.7-native-manifest.yaml"))
repeatTaskUntilSuccess(6, 10) {
applyK3sFileFromResourceTemplate(
File(k3sManualManifestsDir, "metallb-config.yaml"),
k3sConfigMap,
alternativeResourceName = File(metallbConfigResourceFileName)
)
}
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-l2advertisement.yaml"))
// traefik
if (k3sConfig.isDualStack()) {
@ -150,9 +157,9 @@ fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null) =
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 +223,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

@ -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,18 +1,19 @@
apiVersion: v1
kind: ConfigMap
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: public
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: public
protocol: layer2
spec:
addresses:
- ${node_ipv4}/32
- ${node_ipv6}/128
- name: private
protocol: layer2
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: private
namespace: metallb-system
spec:
addresses:
- ${loopback_ipv4}/32
- ${loopback_ipv6}/128

View file

@ -1,16 +1,17 @@
apiVersion: v1
kind: ConfigMap
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: public
namespace: metallb-system
name: config
data:
config: |
address-pools:
- name: public
protocol: layer2
spec:
addresses:
- ${node_ipv4}/32
- name: private
protocol: layer2
---
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: private
namespace: metallb-system
spec:
addresses:
- ${loopback_ipv4}/32

View file

@ -0,0 +1,9 @@
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: layer2
namespace: metallb-system
spec:
ipAddressPools:
- private
- public

View file

@ -1,6 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: metallb-system
labels:
app: metallb

View file

@ -11,4 +11,4 @@ command:
- command: "python3 --version"
- command: "pip3 --version"
- command: "terraform --version"
out: "1.0.8"
out: "1.4.6"

View file

@ -1,9 +1,21 @@
package org.domaindrivenarchitecture.provs.configuration.application
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
private fun parseTarget(
args: Array<String>
): TargetCliCommand {
val parser = CliTargetParser("provs")
parser.parse(args)
return TargetCliCommand(parser.target, parser.passwordInteractive)
}
internal class CliTargetParserTest {
@Test

View file

@ -0,0 +1,91 @@
package org.domaindrivenarchitecture.provs.configuration.application
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import org.domaindrivenarchitecture.provs.framework.core.*
import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerUbuntuHostProcessor
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
class ProvWithSudoKtTest {
@ExtensiveContainerTest
fun test_ensureSudoWithoutPassword_local_Prov() {
mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("testuserpw")
// given
val containerName = "prov-test-sudo-no-pw"
local().provideContainer(containerName, "ubuntu_plus_user")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
startMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
sudo = true,
dockerImage = "ubuntu_plus_user"
),
progressType = ProgressType.NONE
)
prov.deleteFile("/etc/sudoers.d/testuser", sudo = true) // remove no password required config
// when
val canSudo1 = prov.currentUserCanSudoWithoutPassword()
prov.ensureSudoWithoutPassword(null)
val canSudo2 = prov.currentUserCanSudoWithoutPassword()
// then
assertFalse(canSudo1)
assertTrue(canSudo2)
unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
}
@ExtensiveContainerTest
fun test_ensureSudoWithoutPassword_remote_Prov() {
// given
val containerName = "prov-test-sudo-no-pw-ssh"
val password = Secret("testuserpw")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
startMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
sudo = true,
dockerImage = "ubuntu_plus_user",
options = "--expose=22"
),
progressType = ProgressType.NONE
)
prov.makeCurrentUserSudoerWithoutPasswordRequired(password)
prov.task {
aptInstall("openssh-server")
cmd("sudo service ssh start")
deleteFile("/etc/sudoers.d/testuser", sudo = true) // remove no password required config
}
val ip = local().cmd("sudo docker inspect -f \"{{ .NetworkSettings.IPAddress }}\" $containerName").out?.trim()
?: throw IllegalStateException("Ip not found")
val remoteProvBySsh = remote(ip, "testuser", password)
// when
val canSudo1 = remoteProvBySsh.currentUserCanSudoWithoutPassword()
prov.ensureSudoWithoutPassword(password)
val canSudo2 = prov.currentUserCanSudoWithoutPassword()
// then
assertFalse(canSudo1)
assertTrue(canSudo2)
}
}

View file

@ -4,10 +4,11 @@ import io.mockk.*
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.retrievePassword
import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.test.tags.NonCi
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
@ -23,8 +24,8 @@ internal class CliTargetCommandKtTest {
mockkStatic(::remote)
every { remote(any(), any(), any(), any()) } returns Prov.newInstance(PrintOnlyProcessor())
mockkStatic(::retrievePassword)
every { retrievePassword(any()) } returns Secret("sec")
mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec")
}
@AfterAll
@ -33,12 +34,13 @@ internal class CliTargetCommandKtTest {
unmockkObject(Prov)
unmockkStatic(::local)
unmockkStatic(::remote)
unmockkStatic(::retrievePassword)
unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
}
}
@Test
@NonCi
fun createProvInstance_local() {
// given
val cliCommand = TargetCliCommand("local", false)

View file

@ -1,6 +1,8 @@
package org.domaindrivenarchitecture.provs.configuration.infrastructure
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileRepository
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileRepository
import org.domaindrivenarchitecture.provs.server.infrastructure.DefaultApplicationFileRepository
import org.junit.jupiter.api.Test
@ -12,8 +14,8 @@ internal class DefaultConfigFileRepositoryKtTest {
@Test
fun assertExistsThrowsRuntimeException() {
// when
val invalidFileName = ApplicationFileName("iDontExist")
val repo: ApplicationFileRepository = DefaultApplicationFileRepository()
val invalidFileName = ConfigFileName("iDontExist")
val repo: ConfigFileRepository = DefaultConfigFileRepository()
// then
val exception = assertThrows<RuntimeException>(
@ -21,18 +23,18 @@ internal class DefaultConfigFileRepositoryKtTest {
) { repo.assertExists(invalidFileName) }
assertEquals(
"Application file iDontExist not found. Please check if path is correct.",
"Config file not found. Please check if path is correct.",
exception.message)
}
@Test
fun assertExistsPasses() {
// given
val validFileName = "src/test/resources/existing_file"
val validFileName = "src/test/resources/existing-file"
// when
val validFile = ApplicationFileName(File(validFileName).path)
val repo: ApplicationFileRepository = DefaultApplicationFileRepository()
val validFile = ConfigFileName(File(validFileName).path)
val repo: ConfigFileRepository = DefaultConfigFileRepository()
repo.assertExists(validFile)
// then

View file

@ -4,16 +4,21 @@ import ch.qos.logback.classic.Level
import io.mockk.*
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.desktop.domain.*
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopCliCommand
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopConfig
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType
import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktop
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.*
import org.domaindrivenarchitecture.provs.framework.core.cli.retrievePassword
import org.domaindrivenarchitecture.provs.framework.core.cli.getPasswordToConfigureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import org.domaindrivenarchitecture.provs.framework.core.processors.DummyProcessor
import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.io.ByteArrayOutputStream
import java.io.PrintStream
@ -52,8 +57,8 @@ internal class ApplicationKtTest {
cmd = "mocked command"
)
mockkStatic(::retrievePassword)
every { retrievePassword(any()) } returns Secret("sec")
mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec")
}
@Suppress("unused") // false positive
@ -65,7 +70,7 @@ internal class ApplicationKtTest {
unmockkStatic(::remote)
unmockkStatic(::getConfig)
unmockkStatic(Prov::provisionDesktop)
unmockkStatic(::retrievePassword)
unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
}
}
@ -91,6 +96,9 @@ internal class ApplicationKtTest {
@Test
fun prints_error_message_if_config_not_found() {
mockkStatic(::quit)
every { quit(any()) } throws RuntimeException("mockked")
// given
setRootLoggingLevel(Level.OFF)
@ -103,21 +111,28 @@ internal class ApplicationKtTest {
System.setErr(PrintStream(errContent))
// when
assertThrows<RuntimeException> {
main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml"))
}
// then
System.setOut(originalOut)
System.setErr(originalErr)
val expectedOutput =
"Error: File\u001B[31m idontexist.yaml \u001B[0m was not found.Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m idontexist.yaml \u001B[0m and change the content according to your needs."
"Error: File\u001B[31m idontexist.yaml \u001B[0m was not found.Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m idontexist.yaml \u001B[0m and change the content according to your needs.No suitable config found."
assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", ""))
verify(exactly = 0) { any<Prov>().provisionDesktop(any(), any(), any(), any(), any(), any()) }
unmockkStatic(::quit)
}
@Test
fun prints_error_message_if_config_not_parsable() {
mockkStatic(::quit)
every { quit(any()) } throws RuntimeException("mockked")
// given
setRootLoggingLevel(Level.OFF)
@ -130,16 +145,20 @@ internal class ApplicationKtTest {
System.setErr(PrintStream(errContent))
// when
assertThrows<RuntimeException> {
main(arrayOf("basic", "someuser@remotehost", "-c", "src/test/resources/invalid-desktop-config.yaml"))
}
// then
System.setOut(originalOut)
System.setErr(originalErr)
val expectedOutput =
"Error: File \"src/test/resources/invalid-desktop-config.yaml\" has an invalid format and or invalid data."
"Error: File \"src/test/resources/invalid-desktop-config.yaml\" has an invalid format and or invalid data.No suitable config found."
assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", ""))
verify(exactly = 0) { any<Prov>().provisionDesktop(any(), any(), any(), any(), any(), any()) }
unmockkStatic(::quit)
}
}

View file

@ -14,13 +14,4 @@ internal class CliArgumentsParserTest {
assertEquals(null, cli.configFile)
assertEquals(true, cli.target.isValidLocalhost())
}
@Test
fun parse_cliCommand_with_onlyModule_teams_and_local_target() {
val cli = CliArgumentsParser("test").parseCommand(args = arrayOf("ide", "local", "-o", "teams"))
assertTrue(cli.isValid())
assertEquals(true, cli.target.isValidLocalhost())
assertEquals(true, cli.onlyModules?.contains("teams"))
}
}

View file

@ -1,9 +1,16 @@
package org.domaindrivenarchitecture.provs.desktop.domain
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerUbuntuHostProcessor
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
@ -11,6 +18,34 @@ import org.junit.jupiter.api.Test
internal class DesktopServiceKtTest {
@ExtensiveContainerTest
fun provisionLocalDesktop_fails_if_user_cannot_sudo_without_password() {
// given
val containerName = "prov-test-sudo-no-pw"
local().provideContainer(containerName, "ubuntu_plus_user")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
startMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
sudo = true,
dockerImage = "ubuntu_plus_user"
),
progressType = ProgressType.NONE
)
prov.deleteFile("/etc/sudoers.d/testuser", sudo = true) // remove no password required
// when
Assertions.assertThrows(Exception::class.java) {
prov.provisionDesktop(
DesktopType.BASIC,
gitUserName = "testuser",
gitEmail = "testuser@test.org",
onlyModules = null
)
}
}
@ExtensiveContainerTest
@Disabled("Takes very long, enable if you want to test a desktop setup")
fun provisionDesktop() {
// given
val prov = defaultTestContainer()
@ -52,28 +87,6 @@ internal class DesktopServiceKtTest {
// then
assertTrue(res.success)
}
@ExtensiveContainerTest
fun provisionDesktopFromConfigFile() {
// given
val prov = defaultTestContainer()
// when
// in order to test DesktopType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
val config = getConfig("src/test/resources/desktop-config-example.json")
val res = prov.provisionDesktop(
DesktopType.BASIC,
config.ssh?.keyPair(),
config.gpg?.keyPair(),
config.gitUserName,
config.gitEmail,
onlyModules = null
)
// then
assertTrue(res.success)
}
}

View file

@ -8,6 +8,7 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileC
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
internal class DevOpsKtTest {
@ -34,4 +35,17 @@ internal class DevOpsKtTest {
defaultTestContainer().checkFile("/etc/bash_completion.d/kubernetes", sudo = true)
)
}
@ExtensiveContainerTest
@Disabled("Part of test installKubectlAndTools, but can be tested separately by this test if required")
fun installKubectl() {
// given
val prov = defaultTestContainer()
// when
val res = prov.installKubectl()
// then
assertTrue(res.success)
}
}

View file

@ -1,23 +1,52 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.checkPackageInstalled
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
internal class FirefoxKtTest {
@Test
// Attention: this test does not test full functionality of installFirefox, e.g. does not test
// remove snap, as this test runs against a container which does not have snap-firefox installed
@ExtensiveContainerTest
fun installFirefox() {
// when
val res = defaultTestContainer().installFirefox()
val result = defaultTestContainer().session {
installFirefox()
checkPackageInstalled("firefox")
}
// then
assertTrue(res.success)
assertTrue(result.success)
}
val ffIsInstalled = defaultTestContainer().isPackageInstalled("firefox")
assertTrue(ffIsInstalled)
@Test
@Disabled("Update connection details,then enable and run manually")
fun installFirefox_remotely() {
val host = "192.168.56.123"
val user = "user"
var firefoxVersion = ""
// when
val result = remote(
host,
user,
/* remove for ssh authentication */
PromptSecretSource("Remote password for user $user").secret()
).session {
installFirefox()
firefoxVersion = cmd("apt list firefox --installed").out ?: ""
checkPackageInstalled("firefox")
}
// then
assertTrue(result.success)
println("Firefox: $firefoxVersion")
assertTrue(firefoxVersion.contains("build") && !firefoxVersion.contains("snap"))
}
}

View file

@ -1,16 +1,12 @@
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.core.docker.exitAndRmContainer
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.KeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.domaindrivenarchitecture.provs.test.tags.NonCi
import org.domaindrivenarchitecture.provs.test_keys.privateGPGSnakeoilKey
@ -27,11 +23,10 @@ internal class GopassBridgeKtTest {
fun test_downloadGopassBridge() {
// given
local().exitAndRmContainer("provs_test")
val a = defaultTestContainer()
a.aptInstallCurl()
val prov = defaultTestContainer()
// when
val res = a.downloadGopassBridge()
val res = prov.downloadGopassBridge()
// then
assertTrue(res.success)
@ -42,105 +37,81 @@ internal class GopassBridgeKtTest {
fun test_install_and_configure_GopassBridgeJsonApi() {
// given
local().exitAndRmContainer("provs_test")
val a = defaultTestContainer()
val preparationResult = a.task {
aptInstallCurl()
val prov = defaultTestContainer()
val preparationResult = prov.task {
configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true,
skipIfExistin = false
)
installGopass()
if (!chk("gopass ls")) {
// configure/init gopass in default location with gpg-key-fingerprint of snakeoil keys
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
} else {
ProvResult(true, out = "gopass already configured")
}
configureGopass(publicGpgKey = Secret(publicGPGSnakeoilKey()))
}
assertTrue(preparationResult.success)
// when
val res = a.task {
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
val res = prov.task {
installGopassJsonApi()
configureGopassJsonApi()
}
// then
assertTrue(res.success)
}
@ContainerTest
@ExtensiveContainerTest
@Test
@NonCi
@Disabled // long running test (> 1 min); if needed enable test and run manually
fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_jsonapi_version_installed() {
// given
val a = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
val preparationResult = a.task {
aptInstallCurl()
val prov = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
val preparationResult = prov.task {
configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true,
skipIfExistin = false
)
installGopass("1.11.0", enforceVersion = true, "1ec9e0dfcfd9bcc241943e1a7d92f31bf3e66bb16f61ae5d079981325c31baa6")
if (!chk("gopass ls")) {
// configure gopass in default location with gpg-key-fingerprint of snakeoil keys
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
} else {
ProvResult(true, out = "gopass already configured")
}
configureGopass(publicGpgKey = Secret(publicGPGSnakeoilKey()))
}
assertTrue(preparationResult.success)
// when
val res = a.task {
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
val res = prov.task {
installGopassJsonApi()
configureGopassJsonApi()
}
// then
assertFalse(res.success)
}
@ContainerTest
@ExtensiveContainerTest
@Test
@NonCi
@Disabled // long running test (> 1 min); if needed enable test and run manually
@Disabled // long running test (> 1 min); if needed, enable test and run manually
fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_version_installed() {
// given
val a = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
val preparationResult = a.task {
aptInstallCurl()
val prov = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
val preparationResult = prov.task {
configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true,
skipIfExistin = false
)
installGopass("1.9.0", enforceVersion = true, "fe13ef810d7fe200495107161e99eac081368aa0ce5e53971b1bd47a64eba4db")
if (!chk("gopass ls")) {
// configure gopass in default location with gpg-key-fingerprint of snakeoil keys
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
} else {
ProvResult(true, out = "gopass already configured")
}
configureGopass(publicGpgKey = Secret(publicGPGSnakeoilKey()))
}
assertTrue(preparationResult.success)
// when
val res = a.task {
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
val res = prov.task {
installGopassJsonApi()
configureGopassJsonApi()
}
// then
assertFalse(res.success)
}
private fun Prov.aptInstallCurl() = task {
cmd("apt-get update", sudo = true)
aptInstall("curl")
}
}

View file

@ -1,10 +1,8 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.KeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint
@ -12,8 +10,6 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.domaindrivenarchitecture.provs.test_keys.privateGPGSnakeoilKey
import org.domaindrivenarchitecture.provs.test_keys.publicGPGSnakeoilKey
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertFalse
@ -25,7 +21,7 @@ internal class GopassKtTest {
fun test_configureGopass_fails_with_path_starting_with_tilde() {
// when
val res = defaultTestContainer().task {
deleteFile(".config/gopass/config.yml")
deleteFile(".config/gopass/config")
configureGopass("~/somedir")
}
@ -36,32 +32,27 @@ internal class GopassKtTest {
@ExtensiveContainerTest
fun test_installAndConfigureGopassAndMountStore() {
// given
val a = defaultTestContainer()
val prov = defaultTestContainer()
val gopassRootDir = ".password-store"
a.aptInstall("wget git gnupg")
a.createDir(gopassRootDir, "~/")
a.cmd("git init", "~/$gopassRootDir")
val fpr = a.gpgFingerprint(publicGPGSnakeoilKey())
println("+++++++++++++++++++++++++++++++++++++ $fpr +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
a.createFile("~/" + gopassRootDir + "/.gpg-id", fpr)
a.createDir("exampleStoreFolder", "~/")
a.createFile("~/exampleStoreFolder/.gpg-id", fpr)
a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
// when
val res = a.installGopass()
val res2 = a.configureGopass(a.userHome() + gopassRootDir)
val res3 = a.gopassMountStore("exampleStore", "~/exampleStoreFolder")
val res = prov.task("test_installAndConfigureGopassAndMountStore") {
installGopass()
configureGopass(prov.userHome() + gopassRootDir)
gopassInitStoreFolder("~/exampleStoreFolder")
gopassInitStoreFolder("~/exampleStoreFolder") // check idem-potency
gopassMountStore("exampleStore", "~/exampleStoreFolder")
gopassMountStore("exampleStore", "~/exampleStoreFolder") // check idem-potency
prov.cmd("gopass ls")
prov.cmd("gopass sync")
}
// then
a.fileContent("~/.config/gopass/config.yml") // displays the content in the logs
prov.fileContent("~/.config/gopass/config") // displays the content in the logs
assertTrue(res.success)
assertTrue(res2.success)
assertTrue(res3.success)
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "/home/testuser/.password-store"))
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "exampleStore"))
assertTrue(prov.fileContainsText("~/.config/gopass/config", "/home/testuser/.password-store"))
assertTrue(prov.fileContainsText("~/.config/gopass/config", "exampleStore"))
assertTrue(prov.checkDir(".git", gopassRootDir))
}
@Test
@ -74,10 +65,10 @@ internal class GopassKtTest {
val privateKey = GopassSecretSource("path-to/priv.key").secret()
// given
val a = remote(host, user)
val prov = remote(host, user)
// when
val res = a.task {
val res = prov.task {
configureGpgKeys(
KeyPair(
pubKey,
@ -93,11 +84,12 @@ internal class GopassKtTest {
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init " + gpgFingerprint(pubKey.plain())) // gopass init in default location with gpg-key-fingerprint of given key
}
downloadGopassBridge()
installGopassBridgeJsonApi()
configureGopassBridgeJsonApi()
installGopassJsonApi()
configureGopassJsonApi()
}
// then
assertTrue(res.success)
}
}

View file

@ -40,7 +40,7 @@ internal class K3SDesktopConfigRepositoryKtTest {
@Test
fun getConfig_fails_due_to_missing_file() {
val exception = assertThrows<FileNotFoundException> {
getK3sConfig(ConfigFileName("src/test/resources/Idonotexist.yaml"))
getConfig("src/test/resources/Idonotexist.yaml")
}
assertEquals(FileNotFoundException::class.java, exception.javaClass)
}

View file

@ -4,14 +4,12 @@ import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
class MsTeamsKtTest {
internal class NextcloudClientTest {
@ExtensiveContainerTest
fun installMsTeams() {
// given
val a = defaultTestContainer()
fun test_installNextcloudClient() {
// when
val res = a.task { installMsTeams() }
val res = defaultTestContainer().installNextcloudClient()
// then
assertTrue(res.success)
}

View file

@ -2,25 +2,12 @@ package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
internal class VSCodeKtTest {
@ExtensiveContainerTest
fun provisionAdditionalTools() {
// given
defaultTestContainer().aptInstall("curl unzip")
// when
val res = defaultTestContainer().provisionAdditionalToolsForVSCode()
// then
assertTrue(res.success)
}
@Test
@Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker")
fun installVSC() {

View file

@ -254,12 +254,12 @@ internal class ProvTest {
}
// given
// additional methods to be used in the tests below
fun Prov.checkPrereq_evaluateToFailure() = requireLast {
ProvResult(false, err = "This is a test error.")
}
fun Prov.methodThatProvidesSomeOutput() = requireLast {
fun Prov.testMethodForOutputTest_with_mode_requireLast() = requireLast {
if (!checkPrereq_evaluateToFailure().success) {
sh(
@ -273,6 +273,17 @@ internal class ProvTest {
sh("echo -End test-")
}
fun Prov.testMethodForOutputTest_nested_with_failure() = taskWithResult {
taskWithResult(name = "sub1") {
taskWithResult {
ProvResult(true)
}
ProvResult(false, err = "Iamanerrormessage")
}
cmd("echo -End test-")
}
@Test
@NonCi
fun prov_prints_correct_output_for_overall_success() {
@ -290,7 +301,9 @@ internal class ProvTest {
// when
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE)
.methodThatProvidesSomeOutput()
.session {
testMethodForOutputTest_with_mode_requireLast()
}
// then
System.setOut(originalOut)
@ -300,13 +313,14 @@ internal class ProvTest {
val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" +
"> \u001B[92mSuccess\u001B[0m -- methodThatProvidesSomeOutput (requireLast) \n" +
"---> \u001B[93mFAILED\u001B[0m -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" +
"---> \u001B[92mSuccess\u001B[0m -- sh \n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -Start test-]\n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo Some output]\n" +
"---> \u001B[92mSuccess\u001B[0m -- sh \n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -End test-]\n" +
"> \u001B[92mSuccess\u001B[0m -- session \n" +
"---> \u001B[92mSuccess\u001B[0m -- testMethodForOutputTest_with_mode_requireLast (requireLast) \n" +
"------> \u001B[93mFAILED\u001B[0m -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" +
"------> \u001B[92mSuccess\u001B[0m -- sh \n" +
"---------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -Start test-]\n" +
"---------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo Some output]\n" +
"------> \u001B[92mSuccess\u001B[0m -- sh \n" +
"---------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -End test-]\n" +
"----------------------------------------------------------------------------------------------------\n" +
"Overall > \u001B[92mSuccess\u001B[0m\n" +
"============================================ SUMMARY END ===========================================\n" +
@ -317,7 +331,7 @@ internal class ProvTest {
@Test
@NonCi
fun prov_prints_correct_output_for_failure() {
fun prov_prints_correct_output_for_nested_calls_with_failure() {
// given
setRootLoggingLevel(Level.OFF)
@ -331,8 +345,9 @@ internal class ProvTest {
System.setErr(PrintStream(errContent))
// when
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE)
.checkPrereq_evaluateToFailure()
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE).session {
testMethodForOutputTest_nested_with_failure()
}
// then
System.setOut(originalOut)
@ -342,7 +357,14 @@ internal class ProvTest {
val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" +
"> \u001B[91mFAILED\u001B[0m -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" +
"> \u001B[91mFAILED\u001B[0m -- session \n" +
"---> \u001B[91mFAILED\u001B[0m -- testMethodForOutputTest_nested_with_failure \n" +
"------> \u001B[91mFAILED\u001B[0m -- sub1 \n" +
"---------> \u001B[92mSuccess\u001B[0m -- testMethodForOutputTest_nested_with_failure \n" +
"---------> \u001B[91mFAILED\u001B[0m -- <<returned result>> -- Error: Iamanerrormessage\n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -End test-]\n" +
"----------------------------------------------------------------------------------------------------\n" +
"Overall > \u001B[91mFAILED\u001B[0m \n" +
"============================================ SUMMARY END ===========================================\n" +
"\n"
@ -604,9 +626,10 @@ internal class ProvTest {
val prov = Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE)
// when
prov.task {
prov.session {
addInfoText("Text1")
addInfoText("Text2\nwith newline")
ProvResult(true)
}
// then
@ -617,7 +640,7 @@ internal class ProvTest {
val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" +
"> \u001B[92mSuccess\u001B[0m -- infoText_is_printed_correctly \n" +
"> \u001B[92mSuccess\u001B[0m -- session \n" +
"+++++++++++++++++++++++++++++++++++ \u001B[94mAdditional information\u001B[0m +++++++++++++++++++++++++++++++++++++++\n" +
"Text1\n" +
"Text2\n" +
@ -629,5 +652,171 @@ internal class ProvTest {
}
}
// method to be used in the next test
fun Prov.testMethodForOutputTest_with_returned_results() = taskWithResult {
taskWithResult(name = "sub1") {
taskWithResult("sub2a") {
ProvResult(true)
}
taskWithResult("sub2b") {
ProvResult(false, err = "error msg A for sub2b should be shown as result of sub2b")
}
optional("sub2c-optional") {
taskWithResult("sub3a-taskWithResult") {
addResultToEval(
ProvResult(
false,
err = "returned-result - error msg B should be once in output - in addResultToEval"
)
)
}
}
requireLast("sub2d-requireLast") {
taskWithResult("sub3b-taskWithResult without error message") {
ProvResult(false) // no error message
}
}
task("sub2e-task") {
addResultToEval(ProvResult(true))
ProvResult(
false,
err = "error should NOT be in output as results of task (not taskWithResult) are ignored"
)
}
taskWithResult("sub2f-taskWithResult") {
ProvResult(
false,
err = "returned-result - error msg C should be once in output - at the end of sub3taskWithResult "
)
}
ProvResult(false, err = "returned-result - error msg D should be once in output - at the end of sub1 ")
}
}
@Test
@NonCi
fun prov_prints_correct_output_for_returned_results() {
// given
setRootLoggingLevel(Level.OFF)
val outContent = ByteArrayOutputStream()
val errContent = ByteArrayOutputStream()
val originalOut = System.out
val originalErr = System.err
System.setOut(PrintStream(outContent))
System.setErr(PrintStream(errContent))
// when
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE)
.testMethodForOutputTest_with_returned_results()
// then
System.setOut(originalOut)
System.setErr(originalErr)
println(outContent.toString())
val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" +
"> \u001B[91mFAILED\u001B[0m -- testMethodForOutputTest_with_returned_results \n" +
"---> \u001B[91mFAILED\u001B[0m -- sub1 \n" +
"------> \u001B[92mSuccess\u001B[0m -- sub2a \n" +
"------> \u001B[91mFAILED\u001B[0m -- sub2b -- Error: error msg A for sub2b should be shown as result of sub2b\n" +
"------> \u001B[92mSuccess\u001B[0m -- sub2c-optional \n" +
"---------> \u001B[93mFAILED\u001B[0m -- sub3a-taskWithResult \n" +
"------------> \u001B[93mFAILED\u001B[0m -- addResultToEval -- Error: returned-result - error msg B should be once in output - in addResultToEval\n" +
"------> \u001B[91mFAILED\u001B[0m -- sub2d-requireLast \n" +
"---------> \u001B[91mFAILED\u001B[0m -- sub3b-taskWithResult without error message \n" +
"------> \u001B[92mSuccess\u001B[0m -- sub2e-task \n" +
"---------> \u001B[92mSuccess\u001B[0m -- addResultToEval \n" +
"------> \u001B[91mFAILED\u001B[0m -- sub2f-taskWithResult -- Error: returned-result - error msg C should be once in output - at the end of sub3taskWithResult \n" +
"------> \u001B[91mFAILED\u001B[0m -- <<returned result>> -- Error: returned-result - error msg D should be once in output - at the end of sub1 \n" +
"----------------------------------------------------------------------------------------------------\n" +
"Overall > \u001B[91mFAILED\u001B[0m \n" +
"============================================ SUMMARY END ===========================================\n" +
"\n"
assertEquals(expectedOutput, outContent.toString().replace("\r", ""))
}
@Test
fun session_on_top_level_succeeds() {
// when
val result = Prov.newInstance().session { cmd("echo bla") }
// then
assertTrue(result.success)
}
@Test
fun session_not_on_top_level_throws_an_exception() {
// when
val exception = org.junit.jupiter.api.assertThrows<RuntimeException> {
local().session {
session {
cmd("echo bla")
}
}
}
// then
assertEquals(
"A session can only be created on the top-level and may not be included in another session or task.",
exception.message
)
}
// method for task_warning_for_task_on_top_level_is_in_output
// must be declared outside test task_warning_for_task_on_top_level_is_in_output in order to avoid strange naming in result output
fun Prov.tst_task() = task {
task_returningTrue()
task_returningFalse()
}
@Test
fun task_warning_for_task_on_top_level_is_in_output() {
// given
setRootLoggingLevel(Level.OFF)
val outContent = ByteArrayOutputStream()
val errContent = ByteArrayOutputStream()
val originalOut = System.out
val originalErr = System.err
System.setOut(PrintStream(outContent))
System.setErr(PrintStream(errContent))
// when
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.BASIC)
.tst_task().success
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.BASIC)
.tst_task().success // test that also second run gets warning
// then
System.setOut(originalOut)
System.setErr(originalErr)
println(outContent.toString())
val expectedOutputOneRun =
"WARNING: method task should not be used at top-level, use method <session> instead.\n" +
"---------- Processing started ----------\n" +
"> \u001B[90mexecuting...\u001B[0m -- tst_task\n" +
"---> \u001B[90mexecuting...\u001B[0m -- task_returningTrue\n" +
"---> \u001B[90mexecuting...\u001B[0m -- task_returningFalse\n" +
"---------- Processing completed ----------\n" +
"============================================== SUMMARY (test instance with no progress info) =============================================\n" +
"> \u001B[91mFAILED\u001B[0m -- tst_task \n" +
"---> \u001B[92mSuccess\u001B[0m -- task_returningTrue \n" +
"---> \u001B[91mFAILED\u001B[0m -- task_returningFalse \n" +
"----------------------------------------------------------------------------------------------------\n" +
"Overall > \u001B[91mFAILED\u001B[0m \n" +
"============================================ SUMMARY END ===========================================\n" +
"\n"
val expectedOutputDoubleRun = expectedOutputOneRun + expectedOutputOneRun
assertEquals(expectedOutputDoubleRun, outContent.toString().replace("\r", ""))
}
}

View file

@ -1,9 +1,14 @@
package org.domaindrivenarchitecture.provs.framework.core.processors
import org.domaindrivenarchitecture.provs.framework.core.*
import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.domaindrivenarchitecture.provs.test.testDockerWithSudo
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
val DEFAULT_START_MODE_TEST_CONTAINER = ContainerStartMode.USE_RUNNING_ELSE_CREATE
@ -22,4 +27,42 @@ class ContainerUbuntuHostProcessorTest {
assertEquals(0, res.exitCode)
assertEquals("abc", res.out)
}
@ExtensiveContainerTest
fun test_reopeing_ssh_session_succeeds() {
// given
val containerName = "prov-test-ssh-with-container"
val password = Secret("testuserpw")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
containerName,
startMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
sudo = true,
dockerImage = "ubuntu_plus_user",
options = "--expose=22"
),
progressType = ProgressType.NONE
)
prov.task {
makeCurrentUserSudoerWithoutPasswordRequired(password)
aptInstall("openssh-server")
cmd("sudo service ssh start")
}
val ipOfContainer = local().cmd("sudo docker inspect -f \"{{ .NetworkSettings.IPAddress }}\" $containerName").out?.trim()
?: throw IllegalStateException("Ip not found")
val remoteProvBySsh = remote(ipOfContainer, "testuser", password)
// when
val firstSessionResult = remoteProvBySsh.cmd("echo 1")
val secondSessionResult = remoteProvBySsh.cmd("echo 1")
// then
assertTrue(firstSessionResult.success)
assertTrue(secondSessionResult.success)
}
}

View file

@ -5,10 +5,7 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.creat
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileContent
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.configureUser
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.createUser
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.userExists
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.userIsInGroupSudo
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.*
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
@ -58,24 +55,24 @@ internal class ProvisionUserKtTest {
}
@ContainerTest
fun createUserWithSudo() {
fun createUserWithSudoAndCopiedSshKey() {
// given
val a = defaultTestContainer()
val newUser = "testnewsudouser3"
a.task {
val prov = defaultTestContainer()
val newUser = "testnewsudouser4"
prov.task {
createDir(".ssh")
createFile("~/.ssh/authorized_keys", "newdummykey")
}
// when
val res = a.createUser(newUser, sudo = true, copyAuthorizedSshKeysFromCurrentUser = true)
val res = prov.createUser(newUser, userCanSudoWithoutPassword = true, copyAuthorizedSshKeysFromCurrentUser = true)
// then
assertTrue(res.success)
assertTrue(a.userExists(newUser))
assertEquals("newdummykey", a.fileContent("/home/$newUser/.ssh/authorized_keys", sudo = true))
assertTrue(prov.userExists(newUser))
assertEquals("newdummykey", prov.fileContent("/home/$newUser/.ssh/authorized_keys", sudo = true))
// new user can sudo
assertTrue(a.cmd("sudo -H -u $newUser bash -c 'sudo echo \"I am \$USER, with uid \$UID\"' ").success)
assertTrue(prov.cmd("sudo -H -u $newUser bash -c 'sudo echo \"I am \$USER, with uid \$UID\"' ").success)
}
}

View file

@ -45,7 +45,7 @@ internal class CliArgumentParserTest {
// then
assertTrue(result.isValidTarget())
assertEquals(ApplicationFileName("app.yaml"), result.applicationFileName)
assertEquals(ApplicationFileName("app.yaml").fullyQualifiedName(), result.applicationFileName?.fullyQualifiedName())
assertEquals(TargetCliCommand("user@host.com"), result.target)
}
}

View file

@ -0,0 +1,44 @@
package org.domaindrivenarchitecture.provs.server.domain
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository
import org.domaindrivenarchitecture.provs.framework.core.getLocalFileContent
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.domaindrivenarchitecture.provs.server.infrastructure.DefaultApplicationFileRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.io.File
internal class ApplicationFileKtTest {
@Test
fun assertValidateReturnsSpecErrors() {
// given
val applicationFileName = ApplicationFileName("src/test/resources/failed-spec.yaml")
// when
val file = ApplicationFile(applicationFileName, getLocalFileContent(applicationFileName.fullyQualifiedName()))
// then
val result = file.validate()
assertEquals(arrayListOf("Spec failed"), result)
}
@Test
fun assertValidateReturnsJavaErrors() {
// given
val applicationFileName = ApplicationFileName("src/test/resources/java-exception.yaml")
// when
val file = ApplicationFile(applicationFileName, getLocalFileContent(applicationFileName.fullyQualifiedName()))
// then
val result = file.validate()
assertEquals(arrayListOf("Exception in thread"), result)
}
}

View file

@ -2,6 +2,8 @@ package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
@ -12,30 +14,32 @@ internal class DefaultApplicationFileRepositoryKtTest {
@Test
fun assertExistsThrowsRuntimeException() {
// when
val invalidFileName = ConfigFileName("iDontExist")
val repo = DefaultConfigFileRepository()
val invalidFileName = ApplicationFileName("iDontExist")
val repo = DefaultApplicationFileRepository(invalidFileName)
// then
val exception = assertThrows<RuntimeException>(
"Should not find the file."
) { repo.assertExists(invalidFileName) }
) { repo.getFile() }
assertEquals(
"Config file iDontExist not found. Please check if path is correct.",
"Application file not found. Please check if path is correct.",
exception.message)
}
@Test
fun assertExistsPasses() {
// given
val validFileName = "src/test/resources/existing_file"
fun assertGetFileThrowsRuntimeException() {
// when
val validFile = ConfigFileName(File(validFileName).path)
val repo = DefaultConfigFileRepository()
repo.assertExists(validFile)
val invalidFileName = ApplicationFileName("src/test/resources/java-exception.yaml")
val repo = DefaultApplicationFileRepository(invalidFileName)
// then
// no exception is thrown
val exception = assertThrows<RuntimeException>(
"Should not find the file."
) { repo.getFile() }
assertEquals(
"Application file was invalid.",
exception.message)
}
}

View file

@ -1,7 +1,9 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileContainsText
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
@ -12,15 +14,21 @@ class SshKtTest {
fun test_configureSsh() {
// given
val p = defaultTestContainer()
val prov = defaultTestContainer()
prov.task {
aptInstall("openssh-server")
deleteFile(pathSshdHardeningConfig, sudo = true)
}
// when
val res = p.configureSsh()
prov.configureSsh()
// then
assertTrue(res.success)
assertTrue(p.fileContainsText("/etc/ssh/ssh_config","PasswordAuthentication no", sudo=true))
assertTrue(p.fileContainsText("/etc/ssh/sshd_config","PasswordAuthentication no", sudo=true))
assertTrue(p.checkFile("/etc/ssh/sshd_config.d/sshd_hardening.conf"))
// Note: result of method configureSsh might have status failure as restart ssh within docker is not possible,
// but files should have expected content
assertTrue(prov.fileContainsText("/etc/ssh/ssh_config","PasswordAuthentication no", sudo=true))
assertTrue(prov.fileContainsText("/etc/ssh/sshd_config","PasswordAuthentication no", sudo=true))
assertTrue(prov.checkFile("/etc/ssh/sshd_config.d/sshd_hardening.conf"))
}
}

View file

@ -3,10 +3,7 @@ package org.domaindrivenarchitecture.provs.syspec.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.syspec.domain.FileSpec
import org.domaindrivenarchitecture.provs.syspec.domain.FolderSpec
import org.domaindrivenarchitecture.provs.syspec.domain.SocketSpec
import org.domaindrivenarchitecture.provs.syspec.domain.SyspecConfig
import org.domaindrivenarchitecture.provs.syspec.domain.*
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.test.testLocal
@ -131,4 +128,30 @@ internal class VerificationKtTest {
assertFalse(res3)
}
@ContainerTest
fun test_verifySpecConfig_succeeds() {
// given
val dir = "/home/testuser"
val prov = defaultTestContainer()
// when
val res = prov.verifySpecConfig(SyspecConfig(folder = listOf(FolderSpec(dir)), command = listOf(CommandSpec("echo bla"))))
// then
assertTrue(res.success)
}
@ContainerTest
fun test_verifySpecConfig_fails() {
// given
val dir = "/home/testuser"
val prov = defaultTestContainer()
// when
val res = prov.verifySpecConfig(SyspecConfig(command = listOf(CommandSpec("echoo bla"), CommandSpec("echo bla")), folder = listOf(FolderSpec(dir))))
// then
assertFalse(res.success)
}
}

View file

@ -1,6 +0,0 @@
{
"ssh": null,
"gpg": null,
"gitUserName": "mygitusername",
"gitEmail": "my@git.email"
}

View file

@ -0,0 +1,13 @@
-- Spec failed --------------------
{:jvb-auth-password 1234,
^^^^
:jicofo-auth-password ...,
:jicofo-component-secret ...}
should satisfy
bash-env-string?
-------------------------
Detected 1 error

View file

@ -0,0 +1,34 @@
Exception in thread "main" java.lang.IllegalArgumentException: Cannot open <nil> as a Reader.
at clojure.java.io$fn__11641.invokeStatic(io.clj:288)
at clojure.java.io$fn__11641.invoke(io.clj:288)
at clojure.java.io$fn__11530$G__11519__11537.invoke(io.clj:69)
at clojure.java.io$reader.invokeStatic(io.clj:102)
at clojure.java.io$reader.doInvoke(io.clj:86)
at clojure.lang.RestFn.invoke(RestFn.java:410)
at clojure.lang.AFn.applyToHelper(AFn.java:154)
at clojure.lang.RestFn.applyTo(RestFn.java:132)
at clojure.core$apply.invokeStatic(core.clj:669)
at clojure.core$slurp.invokeStatic(core.clj:7009)
at clojure.core$slurp.doInvoke(core.clj:7009)
at clojure.lang.RestFn.invoke(RestFn.java:410)
at dda.c4k_common.yaml$fn__577.invokeStatic(yaml.clj:39)
at dda.c4k_common.yaml$fn__577.invoke(yaml.clj:38)
at clojure.lang.MultiFn.invoke(MultiFn.java:229)
at dda.c4k_common.yaml$load_as_edn.invokeStatic(yaml.clj:42)
at dda.c4k_common.yaml$load_as_edn.invoke(yaml.clj:41)
at dda.c4k_website.website$replace_common_data.invokeStatic(website.cljc:122)
at dda.c4k_website.website$replace_common_data.invoke(website.cljc:117)
at dda.c4k_website.website$generate_hashfile_volume.invokeStatic(website.cljc:198)
at dda.c4k_website.website$generate_hashfile_volume.invoke(website.cljc:196)
at dda.c4k_website.core$generate_configs.invokeStatic(core.cljc:60)
at dda.c4k_website.core$generate_configs.invoke(core.cljc:45)
at dda.c4k_website.core$k8s_objects.invokeStatic(core.cljc:74)
at dda.c4k_website.core$k8s_objects.invoke(core.cljc:66)
at dda.c4k_common.common$generate_common.invokeStatic(common.cljc:68)
at dda.c4k_common.common$generate_common.invoke(common.cljc:60)
at dda.c4k_common.uberjar$main_common.invokeStatic(uberjar.clj:50)
at dda.c4k_common.uberjar$main_common.invoke(uberjar.clj:31)
at dda.c4k_website.uberjar$_main.invokeStatic(uberjar.clj:9)
at dda.c4k_website.uberjar$_main.doInvoke(uberjar.clj:8)
at clojure.lang.RestFn.applyTo(RestFn.java:137)
at dda.c4k_website.uberjar.main(Unknown Source)

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: config
type: Opaque
data:
JVB_AUTH_PASSWORD: BLA
JICOFO_AUTH_PASSWORD: BLA
JICOFO_COMPONENT_SECRET: BLA

View file

@ -2,6 +2,7 @@ package org.domaindrivenarchitecture.provs.test
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.docker.containerRuns
import org.domaindrivenarchitecture.provs.framework.core.docker.dockerImageExists
import org.domaindrivenarchitecture.provs.framework.core.docker.dockerProvideImage
import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.UbuntuPlusUser
@ -15,7 +16,7 @@ const val defaultTestContainerName = "provs_test"
private lateinit var prov: Prov
fun defaultTestContainer(startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE): Prov {
if (!::prov.isInitialized) { prov = initDefaultTestContainer(startMode) }
if (!::prov.isInitialized || !testLocal().containerRuns(defaultTestContainerName) || (startMode == ContainerStartMode.CREATE_NEW_KILL_EXISTING)) { prov = initDefaultTestContainer(startMode) }
return prov
}

View file

@ -6,7 +6,7 @@ import org.junit.jupiter.api.Test
private const val CONTAINER_TEST = "containertest"
private const val EXTENSIVE_CONTAINER_TEST = "extensivecontainertest"
private const val CONTAINER_TEST_NON_CI = "containernonci"
private const val NON_CI = "nonci"
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@ -28,6 +28,7 @@ annotation class ExtensiveContainerTest
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention
@Tag(CONTAINER_TEST_NON_CI)
@Tag(NON_CI)
@Test
// For test which do not run in ci pipeline
annotation class NonCi