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 - test
- package - package
- publish - publish
- release
before_script: before_script:
- echo "---------- Start CI ----------" - echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle - export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x gradlew - chmod +x gradlew
- echo "------ commit tag ---------------" - echo "------ commit info ---------------"
- echo $CI_COMMIT_TAG - echo $CI_COMMIT_TAG
- echo $CI_COMMIT_REF_NAME - echo $CI_COMMIT_REF_NAME
- echo "----------------------------------"
cache: cache:
paths: paths:
- .gradle/wrapper - .gradle/wrapper
- .gradle/caches - .gradle/caches
build: build:
stage: build stage: build
script: script:
@ -29,6 +32,7 @@ build:
- build/libs/*.jar - build/libs/*.jar
expire_in: 1 week expire_in: 1 week
test: test:
stage: test stage: test
image: docker:latest image: docker:latest
@ -46,7 +50,7 @@ test:
- docker build --pull -t "$CI_REGISTRY_IMAGE" . - 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 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 - 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: artifacts:
when: on_failure when: on_failure
paths: paths:
@ -54,40 +58,11 @@ test:
reports: reports:
junit: build/test-results/test/TEST-*.xml 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 stage: package
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
script: script:
- ./gradlew -x assemble -x test jar
- ./gradlew -x assemble -x test -x jar uberjarDesktop - ./gradlew -x assemble -x test -x jar uberjarDesktop
- ./gradlew -x assemble -x test -x jar uberjarServer - ./gradlew -x assemble -x test -x jar uberjarServer
- ./gradlew -x assemble -x test -x jar uberjarSyspec - ./gradlew -x assemble -x test -x jar uberjarSyspec
@ -96,42 +71,38 @@ uberjar:
- find . -type f -exec sha512sum {} \; | sort > sha512sum.lst - find . -type f -exec sha512sum {} \; | sort > sha512sum.lst
artifacts: artifacts:
paths: paths:
- 'build/libs/provs-desktop.jar' - build/libs/*.jar
- 'build/libs/provs-server.jar' - build/libs/*.lst
- 'build/libs/provs-syspec.jar'
- 'build/libs/sha256sum.lst'
- 'build/libs/sha512sum.lst'
expire_in: never
publish-snapshot-lib:
publish-maven-package-to-gitlab:
stage: publish stage: publish
rules: rules:
- if: $CI_PIPELINE_SOURCE != "push" - if: $CI_PIPELINE_SOURCE != "push"
when: never 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: script:
- ./gradlew -x assemble -x test jar - ./gradlew -x assemble -x test publishLibraryPublicationToGitlabRepository
- ./gradlew -x assemble -x test publish
artifacts:
paths:
- build/libs/*.jar
publish-released-lib:
publish-maven-package-to-meissa:
stage: publish stage: publish
allow_failure: true
rules: rules:
- if: $CI_PIPELINE_SOURCE != "push" - if: $CI_PIPELINE_SOURCE != "push"
when: never 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: script:
- ./gradlew -x assemble -x test jar - apt-get update -y
- ./gradlew -x assemble -x test publish - apt-get install -y iputils-ping ssh
artifacts: - ping -c 2 repo.prod.meissa.de
paths: - ssh-keyscan repo.prod.meissa.de
- build/libs/*.jar - ./gradlew -x assemble -x test publishLibraryPublicationToMeissaRepository
release:
release-to-gitlab:
image: registry.gitlab.com/gitlab-org/release-cli:latest image: registry.gitlab.com/gitlab-org/release-cli:latest
stage: publish stage: release
rules: rules:
- if: $CI_PIPELINE_SOURCE != "push" - if: $CI_PIPELINE_SOURCE != "push"
when: never 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\":\"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\"}" \ --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: after_script:
- echo "---------- End CI ----------" - echo "---------- End CI ----------"

View file

@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager"> <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" /> <option name="MAIN_CLASS_NAME" value="org.domaindrivenarchitecture.provs.desktop.application.ApplicationKt" />
<module name="provs.main" /> <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" /> <shortenClasspath name="NONE" />
<method v="2"> <method v="2">
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />

View file

@ -3,7 +3,8 @@ FROM ubuntu:latest
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get -y install apt-utils sudo 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 RUN echo "testuser ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/testuser
USER 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` for only executing one action, e.g.
* `-o verify` for verifying your installation * `-o verify` for verifying your installation
* `-o firefox` to install firefox from apt on ubuntu * `-o firefox` to install firefox from apt on ubuntu
* `-o teams` to install MS-Teams
#### Example #### Example

View file

@ -1,24 +1,25 @@
buildscript { buildscript {
ext.kotlin_version = "1.7.0" ext.kotlin_version = "1.7.20"
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
repositories { mavenCentral() } repositories {
mavenCentral()
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
} }
} }
apply plugin: "org.jetbrains.kotlin.jvm" plugins {
apply plugin: "java-library" id "org.jetbrains.kotlin.jvm" version "$kotlin_version"
apply plugin: "java-test-fixtures" id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version"
id "java"
id "java-test-fixtures"
}
apply plugin: "maven-publish" apply plugin: "maven-publish"
apply plugin: "kotlinx-serialization"
group = "org.domaindrivenarchitecture.provs" group = "org.domaindrivenarchitecture.provs"
version = "0.17.1-SNAPSHOT" version = "0.24.2-SNAPSHOT"
repositories { repositories {
mavenCentral() mavenCentral()
@ -26,11 +27,19 @@ repositories {
java { java {
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
withSourcesJar()
withJavadocJar()
toolchain { toolchain {
languageVersion = JavaLanguageVersion.of(11) languageVersion = JavaLanguageVersion.of(11)
} }
} }
jar {
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
}
test { test {
// set properties for the tests // set properties for the tests
@ -57,12 +66,6 @@ compileJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestFixturesJava.options.debugOptions.debugLevel = "source,lines,vars" compileTestFixturesJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestJava.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 { dependencies {
@ -71,7 +74,7 @@ dependencies {
api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2") api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
api("org.jetbrains.kotlinx:kotlinx-cli:0.3.4") 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("org.slf4j:slf4j-api:1.7.36")
api('ch.qos.logback:logback-classic:1.2.11') 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 from sourceSets.main.output
@ -110,7 +113,7 @@ task uberjarDesktop(type: Jar) {
} }
task uberjarServer(type: Jar) { tasks.register('uberjarServer', Jar) {
from sourceSets.main.output from sourceSets.main.output
@ -131,7 +134,7 @@ task uberjarServer(type: Jar) {
} }
task uberjarSyspec(type: Jar) { tasks.register('uberjarSyspec', Jar) {
from sourceSets.main.output from sourceSets.main.output
@ -155,7 +158,7 @@ def projectRoot = rootProject.projectDir
// copy jar to /usr/local/bin and make it executable // 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) // 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) dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
doLast { doLast {
exec { commandLine("sh", "-c", "sudo apt-get update & sudo apt-get install jarwrapper") } exec { commandLine("sh", "-c", "sudo apt-get update & sudo apt-get install jarwrapper") }
@ -168,34 +171,94 @@ 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 { publishing {
publications { publications {
library(MavenPublication) { library(MavenPublication) {
groupId 'org.domaindrivenarchitecture'
artifactId 'provs'
from components.java from components.java
} }
} }
repositories { repositories {
if (System.getenv("CI_JOB_TOKEN") != null) { maven {
// see https://docs.gitlab.com/ee/user/packages/maven_repository/index.html name = "gitlab"
maven { url = "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
url "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven" credentials(HttpHeaderCredentials) {
name "GitLab" name = "Job-Token"
credentials(HttpHeaderCredentials) { value = System.getenv("CI_JOB_TOKEN")
name = "Job-Token" }
value = System.getenv("CI_JOB_TOKEN") authentication {
} header(HttpHeaderAuthentication)
authentication { }
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 {
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"
} }
} }
} else {
mavenLocal() 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 ## 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 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.ArgParser
import kotlinx.cli.ArgType import kotlinx.cli.ArgType
import kotlinx.cli.default import kotlinx.cli.default
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
open class CliTargetParser(name: String) : ArgParser(name) { open class CliTargetParser(name: String) : ArgParser(name) {
val target by argument( val target by argument(
@ -17,13 +16,3 @@ open class CliTargetParser(name: String) : ArgParser(name) {
"prompt for password for remote target", "prompt for password for remote target",
).default(false) ).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 { class DefaultConfigFileRepository : ConfigFileRepository {
override fun assertExists(configFileName: ConfigFileName?) { override fun assertExists(configFileName: ConfigFileName?) {
if (configFileName != null && !checkLocalFile(configFileName.fullqualified())) { 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 package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.serialization.SerializationException 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.domain.provisionDesktopCommand
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.nio.file.Files
import kotlin.io.path.Path
import kotlin.system.exitProcess import kotlin.system.exitProcess
/** /**
@ -17,19 +23,39 @@ fun main(args: Array<String>) {
exitProcess(1) exitProcess(1)
} }
val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true) val defaultConfigFileName = "desktop-config.yaml"
val config = if ((cmd.configFile == null) && !Files.isRegularFile(Path(defaultConfigFileName))) {
println("ATTENTION: No config provided => using an empty config.")
DesktopConfig()
} else {
val configFileName = cmd.configFile?.fileName ?: defaultConfigFileName
try {
getConfig(configFileName)
} catch (e: SerializationException) {
println(
"Error: File \"${configFileName}\" has an invalid format and or invalid data."
)
null
} catch (e: FileNotFoundException) {
println(
"Error: File\u001b[31m ${configFileName} \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${configFileName} \u001B[0m " +
"and change the content according to your needs."
)
null
}
}
try { if (config == null) {
provisionDesktopCommand(prov, cmd) println("No suitable config found.")
} catch (e: SerializationException) { quit(-1)
println( }
"Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n"
) val prov = createProvInstance(cmd.target)
} catch (e: FileNotFoundException) {
println( prov.session {
"Error: File\u001b[31m ${cmd.configFile?.fileName} \u001b[0m was not found.\n" + ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${cmd.configFile?.fileName} \u001B[0m " + provisionDesktopCommand(cmd, config)
"and change the content according to your needs.\n"
)
} }
} }

View file

@ -1,6 +1,7 @@
package org.domaindrivenarchitecture.provs.desktop.application package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.cli.ArgType import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName 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 import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType
@OptIn(ExperimentalCli::class)
open class CliArgumentsParser(name: String) : CliTargetParser(name) { open class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<DesktopSubcommand> = listOf(Basic(), Office(), Ide()) private val modules: List<DesktopSubcommand> = listOf(Basic(), Office(), Ide())
@ -23,12 +25,14 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
val module = modules.first { it.parsed } val module = modules.first { it.parsed }
val targetCliCommand = TargetCliCommand(
target,
passwordInteractive
)
return DesktopCliCommand( return DesktopCliCommand(
DesktopType.valueOf(module.name.uppercase()), DesktopType.valueOf(module.name.uppercase()),
TargetCliCommand( targetCliCommand,
target,
passwordInteractive
),
module.configFileName, module.configFileName,
module.onlyModules module.onlyModules
) )
@ -55,7 +59,7 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
override fun execute() { override fun execute() {
configFileName = cliConfigFileName?.let { ConfigFileName(it) } configFileName = cliConfigFileName?.let { ConfigFileName(it) }
parsed = true 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 package org.domaindrivenarchitecture.provs.desktop.domain
enum class DesktopOnlyModule { 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.SshKeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.provisionKeys 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 import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
internal fun provisionDesktopCommand(prov: Prov, cmd: DesktopCliCommand) { internal fun Prov.provisionDesktopCommand(cmd: DesktopCliCommand, conf: DesktopConfig) = task {
provisionDesktop(
// retrieve config
val conf = if (cmd.configFile != null) getConfig(cmd.configFile.fileName) else DesktopConfig()
prov.provisionDesktop(
cmd.type, cmd.type,
conf.ssh?.keyPair(), conf.ssh?.keyPair(),
conf.gpg?.keyPair(), conf.gpg?.keyPair(),
@ -65,7 +61,7 @@ internal fun Prov.provisionDesktop(
} }
fun Prov.validatePrecondition() { 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") 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) { fun Prov.provisionOfficeDesktop(onlyModules: List<String>? = null) {
if (onlyModules == null) { if (onlyModules == null) {
aptInstall(ZIP_UTILS) aptInstall(ZIP_UTILS)
aptInstall(SPELLCHECKING_DE)
aptInstall(BROWSER) aptInstall(BROWSER)
aptInstall(EMAIL_CLIENT) aptInstall(EMAIL_CLIENT)
installDeltaChat() installDeltaChat()
aptInstall(OFFICE_SUITE) aptInstall(OFFICE_SUITE)
installZimWiki() installZimWiki()
installNextcloudClient() installNextcloudClient()
aptInstall(COMPARE_TOOLS)
// optional as installation of these tools often fail and they are not considered mandatory // optional as installation of these tools often fail and they are not considered mandatory
optional { optional {
aptInstall(DRAWING_TOOLS) aptInstall(DRAWING_TOOLS)
} }
aptInstall(SPELLCHECKING_DE)
} else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) { } else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) {
verifyOfficeSetup() verifyOfficeSetup()
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) { } else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
@ -155,11 +142,13 @@ fun Prov.provisionBasicDesktop(
installFirefox() installFirefox()
installGopass() installGopass()
installGopassBridgeJsonApi() configureGopass(publicGpgKey = gpg?.publicKey)
installGopassJsonApi()
downloadGopassBridge() downloadGopassBridge()
installRedshift()
installRedshift()
configureRedshift() configureRedshift()
configureNoSwappiness() configureNoSwappiness()
configureBash() configureBash()
installVirtualBoxGuestAdditions() installVirtualBoxGuestAdditions()

View file

@ -11,8 +11,8 @@ import java.io.FileWriter
* Returns DesktopConfig; data for config is read from specified file. * Returns DesktopConfig; data for config is read from specified file.
* Throws exceptions FileNotFoundException, SerializationException if file is not found resp. cannot be parsed. * 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") @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 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 { fun Prov.installDevOps() = task {
installTerraform() installTerraform()
@ -41,20 +43,9 @@ fun Prov.installYq(
fun Prov.installKubectlAndTools(): ProvResult = task { fun Prov.installKubectlAndTools(): ProvResult = task {
task("installKubectl") { task("installKubectl") {
val kubeConfigFile = ".bashrc.d/kubectl.sh" if (!checkFile(KUBE_CONFIG_CONTEXT_SCRIPT)) {
if (!checkFile(kubeConfigFile)) { installKubectl()
// prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/ configureKubectlBashCompletion()
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)
} else { } else {
ProvResult(true, out = "Kubectl already installed") ProvResult(true, out = "Kubectl already installed")
} }
@ -63,20 +54,47 @@ fun Prov.installKubectlAndTools(): ProvResult = task {
installDevopsScripts() 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() { fun Prov.installDevopsScripts() {
task("install ssh helper") { task("install ssh helper") {
createFileFromResource( createFileFromResource(
"/usr/local/bin/sshu.sh", "/usr/local/bin/sshu.sh",
"sshu.sh", "sshu.sh",
resourcePath, RESOURCE_PATH,
"555", "555",
sudo = true sudo = true
) )
createFileFromResource( createFileFromResource(
"/usr/local/bin/ssht.sh", "/usr/local/bin/ssht.sh",
"ssht.sh", "ssht.sh",
resourcePath, RESOURCE_PATH,
"555", "555",
sudo = true sudo = true
) )
@ -87,7 +105,7 @@ fun Prov.installDevopsScripts() {
createFileFromResource( createFileFromResource(
k3sContextFile, k3sContextFile,
"k3s-create-context.sh", "k3s-create-context.sh",
resourcePath, RESOURCE_PATH,
"555", "555",
sudo = true sudo = true
) )
@ -98,12 +116,13 @@ fun Prov.installDevopsScripts() {
createFileFromResource( createFileFromResource(
k3sConnectFile, k3sConnectFile,
"k3s-connect.sh", "k3s-connect.sh",
resourcePath, RESOURCE_PATH,
"555", "555",
sudo = true sudo = true
) )
} }
} }
fun Prov.installTerraform(): ProvResult = task { fun Prov.installTerraform(): ProvResult = task {
val dir = "/usr/lib/tfenv/" 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("ln -s " + dir + "bin/* /usr/local/bin", sudo = true)
} }
cmd("tfenv install", sudo = true) cmd("tfenv install", sudo = true)
cmd("tfenv install latest:^1.0.8", sudo = true) cmd("tfenv install latest:^1.4.6", sudo = true)
cmd("tfenv use latest:^1.0.8", 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 // inspired by: https://www.omgubuntu.co.uk/2022/04/how-to-install-firefox-deb-apt-ubuntu-22-04
if (chk("snap list | grep firefox")) { task("remove snap firefox") {
cmd("snap remove firefox", sudo = true) if (chk("snap list | grep firefox")) {
cmd("snap remove firefox", sudo = true)
}
} }
aptInstall("software-properties-common") aptInstall("software-properties-common")
cmd("add-apt-repository -y ppa:mozillateam/ppa", sudo = true) cmd("add-apt-repository -y ppa:mozillateam/ppa", sudo = true)
// set prio in order to use ppa-firefox above snap
addTextToFile( addTextToFile(
"\nPackage: *\n" + "\nPackage: *\n" +
"Pin: release o=LP-PPA-mozillateam\n" + "Pin: release o=LP-PPA-mozillateam\n" +
@ -34,4 +38,5 @@ fun Prov.installFirefox() = task {
) )
aptInstall("firefox") 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.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult 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.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled 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 import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
fun Prov.installGopass( fun Prov.installGopass(
version: String = "1.12.7", version: String = "1.15.5",
enforceVersion: Boolean = false, enforceVersion: Boolean = false,
sha256sum: String = "0824d5110ff1e68bff1ba10c1be63acb67cb1ad8e3bccddd6b6fc989608beca8" // checksum for sha256sum version 8.30 (e.g. ubuntu 20.04) sha256sum: String = "23ec10015c2643f22cb305859eb36d671094d463d2eb1798cc675e7bb06f4b39"
) = taskWithResult { ) = taskWithResult {
if (isPackageInstalled("gopass") && !enforceVersion) { if (isPackageInstalled("gopass") && !enforceVersion) {
@ -34,29 +36,34 @@ fun Prov.installGopass(
if (result.success) { if (result.success) {
cmd("sudo dpkg -i $path/gopass_${version}_linux_amd64.deb") cmd("sudo dpkg -i $path/gopass_${version}_linux_amd64.deb")
// Cross-check if installation was successful // Cross-check if installation was successful
addResultToEval(ProvResult(checkGopassVersion(version))) return@taskWithResult ProvResult(checkGopassVersion(version))
} else { } 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() { fun Prov.configureGopass(gopassRootFolder: String? = null, publicGpgKey: Secret? = null) = taskWithResult {
val configFile = ".config/gopass/config.yml"
val defaultRootFolder = userHome() + ".password-store"
val rootFolder = gopassRootFolder ?: defaultRootFolder
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") return@taskWithResult ProvResult(true, out = "Gopass already configured in file $configFile")
} }
if ((gopassRootFolder != null) && (!gopassRootFolder.startsWith("/"))) { val defaultRootFolder = userHome() + ".password-store"
return@taskWithResult ProvResult(false, err = "Gopass cannot be initialized with a relative path or path starting with ~") val gopassRoot = gopassRootFolder ?: defaultRootFolder
}
// use default // initialize root store
createDir(rootFolder) val fingerprint = publicGpgKey?.let { gpgFingerprint(it.plain()) }
gopassInitStoreFolder(gopassRoot, fingerprint)
createDirs(".config/gopass") createDirs(".config/gopass")
createFile(configFile, gopassConfig(rootFolder)) createFile(configFile, gopassConfig(gopassRoot))
// auto-completion // auto-completion
configureBashForUser() 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 {
cmd("gopass mounts add $storeName $path") 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.gopassInitStoreFolder(path: String, gpgFingerprint: String? = null ) = task {
fun Prov.gopassInitStore(storeName: String, indexOfRecepientKey: Int = 0) = task { createFile("$path/.gpg-id", gpgFingerprint ?: "_replace_this_by_a_fingerprint_of_a_public_gpg_key_")
cmd("printf \"$indexOfRecepientKey\\n\" | gopass init --store=$storeName") if (!checkDir(".git", path)) {
cmd("git init", path)
}
} }
internal fun gopassConfig(gopassRoot: String): String { internal fun gopassConfig(gopassRoot: String): String {
return """ return """
autoclip: true [core]
autoimport: true parsing = true
cliptimeout: 45 exportkeys = true
exportkeys: true autoclip = true
nocolor: false showsafecontent = false
nopager: false nopager = false
notifications: true cliptimeout = 45
parsing: true notifications = true
path: $gopassRoot autoimport = true
safecontent: false [age]
mounts: {} usekeychain = false
""".trimIndent() + "\n" [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.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
import java.io.File
fun Prov.downloadGopassBridge() = task { fun Prov.downloadGopassBridge() = task {
val version = "0.9.0" // Attention: when changing the version, you also need to change the number after /file/ in the download url below
val filename = "gopass_bridge-${version}-fx.xpi" val filename = "gopass_bridge-0.9.0-fx.xpi"
val downloadDir = "${userHome()}Downloads/" val downloadDir = "${userHome()}Downloads/"
createDirs(downloadDir) createDirs(downloadDir)
@ -19,13 +18,14 @@ fun Prov.downloadGopassBridge() = task {
"-L https://addons.mozilla.org/firefox/downloads/file/3630534/$filename", "-L https://addons.mozilla.org/firefox/downloads/file/3630534/$filename",
downloadDir + 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 // see https://github.com/gopasspw/gopass-jsonapi
val gopassJsonApiVersion = "1.11.1" val sha256sum = "ec9976e39a468428ae2eb1e2e0b9ceccba7f60d66b8097e2425b0c07f4fed108"
val requiredGopassVersion = "1.14.4" val gopassJsonApiVersion = "1.15.5"
val requiredGopassVersion = "1.15.5"
val filename = "gopass-jsonapi_${gopassJsonApiVersion}_linux_amd64.deb" val filename = "gopass-jsonapi_${gopassJsonApiVersion}_linux_amd64.deb"
val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassJsonApiVersion/$filename" val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassJsonApiVersion/$filename"
val downloadDir = "${userHome()}Downloads" val downloadDir = "${userHome()}Downloads"
@ -36,7 +36,7 @@ fun Prov.installGopassBridgeJsonApi() = task {
if (checkGopassVersion(requiredGopassVersion)) { if (checkGopassVersion(requiredGopassVersion)) {
aptInstall("git gnupg2") // required dependencies aptInstall("git gnupg2") // required dependencies
createDir(downloadDir) createDir(downloadDir)
downloadFromURL(downloadUrl, filename, downloadDir) downloadFromURL(downloadUrl, filename, downloadDir, sha256sum = sha256sum)
cmd("dpkg -i $downloadDir/$filename", sudo = true) cmd("dpkg -i $downloadDir/$filename", sudo = true)
} else { } else {
ProvResult( ProvResult(
@ -46,51 +46,56 @@ fun Prov.installGopassBridgeJsonApi() = task {
) )
} }
} else { } else {
addResultToEval( ProvResult(
ProvResult( false,
false, "gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
)
) )
} }
} else { } else {
if (installedJsonApiVersion.startsWith("gopass-jsonapi version $gopassJsonApiVersion")) { 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 { } else {
addResultToEval( ProvResult(
ProvResult( false,
false, err = "gopass-jsonapi (version $gopassJsonApiVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
err = "gopass-jsonapi (version $gopassJsonApiVersion) cannot be installed as version $installedJsonApiVersion is already installed." + " Upgrading gopass-jsonapi is currently not supported by provs."
" 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 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)) { if (checkFile(appArmorFile) && !fileContainsText(appArmorFile, gopassAccessPermission, true)) {
addTextToFile( replaceTextInFile(
"\nowner @{HOME}/.config/gopass/gopass_wrapper.sh Ux\n", appArmorFile, insertAfterText, "$insertAfterText $gopassAccessPermission\n"
File(appArmorFile),
sudo = true
) )
cmd("systemctl reload apparmor", sudo = true)
} }
cmd("systemctl reload apparmor", sudo = true)
} }
fun Prov.configureGopassBridgeJsonApi() = task { fun Prov.configureGopassJsonApi() = taskWithResult {
if (isPackageInstalled("gopass-jsonapi")) { if (isPackageInstalled("gopass-jsonapi")) {
// configure for firefox and choose default for each: // configures gopass-jsonapi for firefox and chooses default for each:
// "Install for all users? [y/N/q]", // * "Install for all users? [y/N/q]",
// "In which path should gopass_wrapper.sh be installed? [/home/testuser/.config/gopass]" // * "In which path should gopass_wrapper.sh be installed? [/home/<user>/.config/gopass]"
// "Wrapper Script for gopass_wrapper.sh ..." // * "Wrapper Script for gopass_wrapper.sh ..."
configureGopassWrapperShForFirefox() //
// 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") cmd("printf \"\\n\\n\\n\" | gopass-jsonapi configure --browser firefox")
configureApparmorForGopassWrapperShForFirefox()
} else { } else {
ProvResult( ProvResult(
false, 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 KEY_MANAGEMENT_GUI = "seahorse"
val BROWSER = "firefox chromium-browser" val BROWSER = "chromium-browser" // firefox can be installed by installFirefox
val EMAIL_CLIENT = "thunderbird" val EMAIL_CLIENT = "thunderbird"
@ -39,3 +39,5 @@ val CLOJURE_TOOLS = "leiningen"
val PASSWORD_TOOLS = "pwgen" val PASSWORD_TOOLS = "pwgen"
val SCREEN_TOOLS = "scrcpy" 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 import java.io.File
fun Prov.provisionPython() = task { fun Prov.provisionPython(venvHome: String? = "~/.venv/meissa") = task {
installPython3() installPython3()
configureVenv() if (venvHome != null) { configureVenv(venvHome) }
installPybuilder() installPybuilder(venvHome)
installRestClient() installRestClient(venvHome)
installJupyterlab() installJupyterlab(venvHome)
installLinters(venvHome)
} }
fun Prov.installPython3(): ProvResult = task { fun Prov.installPython3(): ProvResult = task {
aptInstall("python3-venv python3-pip") aptInstall("python3-venv python3-pip")
} }
fun Prov.configureVenv(): ProvResult = task { fun Prov.configureVenv(venvHome: String): ProvResult = task {
val venvHome = "~/.venv/meissa" cmd("python3 -m venv $venvHome")
cmd("python3 -m venv " + venvHome) createSymlink(File("$venvHome/bin/activate"), File("~/.bashrc.d/venv.sh"))
cmd("source " + venvHome + "/bin/activate") pipInstall("pip --upgrade", venvHome)
createSymlink(File(venvHome + "/bin/activate"), File("~/.bashrc.d/venv.sh"))
cmd("pip3 install pip --upgrade")
} }
fun Prov.installPybuilder(): ProvResult = task { fun Prov.installPybuilder(venvHome: String? = null): ProvResult = task {
cmd("pip3 install pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation python_terraform " + pipInstall("pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation" +
"boto3") " python_terraform dda_python_terraform boto3 pyyaml packaging",
venvHome
)
pipInstall("--upgrade ddadevops", venvHome)
} }
fun Prov.installRestClient(): ProvResult = task { fun Prov.installRestClient(venvHome: String? = null): ProvResult = task {
cmd("pip3 install requests") pipInstall("requests", venvHome)
} }
fun Prov.installJupyterlab(): ProvResult = task { fun Prov.installJupyterlab(venvHome: String? = null): ProvResult = task {
cmd("pip3 install jupyterlab pandas matplotlib") 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 { fun Prov.installVSC(vararg options: String) = task {
val clojureExtensions = val clojureExtensions = setOf("betterthantomorrow.calva", "DavidAnson.vscode-markdownlint")
arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint") val pythonExtensions = setOf("ms-python.python")
val pythonExtensions = arrayListOf("ms-python.python")
prerequisitesVSCinstall() prerequisitesVSCinstall()
@ -24,8 +23,6 @@ fun Prov.installVSC(vararg options: String) = task {
installExtensionsCode(pythonExtensions) installExtensionsCode(pythonExtensions)
installExtensionsCodium(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) var res = ProvResult(true)
for (ext in extensions) { for (ext in extensions) {
res = cmd("code --install-extension $ext") 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 // 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) var res = ProvResult(true)
for (ext in extensions) { for (ext in extensions) {
res = cmd("codium --install-extension $ext") res = cmd("codium --install-extension $ext")
@ -82,12 +79,3 @@ private fun Prov.installExtensionsCodium(extensions: List<String>) = optional {
res res
// Settings can be found at $HOME/.config/Code/User/settings.json // 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 internalResults = arrayListOf<ResultLine>()
private val infoTexts = arrayListOf<String>() 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). * 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. * Returns success if no sub-tasks are called or if all subtasks finish with success.
*/ */
fun task(name: String? = null, taskLambda: Prov.() -> Unit): ProvResult { fun task(name: String? = null, taskLambda: Prov.() -> Unit): ProvResult {
printDeprecationWarningIfLevel0("task")
return evaluate(ResultMode.ALL, name) { taskLambda(); ProvResult(true) } 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. * The returned result is included in the evaluation.
*/ */
fun taskWithResult(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult { fun taskWithResult(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("taskWithResult")
return evaluate(ResultMode.ALL, name) { taskLambda() } return evaluate(ResultMode.ALL, name) { taskLambda() }
} }
/** /**
* defines a task, which returns the returned result, the results of sub-tasks are not considered * defines a task, which returns the returned result, the results of sub-tasks are not considered
*/ */
fun requireLast(a: Prov.() -> ProvResult): ProvResult { fun requireLast(name: String? = null, a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.LAST) { a() } printDeprecationWarningIfLevel0("requireLast")
return evaluate(ResultMode.LAST, name) { a() }
} }
/** /**
* defines a task, which always returns success * defines a task, which always returns success
*/ */
fun optional(a: Prov.() -> ProvResult): ProvResult { fun optional(name: String? = null, a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.OPTIONAL) { a() } printDeprecationWarningIfLevel0("optional")
return evaluate(ResultMode.OPTIONAL, name) { a() }
} }
/** /**
* defines a task, which exits the overall execution on failure * defines a task, which exits the overall execution on failure
*/ */
fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult { fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("exitOnFailure")
return evaluate(ResultMode.FAILEXIT) { a() } return evaluate(ResultMode.FAILEXIT) { a() }
} }
@ -109,6 +125,7 @@ open class Prov protected constructor(
* Runs the provided task in the specified (running) container * Runs the provided task in the specified (running) container
*/ */
fun taskInContainer(containerName: String, taskLambda: Prov.() -> ProvResult): ProvResult { fun taskInContainer(containerName: String, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("taskInContainer")
runInContainerWithName = containerName runInContainerWithName = containerName
val res = evaluate(ResultMode.ALL) { taskLambda() } val res = evaluate(ResultMode.ALL) { taskLambda() }
runInContainerWithName = null runInContainerWithName = null
@ -254,6 +271,8 @@ open class Prov protected constructor(
previousLevel = -1 previousLevel = -1
exit = false exit = false
initProgress() initProgress()
processor.open()
} }
// pre-handling // pre-handling
@ -312,6 +331,15 @@ open class Prov protected constructor(
internalResults[resultIndex].provResult = returnValue 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) { if (level == 0) {
endProgress() endProgress()
processor.close() 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 { 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 offsetVal = 1
val exclude = arrayOf("task", "task\$default", "taskWithResult\$default", "taskWithResult", "def", "def\$default", "record", "invoke", "invoke0", "evaluate", "evaluate\$default", ) 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 // 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 = "" var suffix = ""
val callingFrame = Thread.currentThread().stackTrace val callingFrame = Thread.currentThread().stackTrace
@ -30,7 +30,7 @@ internal fun getCallingMethodName(): String? {
var inc = 0 var inc = 0
while ((method in exclude) or (method in suffixes)) { while ((method in exclude) or (method in suffixes)) {
if (method in suffixes && suffix == "") { if (method in suffixes && suffix == "") {
suffix = method suffix = method.split("$")[0]
} }
inc++ inc++
method = callingFrame[i + offsetVal + inc].methodName 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.local
import org.domaindrivenarchitecture.provs.framework.core.remote import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource 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 import kotlin.system.exitProcess
/** /**
* Returns a Prov instance according to the targetCommand. * 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. * 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( fun createProvInstance(targetCommand: TargetCliCommand): Prov {
targetCommand: TargetCliCommand,
remoteHostSetSudoWithoutPasswordRequired: Boolean = false
): Prov {
if (targetCommand.isValid()) { if (targetCommand.isValid()) {
val password: Secret? = targetCommand.remoteTarget()?.password val password: Secret? = targetCommand.remoteTarget()?.password
val remoteTarget = targetCommand.remoteTarget() return if (targetCommand.isValidLocalhost()) {
if (targetCommand.isValidLocalhost()) { local()
return local() } else if (targetCommand.isValidRemote()) {
} else if (targetCommand.isValidRemote() && remoteTarget != null) { createRemoteProvInstance(targetCommand.remoteTarget(), password)
return createProvInstanceRemote(
remoteTarget.host,
remoteTarget.user,
remoteTarget.password == null,
password,
remoteHostSetSudoWithoutPasswordRequired
)
} else { } 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 { } 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) exitProcess(1)
} }
} }
private fun createProvInstanceRemote(
host: String, internal fun createRemoteProvInstance(
remoteUser: String, target: TargetCliCommand.RemoteTarget?,
sshWithKey: Boolean, password: Secret? = null
password: Secret?,
remoteHostSetSudoWithoutPasswordRequired: Boolean
): Prov { ): Prov {
val prov = return if (target != null) {
if (sshWithKey) { remote(target.host, target.user, target.password ?: password)
remote(host, remoteUser) } else {
} else { throw IllegalArgumentException(
require( "Error: no valid remote target (host & user) was specified!"
password != null, )
{ "No password available for provisioning without ssh keys. Either specify provisioning by ssh-keys or provide password." })
remote(host, remoteUser, password)
}
if (!prov.currentUserCanSudo()) {
if (remoteHostSetSudoWithoutPasswordRequired) {
require(
password != null,
{ "User ${prov.whoami()} not able to sudo on remote machine without password and no password available for the user." })
prov.makeUserSudoerWithNoSudoPasswordRequired(password)
// a new session is required after making the user a sudoer without password
return remote(host, remoteUser, password)
} else {
throw IllegalStateException("User ${prov.whoami()} not able to sudo on remote machine without password and option not set to enable user to sudo without password.")
}
} }
return prov
} }
// todo: consider removal as password can be retrieved by PromptSecretSource internal fun getPasswordToConfigureSudoWithoutPassword(): Secret {
internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? { return PromptSecretSource("password to configure sudo without password.").secret()
var password: Secret? = null }
if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) {
password =
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret() /**
* Wrapper for exitProcess, which allows e.g. mocking for test purposes
} */
return password fun quit(status: Int): Nothing {
exitProcess(status)
} }

View file

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

View file

@ -2,10 +2,13 @@ package org.domaindrivenarchitecture.provs.framework.core.processors
interface Processor { 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 exec(vararg args: String): ProcessResult
fun execNoLog(vararg args: String): ProcessResult fun execNoLog(vararg args: String): ProcessResult
fun close() { 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. * Executes task on a remote machine.
* Attention: host key is currently not being verified * 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 { companion object {
@Suppress("JAVA_CLASS_ON_COMPANION") @Suppress("JAVA_CLASS_ON_COMPANION")
private val log = LoggerFactory.getLogger(javaClass.enclosingClass) private val log = LoggerFactory.getLogger(javaClass.enclosingClass)
} }
private val ssh = SSHClient() private var ssh = SSHClient()
init { override fun open() {
try { 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") log.info("Connecting to $host with user: $user with " + if (password != null) "password" else "ssh-key")
ssh.loadKnownHosts() ssh.loadKnownHosts()
// Attention: host key is not verified // Attention: host key is not verified
ssh.addHostKeyVerifier(PromiscuousVerifier()) ssh.addHostKeyVerifier(PromiscuousVerifier())
ssh.connectTimeout = 30000 // ms
ssh.connect(host) ssh.connect(host)
if (password != null) { if (password != null) {
@ -50,8 +55,9 @@ class RemoteProcessor(host: InetAddress, user: String, password: Secret? = null)
try { try {
ssh.disconnect() ssh.disconnect()
} finally { } finally {
log.error("Got exception when initializing ssh (Username, password or ssh-key might be wrong): " + e.message) val errorMag = "Error when initializing ssh (Host, username, password or ssh-key might be wrong) "
throw RuntimeException("Error when initializing ssh (Username, password or ssh-key might be wrong) ", e) 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 // todo: only use sudo for root or if owner different from current
val content = fileContent(file, true) val content = fileContent(file, true)
if (content != null) { if (content != null) {

View file

@ -45,10 +45,9 @@ fun Prov.gitClone(
fun Prov.trustGithub() = task { fun Prov.trustGithub() = task {
// current fingerprints from https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints // current fingerprints from https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints
val fingerprints = setOf( val fingerprints = setOf(
"SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com", // (RSA) "SHA256:uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s github.com", // (RSA)
// supported beginning September 14, 2021:
"SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA) "SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA)
"SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519) "SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519)
) )
trustHost("github.com", fingerprints) trustHost("github.com", fingerprints)
} }

View file

@ -20,7 +20,10 @@ fun Prov.aptInstall(packages: String, ignoreAlreadyInstalled: Boolean = true): P
if (!allInstalled) { if (!allInstalled) {
if (!isPackageInstalled(packages)) { if (!isPackageInstalled(packages)) {
if (!aptInit) { if (!aptInit) {
cmd("sudo apt-get update") optional {
// may fail for some packages, but this should in general not be an issue
cmd("sudo apt-get update")
}
cmd("sudo apt-get install -qy apt-utils") cmd("sudo apt-get install -qy apt-utils")
aptInit = true 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 @Suppress("unused") // used externally
fun Prov.aptPurge(packageName: String): Boolean { fun Prov.aptPurge(packageName: String): Boolean {

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources 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.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource 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) { class FileSecretSource(fqFileName: String) : SecretSource(fqFileName) {
override fun secret(): Secret { 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.") return p.getSecret("cat " + input) ?: throw Exception("Failed to get secret.")
} }
override fun secretNullable(): 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) return p.getSecret("cat " + input)
} }
} }

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources 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.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource 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.") return secretNullable() ?: throw Exception("Failed to get \"$input\" secret from gopass.")
} }
override fun secretNullable(): Secret? { 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) return p.getSecret("gopass show -f $input", true)
} }
} }

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources 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.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource 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) { class PassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret { 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.") return p.getSecret("pass " + input) ?: throw Exception("Failed to get secret.")
} }
override fun secretNullable(): 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) return p.getSecret("pass " + input)
} }
} }

View file

@ -8,7 +8,7 @@ import javax.swing.*
class PasswordPanel : JPanel(FlowLayout()) { class PasswordPanel : JPanel(FlowLayout()) {
private val passwordField = JPasswordField(20) private val passwordField = JPasswordField(30)
private var entered = false private var entered = false
val enteredPassword val enteredPassword

View file

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

View file

@ -1,6 +1,8 @@
package org.domaindrivenarchitecture.provs.server.application 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.createProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import org.domaindrivenarchitecture.provs.server.domain.ServerType import org.domaindrivenarchitecture.provs.server.domain.ServerType
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sCliCommand import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sCliCommand
import org.domaindrivenarchitecture.provs.server.domain.k3s.provisionK3sCommand 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) val cmd = CliArgumentsParser("provs-server.jar subcommand target").parseCommand(checkedArgs)
// ToDo: exitProcess makes testing harder, find another solution
// validate parsed arguments // validate parsed arguments
if (!cmd.isValidTarget()) { if (!cmd.isValidTarget()) {
println("Remote or localhost not valid, please try -h for help.") println("Remote or localhost not valid, please try -h for help.")
exitProcess(1) quit(1)
} }
val prov = createProvInstance(cmd.target) 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 package org.domaindrivenarchitecture.provs.server.application
import kotlinx.cli.ArgType import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName 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.K3sCliCommand
import org.domaindrivenarchitecture.provs.server.domain.k3s.ServerOnlyModule import org.domaindrivenarchitecture.provs.server.domain.k3s.ServerOnlyModule
@OptIn(ExperimentalCli::class)
class CliArgumentsParser(name: String) : CliTargetParser(name) { class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<ServerSubcommand> = listOf(K3s(), K3d()) private val modules: List<ServerSubcommand> = listOf(K3s(), K3d())
@ -84,7 +86,7 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) {
override fun execute() { override fun execute() {
super.configFileName = cliConfigFileName?.let { ConfigFileName(it) } super.configFileName = cliConfigFileName?.let { ConfigFileName(it) }
super.applicationFileName = cliApplicationFileName?.let { ApplicationFileName(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.reprovision = cliReprovision == true
super.parsed = 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 import java.io.File
data class ApplicationFileName(val fileName: String) { class ApplicationFileName(val fileName: String) {
fun fullqualified() : String { fun fullyQualifiedName() : String {
return File(fileName).absoluteFile.absolutePath return File(fileName).absoluteFile.absolutePath
} }
} }

View file

@ -1,5 +1,6 @@
package org.domaindrivenarchitecture.provs.server.domain.k3s package org.domaindrivenarchitecture.provs.server.domain.k3s
interface ApplicationFileRepository { 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() val grafanaConfigResolved: GrafanaAgentConfigResolved? = findK8sGrafanaConfig(cli.configFileName)?.resolveSecret()
if (cli.onlyModules == null ) { if (cli.onlyModules == null) {
val k3sConfig: K3sConfig = getK3sConfig(cli.configFileName) val k3sConfig: K3sConfig = getK3sConfig(cli.configFileName)
DefaultApplicationFileRepository().assertExists(cli.applicationFileName)
DefaultConfigFileRepository().assertExists(cli.configFileName) DefaultConfigFileRepository().assertExists(cli.configFileName)
val k3sConfigReprovision = k3sConfig.copy(reprovision = cli.reprovision || k3sConfig.reprovision) 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 { } else {
provisionGrafana(cli.onlyModules, grafanaConfigResolved) provisionGrafana(cli.onlyModules, grafanaConfigResolved)
} }
@ -30,7 +30,8 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
fun Prov.provisionK3s( fun Prov.provisionK3s(
k3sConfig: K3sConfig, k3sConfig: K3sConfig,
grafanaConfigResolved: GrafanaAgentConfigResolved? = null, grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
applicationFileName: ApplicationFileName? = null) = task { applicationFile: ApplicationFile? = null
) = task {
if (k3sConfig.reprovision) { if (k3sConfig.reprovision) {
deprovisionK3sInfra() deprovisionK3sInfra()
@ -52,8 +53,8 @@ fun Prov.provisionK3s(
provisionGrafanaAgent(grafanaConfigResolved) provisionGrafanaAgent(grafanaConfigResolved)
} }
if (applicationFileName != null) { if (applicationFile != null) {
provisionK3sApplication(applicationFileName) provisionK3sApplication(applicationFile)
} }
if (!k3sConfig.reprovision) { if (!k3sConfig.reprovision) {
@ -63,7 +64,8 @@ fun Prov.provisionK3s(
private fun Prov.provisionGrafana( private fun Prov.provisionGrafana(
onlyModules: List<String>?, onlyModules: List<String>?,
grafanaConfigResolved: GrafanaAgentConfigResolved?) = task { grafanaConfigResolved: GrafanaAgentConfigResolved?
) = task {
if (onlyModules != null && onlyModules.contains(ServerOnlyModule.GRAFANA.name.lowercase())) { if (onlyModules != null && onlyModules.contains(ServerOnlyModule.GRAFANA.name.lowercase())) {
if (grafanaConfigResolved == null) { if (grafanaConfigResolved == null) {

View file

@ -1,14 +1,30 @@
package org.domaindrivenarchitecture.provs.server.infrastructure 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.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.ApplicationFileName
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileRepository import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileRepository
class DefaultApplicationFileRepository : ApplicationFileRepository {
override fun assertExists(applicationFileName: ApplicationFileName?) { class DefaultApplicationFileRepository(val applicationFileName: ApplicationFileName) : ApplicationFileRepository {
if (applicationFileName != null && !checkLocalFile(applicationFileName.fullqualified())) {
throw RuntimeException("Application file ${applicationFileName.fileName} not found. Please check if path is correct.") 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.core.repeatTaskUntilSuccess
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.* import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.server.domain.CertmanagerEndpoint import org.domaindrivenarchitecture.provs.server.domain.CertmanagerEndpoint
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName import org.domaindrivenarchitecture.provs.server.domain.k3s.*
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 java.io.File import java.io.File
// ----------------------------------- versions -------------------------------- // ----------------------------------- versions --------------------------------
@ -25,8 +22,8 @@ private const val k3sResourceDir = "org/domaindrivenarchitecture/provs/server/in
// ----------------------------------- files -------------------------------- // ----------------------------------- files --------------------------------
private val k3sInstallScript = File( "/usr/local/bin/k3s-install.sh") private val k3sInstallScript = File("/usr/local/bin/k3s-install.sh")
private val k3sConfigFile = File( "/etc/rancher/k3s/config.yaml") private val k3sConfigFile = File("/etc/rancher/k3s/config.yaml")
private val k3sKubeConfig = File("/etc/rancher/k3s/k3s.yaml") private val k3sKubeConfig = File("/etc/rancher/k3s/k3s.yaml")
private val k3sTraefikWorkaround = File(k3sManualManifestsDir, "traefik.yaml") private val k3sTraefikWorkaround = File(k3sManualManifestsDir, "traefik.yaml")
@ -78,25 +75,35 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
if (k3sConfig.isDualStack()) { if (k3sConfig.isDualStack()) {
k3sConfigResourceFileName += ".dual.template.yaml" k3sConfigResourceFileName += ".dual.template.yaml"
metallbConfigResourceFileName += ".dual.template.yaml" metallbConfigResourceFileName += ".dual.template.yaml"
k3sConfigMap = k3sConfigMap.plus("node_ipv6" to k3sConfig.node.ipv6!!) require(k3sConfig.node.ipv6 != null && k3sConfig.loopback.ipv6 != null)
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6!!) k3sConfigMap = k3sConfigMap
.plus("node_ipv6" to k3sConfig.node.ipv6)
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6)
} else { } else {
k3sConfigResourceFileName += ".ipv4.template.yaml" k3sConfigResourceFileName += ".ipv4.template.yaml"
metallbConfigResourceFileName += ".ipv4.template.yaml" metallbConfigResourceFileName += ".ipv4.template.yaml"
} }
createK3sFileFromResourceTemplate(k3sConfigFile, k3sConfigMap, alternativeResourceTemplate = File(k3sConfigResourceFileName)) createK3sFileFromResourceTemplate(
k3sConfigFile,
k3sConfigMap,
alternativeResourceTemplate = File(k3sConfigResourceFileName)
)
createK3sFileFromResource(k3sInstallScript, posixFilePermission = "755") createK3sFileFromResource(k3sInstallScript, posixFilePermission = "755")
cmd("INSTALL_K3S_VERSION=$K3S_VERSION k3s-install.sh") cmd("INSTALL_K3S_VERSION=$K3S_VERSION k3s-install.sh")
// metallb // metallb
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-namespace.yaml")) applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.13.7-native-manifest.yaml"))
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.10.2-manifest.yaml"))
applyK3sFileFromResourceTemplate( repeatTaskUntilSuccess(6, 10) {
File(k3sManualManifestsDir, "metallb-config.yaml"), applyK3sFileFromResourceTemplate(
k3sConfigMap, File(k3sManualManifestsDir, "metallb-config.yaml"),
alternativeResourceName = File(metallbConfigResourceFileName) k3sConfigMap,
) alternativeResourceName = File(metallbConfigResourceFileName)
)
}
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-l2advertisement.yaml"))
// traefik // traefik
if (k3sConfig.isDualStack()) { 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)) applyK3sFileFromResourceTemplate(k3sEcho, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
} }
fun Prov.provisionK3sApplication(applicationFileName: ApplicationFileName) = task { fun Prov.provisionK3sApplication(applicationFile: ApplicationFile) = task {
copyFileFromLocal( copyFileFromLocal(
fullyQualifiedLocalFilename = applicationFileName.fullqualified(), fullyQualifiedLocalFilename = applicationFile.id.fullyQualifiedName(),
fullyQualifiedFilename = k3sManualManifestsDir + "application.yaml", fullyQualifiedFilename = k3sManualManifestsDir + "application.yaml",
posixFilePermission = "644", posixFilePermission = "644",
sudo = true sudo = true
@ -216,5 +223,5 @@ private fun File.templateName(): String {
} }
internal fun Prov.configureShellAliases() = task { 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 { fun Prov.provisionNetwork(k3sConfig: K3sConfig) = task {
if(!testNetworkExists()) { if(!testNetworkExists()) {
if(k3sConfig.isDualStack()) { if(k3sConfig.isDualStack()) {
require(k3sConfig.loopback.ipv6 != null)
createFileFromResourceTemplate( createFileFromResourceTemplate(
loopbackFile, loopbackFile,
"99-loopback.dual.template.yaml", "99-loopback.dual.template.yaml",
resourcePathNetwork, 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", "644",
sudo = true sudo = true
) )

View file

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

View file

@ -1,16 +1,17 @@
apiVersion: v1 apiVersion: metallb.io/v1beta1
kind: ConfigMap kind: IPAddressPool
metadata: metadata:
name: public
namespace: metallb-system namespace: metallb-system
name: config spec:
data: addresses:
config: | - ${node_ipv4}/32
address-pools: ---
- name: public apiVersion: metallb.io/v1beta1
protocol: layer2 kind: IPAddressPool
addresses: metadata:
- ${node_ipv4}/32 name: private
- name: private namespace: metallb-system
protocol: layer2 spec:
addresses: addresses:
- ${loopback_ipv4}/32 - ${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: "python3 --version"
- command: "pip3 --version" - command: "pip3 --version"
- command: "terraform --version" - command: "terraform --version"
out: "1.0.8" out: "1.4.6"

View file

@ -1,9 +1,21 @@
package org.domaindrivenarchitecture.provs.configuration.application package org.domaindrivenarchitecture.provs.configuration.application
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test 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 { internal class CliTargetParserTest {
@Test @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.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance 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.local
import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor
import org.domaindrivenarchitecture.provs.framework.core.remote 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.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -23,8 +24,8 @@ internal class CliTargetCommandKtTest {
mockkStatic(::remote) mockkStatic(::remote)
every { remote(any(), any(), any(), any()) } returns Prov.newInstance(PrintOnlyProcessor()) every { remote(any(), any(), any(), any()) } returns Prov.newInstance(PrintOnlyProcessor())
mockkStatic(::retrievePassword) mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { retrievePassword(any()) } returns Secret("sec") every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec")
} }
@AfterAll @AfterAll
@ -33,12 +34,13 @@ internal class CliTargetCommandKtTest {
unmockkObject(Prov) unmockkObject(Prov)
unmockkStatic(::local) unmockkStatic(::local)
unmockkStatic(::remote) unmockkStatic(::remote)
unmockkStatic(::retrievePassword) unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
} }
} }
@Test @Test
@NonCi
fun createProvInstance_local() { fun createProvInstance_local() {
// given // given
val cliCommand = TargetCliCommand("local", false) val cliCommand = TargetCliCommand("local", false)

View file

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

View file

@ -4,16 +4,21 @@ import ch.qos.logback.classic.Level
import io.mockk.* import io.mockk.*
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand 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.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.* 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.framework.core.processors.DummyProcessor
import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.PrintStream import java.io.PrintStream
@ -52,8 +57,8 @@ internal class ApplicationKtTest {
cmd = "mocked command" cmd = "mocked command"
) )
mockkStatic(::retrievePassword) mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { retrievePassword(any()) } returns Secret("sec") every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("sec")
} }
@Suppress("unused") // false positive @Suppress("unused") // false positive
@ -65,7 +70,7 @@ internal class ApplicationKtTest {
unmockkStatic(::remote) unmockkStatic(::remote)
unmockkStatic(::getConfig) unmockkStatic(::getConfig)
unmockkStatic(Prov::provisionDesktop) unmockkStatic(Prov::provisionDesktop)
unmockkStatic(::retrievePassword) unmockkStatic(::getPasswordToConfigureSudoWithoutPassword)
} }
} }
@ -91,6 +96,9 @@ internal class ApplicationKtTest {
@Test @Test
fun prints_error_message_if_config_not_found() { fun prints_error_message_if_config_not_found() {
mockkStatic(::quit)
every { quit(any()) } throws RuntimeException("mockked")
// given // given
setRootLoggingLevel(Level.OFF) setRootLoggingLevel(Level.OFF)
@ -103,21 +111,28 @@ internal class ApplicationKtTest {
System.setErr(PrintStream(errContent)) System.setErr(PrintStream(errContent))
// when // when
main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml")) assertThrows<RuntimeException> {
main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml"))
}
// then // then
System.setOut(originalOut) System.setOut(originalOut)
System.setErr(originalErr) System.setErr(originalErr)
val expectedOutput = 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", "")) assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", ""))
verify(exactly = 0) { any<Prov>().provisionDesktop(any(), any(), any(), any(), any(), any()) } verify(exactly = 0) { any<Prov>().provisionDesktop(any(), any(), any(), any(), any(), any()) }
unmockkStatic(::quit)
} }
@Test @Test
fun prints_error_message_if_config_not_parsable() { fun prints_error_message_if_config_not_parsable() {
mockkStatic(::quit)
every { quit(any()) } throws RuntimeException("mockked")
// given // given
setRootLoggingLevel(Level.OFF) setRootLoggingLevel(Level.OFF)
@ -130,16 +145,20 @@ internal class ApplicationKtTest {
System.setErr(PrintStream(errContent)) System.setErr(PrintStream(errContent))
// when // when
main(arrayOf("basic", "someuser@remotehost", "-c", "src/test/resources/invalid-desktop-config.yaml")) assertThrows<RuntimeException> {
main(arrayOf("basic", "someuser@remotehost", "-c", "src/test/resources/invalid-desktop-config.yaml"))
}
// then // then
System.setOut(originalOut) System.setOut(originalOut)
System.setErr(originalErr) System.setErr(originalErr)
val expectedOutput = 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", "")) assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", ""))
verify(exactly = 0) { any<Prov>().provisionDesktop(any(), any(), any(), any(), any(), any()) } 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(null, cli.configFile)
assertEquals(true, cli.target.isValidLocalhost()) 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 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.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.deleteFile
import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest 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.Assertions.assertTrue
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@ -11,6 +18,34 @@ import org.junit.jupiter.api.Test
internal class DesktopServiceKtTest { internal class DesktopServiceKtTest {
@ExtensiveContainerTest @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() { fun provisionDesktop() {
// given // given
val prov = defaultTestContainer() val prov = defaultTestContainer()
@ -52,28 +87,6 @@ internal class DesktopServiceKtTest {
// then // then
assertTrue(res.success) 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.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
internal class DevOpsKtTest { internal class DevOpsKtTest {
@ -34,4 +35,17 @@ internal class DevOpsKtTest {
defaultTestContainer().checkFile("/etc/bash_completion.d/kubernetes", sudo = true) 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 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.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class FirefoxKtTest { 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 @ExtensiveContainerTest
fun installFirefox() { fun installFirefox() {
// when // when
val res = defaultTestContainer().installFirefox() val result = defaultTestContainer().session {
installFirefox()
checkPackageInstalled("firefox")
}
// then // then
assertTrue(res.success) assertTrue(result.success)
}
val ffIsInstalled = defaultTestContainer().isPackageInstalled("firefox") @Test
assertTrue(ffIsInstalled) @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 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.Secret
import org.domaindrivenarchitecture.provs.framework.core.docker.exitAndRmContainer import org.domaindrivenarchitecture.provs.framework.core.docker.exitAndRmContainer
import org.domaindrivenarchitecture.provs.framework.core.local import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode 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.KeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys
import org.domaindrivenarchitecture.provs.test.defaultTestContainer 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.ExtensiveContainerTest
import org.domaindrivenarchitecture.provs.test.tags.NonCi import org.domaindrivenarchitecture.provs.test.tags.NonCi
import org.domaindrivenarchitecture.provs.test_keys.privateGPGSnakeoilKey import org.domaindrivenarchitecture.provs.test_keys.privateGPGSnakeoilKey
@ -27,11 +23,10 @@ internal class GopassBridgeKtTest {
fun test_downloadGopassBridge() { fun test_downloadGopassBridge() {
// given // given
local().exitAndRmContainer("provs_test") local().exitAndRmContainer("provs_test")
val a = defaultTestContainer() val prov = defaultTestContainer()
a.aptInstallCurl()
// when // when
val res = a.downloadGopassBridge() val res = prov.downloadGopassBridge()
// then // then
assertTrue(res.success) assertTrue(res.success)
@ -42,105 +37,81 @@ internal class GopassBridgeKtTest {
fun test_install_and_configure_GopassBridgeJsonApi() { fun test_install_and_configure_GopassBridgeJsonApi() {
// given // given
local().exitAndRmContainer("provs_test") local().exitAndRmContainer("provs_test")
val a = defaultTestContainer() val prov = defaultTestContainer()
val preparationResult = a.task { val preparationResult = prov.task {
aptInstallCurl()
configureGpgKeys( configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true, trust = true,
skipIfExistin = false skipIfExistin = false
) )
installGopass() installGopass()
if (!chk("gopass ls")) { configureGopass(publicGpgKey = Secret(publicGPGSnakeoilKey()))
// 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")
}
} }
assertTrue(preparationResult.success) assertTrue(preparationResult.success)
// when // when
val res = a.task { val res = prov.task {
installGopassBridgeJsonApi() installGopassJsonApi()
configureGopassBridgeJsonApi() configureGopassJsonApi()
} }
// then // then
assertTrue(res.success) assertTrue(res.success)
} }
@ContainerTest @ExtensiveContainerTest
@Test @Test
@NonCi @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_jsonapi_version_installed() { fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_jsonapi_version_installed() {
// given // given
val a = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING) val prov = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
val preparationResult = a.task { val preparationResult = prov.task {
aptInstallCurl()
configureGpgKeys( configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true, trust = true,
skipIfExistin = false skipIfExistin = false
) )
installGopass("1.11.0", enforceVersion = true, "1ec9e0dfcfd9bcc241943e1a7d92f31bf3e66bb16f61ae5d079981325c31baa6") installGopass("1.11.0", enforceVersion = true, "1ec9e0dfcfd9bcc241943e1a7d92f31bf3e66bb16f61ae5d079981325c31baa6")
if (!chk("gopass ls")) { configureGopass(publicGpgKey = Secret(publicGPGSnakeoilKey()))
// 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")
}
} }
assertTrue(preparationResult.success) assertTrue(preparationResult.success)
// when // when
val res = a.task { val res = prov.task {
installGopassBridgeJsonApi() installGopassJsonApi()
configureGopassBridgeJsonApi() configureGopassJsonApi()
} }
// then // then
assertFalse(res.success) assertFalse(res.success)
} }
@ContainerTest @ExtensiveContainerTest
@Test @Test
@NonCi @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() { fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_version_installed() {
// given // given
val a = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING) val prov = defaultTestContainer(ContainerStartMode.CREATE_NEW_KILL_EXISTING)
val preparationResult = a.task { val preparationResult = prov.task {
aptInstallCurl()
configureGpgKeys( configureGpgKeys(
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
trust = true, trust = true,
skipIfExistin = false skipIfExistin = false
) )
installGopass("1.9.0", enforceVersion = true, "fe13ef810d7fe200495107161e99eac081368aa0ce5e53971b1bd47a64eba4db") installGopass("1.9.0", enforceVersion = true, "fe13ef810d7fe200495107161e99eac081368aa0ce5e53971b1bd47a64eba4db")
if (!chk("gopass ls")) { configureGopass(publicGpgKey = Secret(publicGPGSnakeoilKey()))
// 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")
}
} }
assertTrue(preparationResult.success) assertTrue(preparationResult.success)
// when // when
val res = a.task { val res = prov.task {
installGopassBridgeJsonApi() installGopassJsonApi()
configureGopassBridgeJsonApi() configureGopassJsonApi()
} }
// then // then
assertFalse(res.success) 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 package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.remote import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest 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.KeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint 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.Assertions.assertTrue
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test 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.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
@ -25,7 +21,7 @@ internal class GopassKtTest {
fun test_configureGopass_fails_with_path_starting_with_tilde() { fun test_configureGopass_fails_with_path_starting_with_tilde() {
// when // when
val res = defaultTestContainer().task { val res = defaultTestContainer().task {
deleteFile(".config/gopass/config.yml") deleteFile(".config/gopass/config")
configureGopass("~/somedir") configureGopass("~/somedir")
} }
@ -36,32 +32,27 @@ internal class GopassKtTest {
@ExtensiveContainerTest @ExtensiveContainerTest
fun test_installAndConfigureGopassAndMountStore() { fun test_installAndConfigureGopassAndMountStore() {
// given // given
val a = defaultTestContainer() val prov = defaultTestContainer()
val gopassRootDir = ".password-store" 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 // when
val res = a.installGopass() val res = prov.task("test_installAndConfigureGopassAndMountStore") {
val res2 = a.configureGopass(a.userHome() + gopassRootDir) installGopass()
val res3 = a.gopassMountStore("exampleStore", "~/exampleStoreFolder") 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 // 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(res.success)
assertTrue(res2.success) assertTrue(prov.fileContainsText("~/.config/gopass/config", "/home/testuser/.password-store"))
assertTrue(res3.success) assertTrue(prov.fileContainsText("~/.config/gopass/config", "exampleStore"))
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "/home/testuser/.password-store")) assertTrue(prov.checkDir(".git", gopassRootDir))
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "exampleStore"))
} }
@Test @Test
@ -74,10 +65,10 @@ internal class GopassKtTest {
val privateKey = GopassSecretSource("path-to/priv.key").secret() val privateKey = GopassSecretSource("path-to/priv.key").secret()
// given // given
val a = remote(host, user) val prov = remote(host, user)
// when // when
val res = a.task { val res = prov.task {
configureGpgKeys( configureGpgKeys(
KeyPair( KeyPair(
pubKey, 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 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() downloadGopassBridge()
installGopassBridgeJsonApi() installGopassJsonApi()
configureGopassBridgeJsonApi() configureGopassJsonApi()
} }
// then // then
assertTrue(res.success) assertTrue(res.success)
} }
} }

View file

@ -40,7 +40,7 @@ internal class K3SDesktopConfigRepositoryKtTest {
@Test @Test
fun getConfig_fails_due_to_missing_file() { fun getConfig_fails_due_to_missing_file() {
val exception = assertThrows<FileNotFoundException> { val exception = assertThrows<FileNotFoundException> {
getK3sConfig(ConfigFileName("src/test/resources/Idonotexist.yaml")) getConfig("src/test/resources/Idonotexist.yaml")
} }
assertEquals(FileNotFoundException::class.java, exception.javaClass) 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.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
class MsTeamsKtTest { internal class NextcloudClientTest {
@ExtensiveContainerTest @ExtensiveContainerTest
fun installMsTeams() { fun test_installNextcloudClient() {
// given
val a = defaultTestContainer()
// when // when
val res = a.task { installMsTeams() } val res = defaultTestContainer().installNextcloudClient()
// then // then
assertTrue(res.success) 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.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.test.defaultTestContainer 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.Assertions.assertTrue
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class VSCodeKtTest { internal class VSCodeKtTest {
@ExtensiveContainerTest
fun provisionAdditionalTools() {
// given
defaultTestContainer().aptInstall("curl unzip")
// when
val res = defaultTestContainer().provisionAdditionalToolsForVSCode()
// then
assertTrue(res.success)
}
@Test @Test
@Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker") @Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker")
fun installVSC() { 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 { fun Prov.checkPrereq_evaluateToFailure() = requireLast {
ProvResult(false, err = "This is a test error.") ProvResult(false, err = "This is a test error.")
} }
fun Prov.methodThatProvidesSomeOutput() = requireLast { fun Prov.testMethodForOutputTest_with_mode_requireLast() = requireLast {
if (!checkPrereq_evaluateToFailure().success) { if (!checkPrereq_evaluateToFailure().success) {
sh( sh(
@ -273,6 +273,17 @@ internal class ProvTest {
sh("echo -End test-") 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 @Test
@NonCi @NonCi
fun prov_prints_correct_output_for_overall_success() { fun prov_prints_correct_output_for_overall_success() {
@ -290,7 +301,9 @@ internal class ProvTest {
// when // when
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE) Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE)
.methodThatProvidesSomeOutput() .session {
testMethodForOutputTest_with_mode_requireLast()
}
// then // then
System.setOut(originalOut) System.setOut(originalOut)
@ -300,13 +313,14 @@ internal class ProvTest {
val expectedOutput = val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" + "============================================== SUMMARY (test instance with no progress info) =============================================\n" +
"> \u001B[92mSuccess\u001B[0m -- methodThatProvidesSomeOutput (requireLast) \n" + "> \u001B[92mSuccess\u001B[0m -- session \n" +
"---> \u001B[93mFAILED\u001B[0m -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" + "---> \u001B[92mSuccess\u001B[0m -- testMethodForOutputTest_with_mode_requireLast (requireLast) \n" +
"---> \u001B[92mSuccess\u001B[0m -- sh \n" + "------> \u001B[93mFAILED\u001B[0m -- checkPrereq_evaluateToFailure (requireLast) -- Error: This is a test error.\n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -Start test-]\n" + "------> \u001B[92mSuccess\u001B[0m -- sh \n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo Some output]\n" + "---------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -Start test-]\n" +
"---> \u001B[92mSuccess\u001B[0m -- sh \n" + "---------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo Some output]\n" +
"------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -End test-]\n" + "------> \u001B[92mSuccess\u001B[0m -- sh \n" +
"---------> \u001B[92mSuccess\u001B[0m -- cmd [/bin/bash, -c, echo -End test-]\n" +
"----------------------------------------------------------------------------------------------------\n" + "----------------------------------------------------------------------------------------------------\n" +
"Overall > \u001B[92mSuccess\u001B[0m\n" + "Overall > \u001B[92mSuccess\u001B[0m\n" +
"============================================ SUMMARY END ===========================================\n" + "============================================ SUMMARY END ===========================================\n" +
@ -317,7 +331,7 @@ internal class ProvTest {
@Test @Test
@NonCi @NonCi
fun prov_prints_correct_output_for_failure() { fun prov_prints_correct_output_for_nested_calls_with_failure() {
// given // given
setRootLoggingLevel(Level.OFF) setRootLoggingLevel(Level.OFF)
@ -331,8 +345,9 @@ internal class ProvTest {
System.setErr(PrintStream(errContent)) System.setErr(PrintStream(errContent))
// when // when
Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE) Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE).session {
.checkPrereq_evaluateToFailure() testMethodForOutputTest_nested_with_failure()
}
// then // then
System.setOut(originalOut) System.setOut(originalOut)
@ -342,7 +357,14 @@ internal class ProvTest {
val expectedOutput = val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" + "============================================== 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" + "============================================ SUMMARY END ===========================================\n" +
"\n" "\n"
@ -603,10 +625,11 @@ internal class ProvTest {
val prov = Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE) val prov = Prov.newInstance(name = "test instance with no progress info", progressType = ProgressType.NONE)
// when // when
prov.task { prov.session {
addInfoText("Text1") addInfoText("Text1")
addInfoText("Text2\nwith newline") addInfoText("Text2\nwith newline")
ProvResult(true)
} }
// then // then
@ -617,7 +640,7 @@ internal class ProvTest {
val expectedOutput = val expectedOutput =
"============================================== SUMMARY (test instance with no progress info) =============================================\n" + "============================================== 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" + "+++++++++++++++++++++++++++++++++++ \u001B[94mAdditional information\u001B[0m +++++++++++++++++++++++++++++++++++++++\n" +
"Text1\n" + "Text1\n" +
"Text2\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 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.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.ContainerTest
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.domaindrivenarchitecture.provs.test.testDockerWithSudo import org.domaindrivenarchitecture.provs.test.testDockerWithSudo
import org.junit.jupiter.api.Assertions.assertEquals 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 val DEFAULT_START_MODE_TEST_CONTAINER = ContainerStartMode.USE_RUNNING_ELSE_CREATE
@ -22,4 +27,42 @@ class ContainerUbuntuHostProcessorTest {
assertEquals(0, res.exitCode) assertEquals(0, res.exitCode)
assertEquals("abc", res.out) 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.filesystem.base.fileContent
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.* import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType 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.*
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.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
@ -58,24 +55,24 @@ internal class ProvisionUserKtTest {
} }
@ContainerTest @ContainerTest
fun createUserWithSudo() { fun createUserWithSudoAndCopiedSshKey() {
// given // given
val a = defaultTestContainer() val prov = defaultTestContainer()
val newUser = "testnewsudouser3" val newUser = "testnewsudouser4"
a.task { prov.task {
createDir(".ssh") createDir(".ssh")
createFile("~/.ssh/authorized_keys", "newdummykey") createFile("~/.ssh/authorized_keys", "newdummykey")
} }
// when // when
val res = a.createUser(newUser, sudo = true, copyAuthorizedSshKeysFromCurrentUser = true) val res = prov.createUser(newUser, userCanSudoWithoutPassword = true, copyAuthorizedSshKeysFromCurrentUser = true)
// then // then
assertTrue(res.success) assertTrue(res.success)
assertTrue(a.userExists(newUser)) assertTrue(prov.userExists(newUser))
assertEquals("newdummykey", a.fileContent("/home/$newUser/.ssh/authorized_keys", sudo = true)) assertEquals("newdummykey", prov.fileContent("/home/$newUser/.ssh/authorized_keys", sudo = true))
// new user can sudo // 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 // then
assertTrue(result.isValidTarget()) assertTrue(result.isValidTarget())
assertEquals(ApplicationFileName("app.yaml"), result.applicationFileName) assertEquals(ApplicationFileName("app.yaml").fullyQualifiedName(), result.applicationFileName?.fullyQualifiedName())
assertEquals(TargetCliCommand("user@host.com"), result.target) 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.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository 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.Assertions.assertEquals
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
@ -12,30 +14,32 @@ internal class DefaultApplicationFileRepositoryKtTest {
@Test @Test
fun assertExistsThrowsRuntimeException() { fun assertExistsThrowsRuntimeException() {
// when // when
val invalidFileName = ConfigFileName("iDontExist") val invalidFileName = ApplicationFileName("iDontExist")
val repo = DefaultConfigFileRepository() val repo = DefaultApplicationFileRepository(invalidFileName)
// then // then
val exception = assertThrows<RuntimeException>( val exception = assertThrows<RuntimeException>(
"Should not find the file." "Should not find the file."
) { repo.assertExists(invalidFileName) } ) { repo.getFile() }
assertEquals( assertEquals(
"Config file iDontExist not found. Please check if path is correct.", "Application file not found. Please check if path is correct.",
exception.message) exception.message)
} }
@Test @Test
fun assertExistsPasses() { fun assertGetFileThrowsRuntimeException() {
// given
val validFileName = "src/test/resources/existing_file"
// when // when
val validFile = ConfigFileName(File(validFileName).path) val invalidFileName = ApplicationFileName("src/test/resources/java-exception.yaml")
val repo = DefaultConfigFileRepository() val repo = DefaultApplicationFileRepository(invalidFileName)
repo.assertExists(validFile)
// then // 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 package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile 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.filesystem.base.fileContainsText
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
@ -12,15 +14,21 @@ class SshKtTest {
fun test_configureSsh() { fun test_configureSsh() {
// given // given
val p = defaultTestContainer() val prov = defaultTestContainer()
prov.task {
aptInstall("openssh-server")
deleteFile(pathSshdHardeningConfig, sudo = true)
}
// when // when
val res = p.configureSsh() prov.configureSsh()
// then // then
assertTrue(res.success)
assertTrue(p.fileContainsText("/etc/ssh/ssh_config","PasswordAuthentication no", sudo=true)) // Note: result of method configureSsh might have status failure as restart ssh within docker is not possible,
assertTrue(p.fileContainsText("/etc/ssh/sshd_config","PasswordAuthentication no", sudo=true)) // but files should have expected content
assertTrue(p.checkFile("/etc/ssh/sshd_config.d/sshd_hardening.conf")) 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.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.syspec.domain.FileSpec import org.domaindrivenarchitecture.provs.syspec.domain.*
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.test.defaultTestContainer import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.test.testLocal import org.domaindrivenarchitecture.provs.test.testLocal
@ -131,4 +128,30 @@ internal class VerificationKtTest {
assertFalse(res3) 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.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov 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.dockerImageExists
import org.domaindrivenarchitecture.provs.framework.core.docker.dockerProvideImage import org.domaindrivenarchitecture.provs.framework.core.docker.dockerProvideImage
import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.UbuntuPlusUser import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.UbuntuPlusUser
@ -15,7 +16,7 @@ const val defaultTestContainerName = "provs_test"
private lateinit var prov: Prov private lateinit var prov: Prov
fun defaultTestContainer(startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE): 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 return prov
} }

View file

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