Compare commits
No commits in common. "main" and "release-0.0.555" have entirely different histories.
main
...
release-0.
160 changed files with 2094 additions and 8366 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,4 +9,3 @@
|
||||||
/server-config.yaml
|
/server-config.yaml
|
||||||
/desktop-config.yaml
|
/desktop-config.yaml
|
||||||
/syspec-config.yaml
|
/syspec-config.yaml
|
||||||
/.kotlin/
|
|
||||||
|
|
159
.gitlab-ci.yml
159
.gitlab-ci.yml
|
@ -1,112 +1,157 @@
|
||||||
|
image: openjdk:11-jdk-slim
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- test
|
- test
|
||||||
- package
|
- package
|
||||||
- publish
|
- publish
|
||||||
- release
|
|
||||||
|
|
||||||
.kotlin-job: &kotlin
|
|
||||||
image: domaindrivenarchitecture/ddadevops-kotlin:4.10.7
|
|
||||||
cache:
|
|
||||||
key: ${CI_COMMIT_REF_SLUG}
|
|
||||||
paths:
|
|
||||||
- .gradle/wrapper
|
|
||||||
- .gradle/caches
|
|
||||||
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
|
||||||
- export RELEASE_ARTIFACT_TOKEN=$MEISSA_REPO_BUERO_RW
|
- 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 "----------------------------------"
|
|
||||||
|
|
||||||
.tag_only: &tag_only
|
|
||||||
rules:
|
|
||||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
||||||
when: never
|
|
||||||
- if: '$CI_COMMIT_TAG =~ /^[0-9]+\.[0-9]+\.[0-9]+$/'
|
|
||||||
|
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .gradle/wrapper
|
||||||
|
- .gradle/caches
|
||||||
|
|
||||||
build:
|
build:
|
||||||
<<: *kotlin
|
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script:
|
||||||
- pyb build
|
- echo "---------- build stage ----------"
|
||||||
|
- ./gradlew assemble
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- build/libs/*.jar
|
- build/libs/*.jar
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
|
|
||||||
|
|
||||||
variables:
|
|
||||||
DOCKER_TLS_CERTDIR: "/certs"
|
|
||||||
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
<<: *tag_only
|
|
||||||
stage: test
|
stage: test
|
||||||
image: docker:24.0.5
|
image: docker:latest
|
||||||
services:
|
services:
|
||||||
- docker:24.0.5-dind
|
- docker:dind
|
||||||
dependencies:
|
dependencies:
|
||||||
- build
|
- build
|
||||||
before_script:
|
before_script:
|
||||||
- echo "---------- BEFORE -------------"
|
- echo "---------- BEFORE -------------"
|
||||||
- echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin
|
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
|
||||||
script:
|
script:
|
||||||
- echo "---------- TEST -------------"
|
- echo "---------- TEST -------------"
|
||||||
- apk update && apk add bash openjdk11 git
|
- apk update && apk add bash openjdk11
|
||||||
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
|
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
|
||||||
- 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,nonci
|
- ./gradlew -x assemble test -Dtestdockerwithoutsudo=true -DexcludeTags=extensivecontainertest
|
||||||
artifacts:
|
artifacts:
|
||||||
when: on_failure
|
when: on_failure
|
||||||
paths:
|
paths:
|
||||||
- build/reports/*
|
- build/reports/tests/test
|
||||||
reports:
|
reports:
|
||||||
junit: build/test-results/test/TEST-*.xml
|
junit: build/test-results/test/TEST-*.xml
|
||||||
|
|
||||||
|
.fatjars:
|
||||||
package:
|
|
||||||
<<: *kotlin
|
|
||||||
<<: *tag_only
|
|
||||||
stage: package
|
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:
|
script:
|
||||||
- pyb package
|
- 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:
|
||||||
|
stage: package
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE != "push"
|
||||||
|
when: never
|
||||||
|
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
|
||||||
|
script:
|
||||||
|
- ./gradlew -x assemble -x test -x jar uberjarDesktop
|
||||||
|
- ./gradlew -x assemble -x test -x jar uberjarServer
|
||||||
|
- ./gradlew -x assemble -x test -x jar uberjarSyspec
|
||||||
|
- cd build/libs/
|
||||||
|
- find . -type f -exec sha256sum {} \; | sort > sha256sum.lst
|
||||||
|
- find . -type f -exec sha512sum {} \; | sort > sha512sum.lst
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- 'build/libs/provs-desktop.jar'
|
||||||
|
- 'build/libs/provs-server.jar'
|
||||||
|
- 'build/libs/provs-syspec.jar'
|
||||||
|
- 'build/libs/sha256sum.lst'
|
||||||
|
- 'build/libs/sha512sum.lst'
|
||||||
|
expire_in: never
|
||||||
|
|
||||||
|
publish-snapshot-lib:
|
||||||
|
stage: publish
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE != "push"
|
||||||
|
when: never
|
||||||
|
- if: $CI_COMMIT_TAG !~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
|
||||||
|
script:
|
||||||
|
- ./gradlew -x assemble -x test jar
|
||||||
|
- ./gradlew -x assemble -x test publish
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- build/libs/*.jar
|
- build/libs/*.jar
|
||||||
- build/libs/*.lst
|
|
||||||
|
|
||||||
|
publish-released-lib:
|
||||||
publish-maven-package-to-gitlab:
|
|
||||||
<<: *kotlin
|
|
||||||
<<: *tag_only
|
|
||||||
stage: publish
|
stage: publish
|
||||||
|
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 publishLibraryPublicationToGitlabRepository
|
- ./gradlew -x assemble -x test jar
|
||||||
|
- ./gradlew -x assemble -x test publish
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- build/libs/*.jar
|
||||||
|
|
||||||
|
release:
|
||||||
publish-maven-package-to-meissa:
|
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||||
<<: *kotlin
|
|
||||||
<<: *tag_only
|
|
||||||
stage: publish
|
stage: publish
|
||||||
allow_failure: true
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE != "push"
|
||||||
|
when: never
|
||||||
|
- if: $CI_COMMIT_TAG =~ /^release-[0-9]+[.][0-9]+([.][0-9]+)?$/
|
||||||
|
artifacts:
|
||||||
|
paths:
|
||||||
|
- 'build/libs/provs-desktop.jar'
|
||||||
|
- 'build/libs/provs-server.jar'
|
||||||
|
- 'build/libs/provs-syspec.jar'
|
||||||
|
- 'build/libs/sha256sum.lst'
|
||||||
|
- 'build/libs/sha512sum.lst'
|
||||||
script:
|
script:
|
||||||
- ./gradlew -x assemble -x test publishLibraryPublicationToMeissaRepository
|
- apk --no-cache add curl
|
||||||
|
- |
|
||||||
|
release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
|
||||||
release-to-meissa:
|
--assets-link "{\"name\":\"provs-desktop.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-desktop.jar\"}" \
|
||||||
<<: *kotlin
|
--assets-link "{\"name\":\"provs-server.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-server.jar\"}" \
|
||||||
<<: *tag_only
|
--assets-link "{\"name\":\"provs-syspec.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-syspec.jar\"}" \
|
||||||
stage: release
|
--assets-link "{\"name\":\"sha256sum.lst\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/sha256sum.lst\"}" \
|
||||||
allow_failure: true
|
--assets-link "{\"name\":\"sha512sum.lst\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/sha512sum.lst\"}" \
|
||||||
script:
|
|
||||||
- pyb publish_release
|
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
- echo "---------- End CI ----------"
|
- echo "---------- End CI ----------"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="provision-basic-desktop" type="JetRunConfigurationType">
|
<configuration default="false" name="provs-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 user@192.168.56.146 -p" />
|
<option name="PROGRAM_PARAMETERS" value="basic local -o provsbinaries" />
|
||||||
<shortenClasspath name="NONE" />
|
<shortenClasspath name="NONE" />
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="Make" enabled="true" />
|
<option name="Make" enabled="true" />
|
|
@ -1,6 +1,6 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="test_incl_extensive_container_tests" type="JUnit" factoryName="JUnit">
|
<configuration default="false" name="test_incl_extensive_container_tests" type="JUnit" factoryName="JUnit">
|
||||||
<module name="org.domaindrivenarchitecture.provs.provs.test" />
|
<module name="provs.test" />
|
||||||
<option name="PACKAGE_NAME" value="org" />
|
<option name="PACKAGE_NAME" value="org" />
|
||||||
<option name="MAIN_CLASS_NAME" value="" />
|
<option name="MAIN_CLASS_NAME" value="" />
|
||||||
<option name="METHOD_NAME" value="" />
|
<option name="METHOD_NAME" value="" />
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
# image for usage in ci pipeline
|
FROM ubuntu:latest
|
||||||
FROM ubuntu:22.04
|
|
||||||
|
|
||||||
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 && usermod -aG sudo testuser
|
|
||||||
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
|
||||||
|
|
77
README.md
77
README.md
|
@ -1,20 +1,20 @@
|
||||||
# provs
|
# provs
|
||||||
[![pipeline status](https://gitlab.com/domaindrivenarchitecture/provs/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/provs/-/commits/master)
|
[![pipeline status](https://gitlab.com/domaindrivenarchitecture/provs/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/provs/-/commits/master)
|
||||||
|
|
||||||
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa.de/images/parts/contact/mastodon36_hue9b2464f10b18e134322af482b9c915e_5501_filter_14705073121015236177.png" width=20 alt="M"> meissa@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@meissa) | [Blog](https://domaindrivenarchitecture.org) | [Website](https://meissa.de)
|
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa-gmbh.de/img/community/Mastodon_Logotype.svg" width=20 alt="team@social.meissa-gmbh.de"> team@social.meissa-gmbh.de](https://social.meissa-gmbh.de/@team) | [Website & Blog](https://domaindrivenarchitecture.org)
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
provs provides cli-based tools for
|
provs provides cli-based tools for
|
||||||
* provisioning desktop software for different desktop types:
|
* provisioning a desktop (various kinds)
|
||||||
* basic
|
|
||||||
* office
|
|
||||||
* IDE
|
|
||||||
* provisioning a k3s server
|
* provisioning a k3s server
|
||||||
* performing system checks
|
* performing system checks
|
||||||
|
|
||||||
Tasks can be run locally or remotely.
|
Tasks can be run locally or remotely.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
under development - though we already set up a few IDEs and servers with provs.
|
||||||
|
|
||||||
## Try out
|
## Try out
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
@ -28,9 +28,8 @@ Tasks can be run locally or remotely.
|
||||||
* Download the latest `provs-desktop.jar`,`provs-server.jar` and/or `provs-syspec.jar` from: https://gitlab.com/domaindrivenarchitecture/provs/-/releases
|
* Download the latest `provs-desktop.jar`,`provs-server.jar` and/or `provs-syspec.jar` from: https://gitlab.com/domaindrivenarchitecture/provs/-/releases
|
||||||
* Preferably into `/usr/local/bin` or any other folder where executables can be found by the system
|
* Preferably into `/usr/local/bin` or any other folder where executables can be found by the system
|
||||||
* Make the jar-file executable e.g. by `chmod +x provs-desktop.jar`
|
* Make the jar-file executable e.g. by `chmod +x provs-desktop.jar`
|
||||||
* Check with `provs-desktop.jar -h` to show help information
|
|
||||||
|
|
||||||
###### Build the binaries
|
#### Build the binaries
|
||||||
|
|
||||||
Instead of downloading the binaries you can build them yourself
|
Instead of downloading the binaries you can build them yourself
|
||||||
|
|
||||||
|
@ -61,6 +60,7 @@ 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
|
||||||
|
@ -90,39 +90,6 @@ node:
|
||||||
echo: true # for demo reasons only - deploys an echo app
|
echo: true # for demo reasons only - deploys an echo app
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Monthly reboot
|
|
||||||
|
|
||||||
A monthly reboot can be deployed as only-module (i.e. without a full server deployment) by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
provs-server k3s <user>@<server> -o monthly_reboot
|
|
||||||
# Example
|
|
||||||
provs-server k3s root@myserver.org -o monthly_reboot
|
|
||||||
```
|
|
||||||
#### Hetzner CSI driver
|
|
||||||
|
|
||||||
To add the hetzner csi driver and encrypted volumes to your k3s installation add the following to the config:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
hetzner:
|
|
||||||
hcloudApiToken:
|
|
||||||
source: "PLAIN" # PLAIN, GOPASS or PROMPT
|
|
||||||
parameter: "mypassword" # the api key for the hetzner cloud
|
|
||||||
encryptionPassphrase:
|
|
||||||
source: "PLAIN" # PLAIN, GOPASS or PROMPT
|
|
||||||
parameter: "mypassword" # the encryption passphrase for created volumes
|
|
||||||
```
|
|
||||||
|
|
||||||
or as only-module (i.e. without a full server deployment) by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
provs-server k3s <user>@<server> -o hetzner_csi
|
|
||||||
# Example
|
|
||||||
provs-server k3s root@myserver.org -o hetzner_csi
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Grafana agent
|
|
||||||
|
|
||||||
To add a grafana agent to your k3s installation add the following to the config:
|
To add a grafana agent to your k3s installation add the following to the config:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -140,12 +107,6 @@ To provision the grafana agent only to an existing k8s system, ensure that the c
|
||||||
provs-server.jar k3s myuser@myhost.com -o grafana
|
provs-server.jar k3s myuser@myhost.com -o grafana
|
||||||
```
|
```
|
||||||
|
|
||||||
To provision the grafana agent only to an existing k8s system, ensure that the config (as above) is available and execute:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
provs-server.jar k3s myuser@myhost.com -o grafana
|
|
||||||
```
|
|
||||||
|
|
||||||
Reprovisioning the server can easily be done using the -r or --reprovision option.
|
Reprovisioning the server can easily be done using the -r or --reprovision option.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -184,27 +145,3 @@ Or to get help for subcommands e.g.
|
||||||
provs-desktop.jar ide -h
|
provs-desktop.jar ide -h
|
||||||
provs-server.jar k3s -h
|
provs-server.jar k3s -h
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development & mirrors
|
|
||||||
|
|
||||||
Development happens at: https://repo.prod.meissa.de/meissa/provs
|
|
||||||
|
|
||||||
Mirrors are:
|
|
||||||
* https://gitlab.com/domaindrivenarchitecture/provs (CI issues and PR)
|
|
||||||
* https://github.com/DomainDrivenArchitecture/provs
|
|
||||||
|
|
||||||
For more details about our repository model see: https://repo.prod.meissa.de/meissa/federate-your-repos
|
|
||||||
|
|
||||||
## Developer information
|
|
||||||
|
|
||||||
For using provs framework, add the required dependency to your project, then you can implement your own tasks e.g. by:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
|
|
||||||
fun Prov.myCustomTask() = task {
|
|
||||||
cmd("echo \"Hello world!\"")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See also [ForDevelopers.md](doc/ForDevelopers.md)
|
|
134
build.gradle
134
build.gradle
|
@ -1,24 +1,24 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version_no = "1.8.20"
|
ext.kotlin_version = "1.7.0"
|
||||||
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
|
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
|
||||||
|
|
||||||
repositories {
|
repositories { mavenCentral() }
|
||||||
mavenCentral()
|
|
||||||
|
dependencies {
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
apply plugin: "org.jetbrains.kotlin.jvm"
|
||||||
id "org.jetbrains.kotlin.jvm" version "$kotlin_version_no"
|
apply plugin: "java-library"
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version_no"
|
apply plugin: "java-test-fixtures"
|
||||||
id "java"
|
|
||||||
id "java-test-fixtures"
|
|
||||||
}
|
|
||||||
apply plugin: "maven-publish"
|
apply plugin: "maven-publish"
|
||||||
|
apply plugin: "kotlinx-serialization"
|
||||||
|
|
||||||
|
|
||||||
version = "0.39.5-SNAPSHOT"
|
|
||||||
group = "org.domaindrivenarchitecture.provs"
|
group = "org.domaindrivenarchitecture.provs"
|
||||||
|
version = "0.17.1-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -26,15 +26,12 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
test {
|
test {
|
||||||
// set properties for the tests
|
// set properties for the tests
|
||||||
def propertiesForTests = ["testdockerwithoutsudo"]
|
def propertiesForTests = ["testdockerwithoutsudo"]
|
||||||
|
@ -60,28 +57,39 @@ 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 {
|
||||||
|
|
||||||
api("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version_no")
|
api("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version")
|
||||||
|
api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2")
|
||||||
|
api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.3.2")
|
||||||
api("org.jetbrains.kotlinx:kotlinx-cli:0.3.4")
|
api("org.jetbrains.kotlinx:kotlinx-cli:0.3.4")
|
||||||
|
|
||||||
api('com.charleskorn.kaml:kaml:0.54.0')
|
api('com.charleskorn.kaml:kaml:0.43.0')
|
||||||
|
|
||||||
api("org.slf4j:slf4j-api:1.7.36")
|
api("org.slf4j:slf4j-api:1.7.36")
|
||||||
api('ch.qos.logback:logback-classic:1.4.14')
|
api('ch.qos.logback:logback-classic:1.2.11')
|
||||||
api('ch.qos.logback:logback-core:1.4.14')
|
api('ch.qos.logback:logback-core:1.2.11')
|
||||||
|
|
||||||
implementation("com.hierynomus:sshj:0.38.0")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
|
||||||
|
implementation("com.hierynomus:sshj:0.32.0")
|
||||||
|
|
||||||
testFixturesApi('io.mockk:mockk:1.12.3')
|
implementation("aws.sdk.kotlin:s3:0.17.1-beta")
|
||||||
|
|
||||||
testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
testFixturesApi("org.junit.jupiter:junit-jupiter-api:5.8.2")
|
||||||
|
testFixturesApi('io.mockk:mockk:1.12.3')
|
||||||
|
|
||||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
|
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register('uberjarDesktop', Jar) {
|
task uberjarDesktop(type: Jar) {
|
||||||
|
|
||||||
from sourceSets.main.output
|
from sourceSets.main.output
|
||||||
|
|
||||||
|
@ -102,7 +110,7 @@ tasks.register('uberjarDesktop', Jar) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register('uberjarServer', Jar) {
|
task uberjarServer(type: Jar) {
|
||||||
|
|
||||||
from sourceSets.main.output
|
from sourceSets.main.output
|
||||||
|
|
||||||
|
@ -123,7 +131,7 @@ tasks.register('uberjarServer', Jar) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
tasks.register('uberjarSyspec', Jar) {
|
task uberjarSyspec(type: Jar) {
|
||||||
|
|
||||||
from sourceSets.main.output
|
from sourceSets.main.output
|
||||||
|
|
||||||
|
@ -147,7 +155,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)
|
||||||
tasks.register('installlocally') {
|
task 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") }
|
||||||
|
@ -160,38 +168,24 @@ tasks.register('installlocally') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
task sourceJar(type: Jar, dependsOn: classes) {
|
||||||
// create binaries and install into /usr/local/bin
|
from sourceSets.main.allSource
|
||||||
// PREREQUISITE: graalvm / native-image must be installed - see https://www.graalvm.org/
|
archiveClassifier.set("sources")
|
||||||
// NOTE: May take up tp 10 Min altogether!
|
|
||||||
tasks.register('binariesInstall') {
|
|
||||||
dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
|
|
||||||
doLast {
|
|
||||||
println "Building binaries ..."
|
|
||||||
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-desktop.jar") }
|
|
||||||
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-server.jar") }
|
|
||||||
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-syspec.jar") }
|
|
||||||
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-desktop /usr/local/bin/") }
|
|
||||||
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-server /usr/local/bin/") }
|
|
||||||
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-syspec /usr/local/bin/") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// publish to repo.prod.meissa.de with task "publishLibraryPublicationToMeissaRepository" -- (using pattern "publishLibraryPublicationTo<MAVEN REPOSITORY NAME>Repository")
|
|
||||||
publishing {
|
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) {
|
||||||
|
// see https://docs.gitlab.com/ee/user/packages/maven_repository/index.html
|
||||||
maven {
|
maven {
|
||||||
name = "gitlab"
|
url "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
|
||||||
url = "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
|
name "GitLab"
|
||||||
credentials(HttpHeaderCredentials) {
|
credentials(HttpHeaderCredentials) {
|
||||||
name = "Job-Token"
|
name = "Job-Token"
|
||||||
value = System.getenv("CI_JOB_TOKEN")
|
value = System.getenv("CI_JOB_TOKEN")
|
||||||
|
@ -200,54 +194,8 @@ publishing {
|
||||||
header(HttpHeaderAuthentication)
|
header(HttpHeaderAuthentication)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
maven {
|
|
||||||
name = "meissa"
|
|
||||||
url = uri("https://repo.prod.meissa.de/api/packages/meissa/maven")
|
|
||||||
|
|
||||||
credentials(HttpHeaderCredentials) {
|
|
||||||
name = "Authorization"
|
|
||||||
def publishPackageTokenName = "MEISSA_PUBLISH_PACKAGE_TOKEN"
|
|
||||||
if (System.getenv("CI_JOB_TOKEN") != null) {
|
|
||||||
def tokenFromEnv = System.getenv(publishPackageTokenName)
|
|
||||||
if (tokenFromEnv == null) {
|
|
||||||
println "Error: $publishPackageTokenName not found"
|
|
||||||
} else {
|
} else {
|
||||||
value = "token " + tokenFromEnv
|
mavenLocal()
|
||||||
println "$publishPackageTokenName found - "
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// use project-property (define e.g. in "~/.gradle/gradle.properties") when not running in ci
|
|
||||||
// you can create a token in gitea "Profile and Settings ... > Settings > Applications", Token Name, Select scopes (write:package) > "Generate Token"
|
|
||||||
if (!project.hasProperty(publishPackageTokenName)) {
|
|
||||||
// if token is missing, provide a dummy in order to avoid error "Could not get unknown property 'MEISSA_PUBLISH_PACKAGE_TOKEN' for Credentials [header: Authorization]" for other gradle tasks
|
|
||||||
ext.MEISSA_PUBLISH_PACKAGE_TOKEN = "Token $publishPackageTokenName not provided in file \".gradle/gradle.properties\""
|
|
||||||
println "Error: Token $publishPackageTokenName not found"
|
|
||||||
} else {
|
|
||||||
value = "token " + project.property(publishPackageTokenName)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication {
|
|
||||||
header(HttpHeaderAuthentication)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// create version file to allow Kotlin code to print own version - see https://stackoverflow.com/questions/33020069/how-to-get-version-attribute-from-a-gradle-build-to-be-included-in-runtime-swing
|
|
||||||
tasks.register('createVersion') {
|
|
||||||
dependsOn processResources
|
|
||||||
doLast {
|
|
||||||
def version = project.version.toString()
|
|
||||||
def fileName = "src/main/resources/version.txt"
|
|
||||||
def file = new File(fileName)
|
|
||||||
file.write(version)
|
|
||||||
println "Created file: " + fileName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
classes {
|
|
||||||
dependsOn createVersion
|
|
||||||
}
|
|
||||||
|
|
141
build.py
141
build.py
|
@ -1,141 +0,0 @@
|
||||||
import os
|
|
||||||
from subprocess import run
|
|
||||||
from pybuilder.core import task, init
|
|
||||||
from ddadevops import *
|
|
||||||
|
|
||||||
|
|
||||||
name = "provs"
|
|
||||||
PROJECT_ROOT_PATH = "."
|
|
||||||
|
|
||||||
|
|
||||||
version = "0.38.3-dev"
|
|
||||||
|
|
||||||
|
|
||||||
@init
|
|
||||||
def initialize0(project):
|
|
||||||
"""
|
|
||||||
workaround to avoid prompt for gopass if no artifacts need to be uploaded
|
|
||||||
usage: with option "-E ng" , e.g. "pyb -E artifacts patch_local"
|
|
||||||
"""
|
|
||||||
os.environ["RELEASE_ARTIFACT_TOKEN"] = "dummy" # avoids prompt for RELEASE_ARTIFACT_TOKEN
|
|
||||||
|
|
||||||
|
|
||||||
@init(environments=["artifacts"])
|
|
||||||
def initialize1(project):
|
|
||||||
"""
|
|
||||||
prompt for gopass if artifacts need to be uploaded
|
|
||||||
usage: with option "-E artifacts" , e.g. "pyb -E artifacts dev"
|
|
||||||
"""
|
|
||||||
del os.environ["RELEASE_ARTIFACT_TOKEN"]
|
|
||||||
|
|
||||||
|
|
||||||
@init
|
|
||||||
def initialize2(project):
|
|
||||||
input = {
|
|
||||||
"name": name,
|
|
||||||
"module": "notused",
|
|
||||||
"stage": "notused",
|
|
||||||
"project_root_path": PROJECT_ROOT_PATH,
|
|
||||||
"build_types": [],
|
|
||||||
"mixin_types": ["RELEASE"],
|
|
||||||
"release_main_branch": "main",
|
|
||||||
"release_primary_build_file": "build.gradle",
|
|
||||||
"release_secondary_build_files": ["build.py"],
|
|
||||||
# release artifacts
|
|
||||||
"release_artifact_server_url": "https://repo.prod.meissa.de",
|
|
||||||
"release_organisation": "meissa",
|
|
||||||
"release_repository_name": name,
|
|
||||||
"release_artifacts": [
|
|
||||||
"build/libs/provs-server.jar",
|
|
||||||
"build/libs/provs-desktop.jar",
|
|
||||||
"build/libs/provs-syspec.jar",
|
|
||||||
"build/libs/sha256sum.lst",
|
|
||||||
"build/libs/sha512sum.lst",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
build = ReleaseMixin(project, input)
|
|
||||||
build.initialize_build_dir()
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def dev(project):
|
|
||||||
"""
|
|
||||||
to avoid gopass prompt set RELEASE_ARTIFACT_TOKEN e.g.:
|
|
||||||
RELEASE_ARTIFACT_TOKEN=xxx pyb dev
|
|
||||||
"""
|
|
||||||
run("./gradlew assemble", shell=True)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def build(project):
|
|
||||||
run("./gradlew assemble", shell=True)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def patch(project):
|
|
||||||
"""
|
|
||||||
updates version to next patch level, creates a tag, creates new SNAPSHOT version,
|
|
||||||
commits primary build file (build.gradle) and pushes to remote
|
|
||||||
"""
|
|
||||||
increase_version_number(project, "PATCH")
|
|
||||||
release(project)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def minor(project):
|
|
||||||
"""
|
|
||||||
updates version to next minor level, creates a tag, creates new SNAPSHOT version,
|
|
||||||
commits primary build file (build.gradle) and pushes to remote
|
|
||||||
"""
|
|
||||||
increase_version_number(project, "MINOR")
|
|
||||||
release(project)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def major(project):
|
|
||||||
"""
|
|
||||||
updates version to next major level, creates a tag, creates new SNAPSHOT version,
|
|
||||||
commits primary build file (build.gradle) and pushes to remote
|
|
||||||
"""
|
|
||||||
increase_version_number(project, "MAJOR")
|
|
||||||
release(project)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def tag(project):
|
|
||||||
build = get_devops_build(project)
|
|
||||||
build.tag_bump_and_push_release()
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def release(project):
|
|
||||||
build = get_devops_build(project)
|
|
||||||
build.prepare_release()
|
|
||||||
tag(project)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def package(project):
|
|
||||||
run("./gradlew assemble -x test jar", shell=True)
|
|
||||||
run("./gradlew assemble -x test uberjarDesktop", shell=True)
|
|
||||||
run("./gradlew assemble -x test uberjarServer", shell=True)
|
|
||||||
run("./gradlew assemble -x test uberjarSyspec", shell=True)
|
|
||||||
run("cd build/libs/ && find . -type f -exec sha256sum {} \; | sort > sha256sum.lst", shell=True)
|
|
||||||
run("cd build/libs/ && find . -type f -exec sha512sum {} \; | sort > sha512sum.lst", shell=True)
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def publish_release(project):
|
|
||||||
""" creates a release in repo.meissa and uploads artifacts (jar-files and checksum files) """
|
|
||||||
build = get_devops_build(project)
|
|
||||||
build.publish_artifacts()
|
|
||||||
|
|
||||||
|
|
||||||
@task
|
|
||||||
def inst(project):
|
|
||||||
run("./gradlew inst", shell=True)
|
|
||||||
|
|
||||||
|
|
||||||
def increase_version_number(project, release_type):
|
|
||||||
build = get_devops_build(project)
|
|
||||||
build.update_release_type(release_type)
|
|
63
doc/CliApplication.md
Normal file
63
doc/CliApplication.md
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
```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
|
||||||
|
```
|
|
@ -1,55 +0,0 @@
|
||||||
```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
|
|
||||||
|
|
||||||
```
|
|
|
@ -1,38 +0,0 @@
|
||||||
```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
|
|
||||||
|
|
||||||
```
|
|
|
@ -1,8 +1,22 @@
|
||||||
This page provides information for developers.
|
# Information for developers
|
||||||
|
|
||||||
# Tasks
|
## Create a provs jar-file
|
||||||
|
|
||||||
## What is a task ?
|
* Clone this repo
|
||||||
|
* Build the jar-file by `./gradlew uberjarDesktop`
|
||||||
|
* In folder build/libs you'll find the file `provs-desktop.jar`
|
||||||
|
|
||||||
|
This uberjar is a Java jar-file including all required dependencies.
|
||||||
|
|
||||||
|
## Task
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
fun Prov.provisionK8s() = task { /* ... code and subtasks come here ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're having a deeper look into the provs code, you'll see regularly a task definition like this and might wonder ...
|
||||||
|
|
||||||
|
### What is a task ?
|
||||||
|
|
||||||
A task is the **basic execution unit** in provs. When executed, each task produces exactly one result (line) with either success or failure.
|
A task is the **basic execution unit** in provs. When executed, each task produces exactly one result (line) with either success or failure.
|
||||||
|
|
||||||
|
@ -12,108 +26,9 @@ The success or failure is computed automatically in the following way:
|
||||||
* a task defined with **optional** (i.e. `= optional { /* ... */ }` always returns success (even if there are failing subtasks)
|
* a task defined with **optional** (i.e. `= optional { /* ... */ }` always returns success (even if there are failing subtasks)
|
||||||
* **requireLast** defines a task which must provide an explicit result and solely this result counts for success calculation
|
* **requireLast** defines a task which must provide an explicit result and solely this result counts for success calculation
|
||||||
|
|
||||||
## Task declaration
|
|
||||||
|
|
||||||
### Recommended way
|
|
||||||
|
|
||||||
A task can be declared by
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
fun Prov.myCustomTask() = task { /* ... code and subtasks come here ... */ }
|
|
||||||
// e.g.
|
|
||||||
fun Prov.myEchoTask() = task {
|
|
||||||
cmd("echo hello world!")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The task will succeed if all sub-tasks (called tasks during execution) have succeeded resp. if no sub-task was called.
|
|
||||||
|
|
||||||
### Alternative ways
|
|
||||||
|
|
||||||
The following ways are equivalent but are more verbose:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
// Redundant declaration of the return type (ProvResult), which is already declared by task
|
|
||||||
fun Prov.myCustomTask(): ProvResult = task { /* ... code and subtasks come here ... */ }
|
|
||||||
|
|
||||||
// Redundant parentheses behind task
|
|
||||||
fun Prov.myCustomTask() = task() { /* ... code and subtasks come here ... */ }
|
|
||||||
|
|
||||||
// Redundant definition of the task name, but could be used to output a different task name
|
|
||||||
fun Prov.myCustomTask() = task("myCustomTask") { /* ... code and subtasks come here ... */ }
|
|
||||||
|
|
||||||
// Functionally equal, but with additional curly brackets
|
|
||||||
fun Prov.myCustomTask() { task { /* ... code and subtasks come here ... */ } }
|
|
||||||
```
|
|
||||||
|
|
||||||
Btw, the following lines and WILL NOT work as expected.
|
|
||||||
Due to too much lamda nesting, the code within the task is NOT executed:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
fun Prov.myCustomTask() = { task { /* ... code and subtasks come here ... */ } }
|
|
||||||
fun Prov.myCustomTask() {{ task { /* ... code and subtasks come here ... */ } }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add custom results
|
|
||||||
|
|
||||||
If you want to add a result explicitly, you can use method `addResultToEval`.
|
|
||||||
This maxy be used e.g. to add explicitly an error line, like in:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
fun Prov.myCustomTask() = task {
|
|
||||||
/* some other code ... */
|
|
||||||
addResultToEval(ProvResult(false, err = "my error msg"))
|
|
||||||
/* some other code ... */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
or alternatively you can use `taskWithResult`.
|
|
||||||
|
|
||||||
#### TaskWithResult
|
|
||||||
|
|
||||||
In case you want to include the return value (of type `ProvResult`) of a task to be added to the evaluation,
|
|
||||||
you can use `taskWithResult` instead of `task` and return the value, e.g. like
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
fun Prov.myEchoTask() = taskWithResult {
|
|
||||||
cmd("echo hello world!")
|
|
||||||
// ...
|
|
||||||
ProvResult(false, "Error: ... error message ...") // will be the returned as return value and included in the evaluation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
IMPORTANT: the value you want to return must be placed at the end of the lambda code (as usual in functional programming)!
|
|
||||||
The following will NOT work as expected:
|
|
||||||
|
|
||||||
```kotlin
|
|
||||||
fun Prov.myEchoTask() = taskWithResult {
|
|
||||||
ProvResult(false, "Error: ... error message ...") // will be ignored
|
|
||||||
// the result from the call below (i.e. from task "cmd") will be returned by myEchoTask,
|
|
||||||
// which is redundant as its result is already included in the evaluation anyway.
|
|
||||||
cmd("echo hello world!")
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Task output
|
|
||||||
|
|
||||||
If a task is run e.g. with `local().myEchoTask()`, it will produce output like
|
|
||||||
```
|
|
||||||
> Success -- myEchoTask
|
|
||||||
---> Success -- cmd [/bin/bash, -c, echo hello world!]
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Call hierarchy
|
## Call hierarchy
|
||||||
|
|
||||||
In the following link you can find an example of a sequence diagram when provisioning a desktop:
|
Find below an example of a sequence diagram when provisioning a desktop workplace:
|
||||||
|
|
||||||
[ProvisionDesktopSequence.md](ProvisionDesktopSequence.md)
|
![img.png](resources/provision-workplace-sequence.diagram.png)
|
||||||
|
|
||||||
|
|
||||||
## Create a provs jar-file
|
|
||||||
|
|
||||||
* Clone this repo
|
|
||||||
* Build the jar-file by `./gradlew uberjarDesktop`
|
|
||||||
* In folder build/libs you'll find the file `provs-desktop.jar`
|
|
||||||
|
|
||||||
This uberjar is a Java jar-file including all required dependencies.
|
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
# Go / forgejo Installation and Testing
|
|
||||||
|
|
||||||
## go install/update
|
|
||||||
#### remove old version
|
|
||||||
sudo rm -rf ~/go
|
|
||||||
### download latest version and configure
|
|
||||||
curl -OL https://go.dev/dl/go1.21.3.linux-amd64.tar.gz
|
|
||||||
|
|
||||||
# extract latest version to ~/go
|
|
||||||
tar -C ~ -xzf go*.linux-amd64.tar.gz
|
|
||||||
|
|
||||||
# append path
|
|
||||||
```
|
|
||||||
(meissa) jem@meissa-ide-2023:~$ cat .bashrc.d/go.sh
|
|
||||||
PATH=$PATH:$HOME/go/bin
|
|
||||||
export PATH
|
|
||||||
```
|
|
||||||
|
|
||||||
## VScode optional - TODO!?!
|
|
||||||
"Go for VS Code v0.39.1"
|
|
||||||
|
|
||||||
## Testing forgejo
|
|
||||||
full:
|
|
||||||
make test
|
|
||||||
|
|
||||||
require node:
|
|
||||||
make test-frontend
|
|
||||||
|
|
||||||
require go:
|
|
||||||
make test-backend
|
|
||||||
|
|
||||||
#nvm - required to build forgejo frontend
|
|
||||||
sudo apt remove nodejs
|
|
||||||
sudo apt autoremove
|
|
||||||
|
|
||||||
adapt version to latest:
|
|
||||||
curl o https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash
|
|
||||||
nvm install
|
|
||||||
|
|
||||||
optional:
|
|
||||||
nvm alias default "latest"
|
|
||||||
|
|
||||||
## forgejo build
|
|
||||||
TAGS="bindata" make build
|
|
||||||
-> include make frontend & make backend //see details Makefile
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Modules
|
|
||||||
|
|
||||||
## Modules and their possible relations
|
|
||||||
|
|
||||||
![modularization.png](resources/modularization.png)
|
|
||||||
|
|
||||||
|
|
||||||
#### Modules
|
|
||||||
|
|
||||||
A,B,C: Modules with both domain and infrastructure layer code - common type of modules
|
|
||||||
D: Module with only domain: can sometimes make sense if only domain logic and no infrastructure logic is required
|
|
||||||
E: Module with only infrastructure: usually utility modules that just provide a collection of infrastructure functionality
|
|
||||||
|
|
||||||
#### Interactions
|
|
||||||
|
|
||||||
1. Domain calls (a function in) the infrastructure of the same module - common practice within a module
|
|
||||||
1. Domain calls (a function in) the domain another module - common practice between modules
|
|
||||||
1. Infrastructure calls infrastructure of another module - usually not recommended
|
|
||||||
1. Domain calls infrastructure in another module - can make sense in some cases e.g. if module D just needs some low-level function of module D. However where possible calling domain of module C should be preferred
|
|
||||||
1. Domain calls infrastructure in another module, which only has infrastructure - common practice for calling utility modules, which don't have a domain.
|
|
35
doc/ProvSequence.puml
Normal file
35
doc/ProvSequence.puml
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
@startuml
|
||||||
|
autonumber
|
||||||
|
|
||||||
|
Application -> Prov: create
|
||||||
|
|
||||||
|
activate Prov
|
||||||
|
Application -> DesktopService.kt: provisionDesktop(prov, ...)
|
||||||
|
DesktopService.kt -> Install.kt: aptInstall(prov, lambda=cmd "apt install", ..)
|
||||||
|
Install.kt -> Prov: taskWithResult
|
||||||
|
|
||||||
|
activate Prov
|
||||||
|
Prov -> Prov: evaluate
|
||||||
|
|
||||||
|
activate Prov
|
||||||
|
Prov -> Prov: initProgress (bei level 0)
|
||||||
|
Prov -> Prov: progress
|
||||||
|
|
||||||
|
activate Prov
|
||||||
|
Prov -> Prov: lambda
|
||||||
|
|
||||||
|
activate Prov
|
||||||
|
Prov -> Processor: exec
|
||||||
|
deactivate Prov
|
||||||
|
|
||||||
|
Prov <-- Prov: ProvResult
|
||||||
|
deactivate Prov
|
||||||
|
|
||||||
|
Prov -> Prov: endProgress (bei level 0)
|
||||||
|
Prov -> Prov: printResults (bei level 0)
|
||||||
|
|
||||||
|
deactivate Prov
|
||||||
|
deactivate Prov
|
||||||
|
Install.kt <-- Prov: ProvResult
|
||||||
|
|
||||||
|
@enduml
|
|
@ -1,47 +0,0 @@
|
||||||
```plantuml
|
|
||||||
@startuml
|
|
||||||
autonumber
|
|
||||||
|
|
||||||
participant Application
|
|
||||||
participant DesktopService
|
|
||||||
participant Install
|
|
||||||
participant Prov
|
|
||||||
participant Processor
|
|
||||||
|
|
||||||
Application -> Prov: create
|
|
||||||
|
|
||||||
activate Prov
|
|
||||||
Application -> DesktopService: provisionDesktop(prov, ...)
|
|
||||||
DesktopService -> Install: prov.aptInstall()
|
|
||||||
Install -> Prov: taskWithResult( lambda = cmd("sudo apt install ...") )
|
|
||||||
|
|
||||||
activate Prov
|
|
||||||
Prov -> Prov: evaluate
|
|
||||||
|
|
||||||
activate Prov
|
|
||||||
Prov -> Prov: initProgress (if level 0)
|
|
||||||
Prov -> Prov: progress
|
|
||||||
|
|
||||||
activate Prov
|
|
||||||
Prov -> Prov: lambda
|
|
||||||
|
|
||||||
activate Prov
|
|
||||||
|
|
||||||
Prov -> Processor: exec
|
|
||||||
Prov <-- Processor: exec
|
|
||||||
|
|
||||||
deactivate Prov
|
|
||||||
deactivate Prov
|
|
||||||
|
|
||||||
Prov -> Prov: endProgress (if level 0)
|
|
||||||
Prov -> Prov: printResults (if level 0)
|
|
||||||
|
|
||||||
deactivate Prov
|
|
||||||
deactivate Prov
|
|
||||||
|
|
||||||
Install <-- Prov: ProvResult
|
|
||||||
DesktopService <-- Install
|
|
||||||
Application <-- DesktopService
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
```
|
|
|
@ -1,53 +0,0 @@
|
||||||
```plantuml
|
|
||||||
@startuml
|
|
||||||
|
|
||||||
autonumber
|
|
||||||
|
|
||||||
skinparam sequenceBox {
|
|
||||||
borderColor White
|
|
||||||
}
|
|
||||||
|
|
||||||
participant User
|
|
||||||
|
|
||||||
box "application" #LightBlue
|
|
||||||
participant Application
|
|
||||||
participant CliArgumentsParser
|
|
||||||
participant DesktopCliCommand
|
|
||||||
participant ProvWithSudo
|
|
||||||
end box
|
|
||||||
|
|
||||||
box #White
|
|
||||||
participant CliUtils
|
|
||||||
participant "Prov (local or remote...)" as ProvInstance
|
|
||||||
end box
|
|
||||||
|
|
||||||
box "domain" #LightGreen
|
|
||||||
participant "DesktopService"
|
|
||||||
end box
|
|
||||||
|
|
||||||
box "infrastructure" #CornSilk
|
|
||||||
participant ConfigRepository
|
|
||||||
participant "Various\ninfrastructure functions" as Infrastructure_functions
|
|
||||||
end box
|
|
||||||
|
|
||||||
|
|
||||||
User -> Application ++ : main(args...)
|
|
||||||
Application -> CliArgumentsParser : parseCommand
|
|
||||||
Application -> DesktopCliCommand : isValid ?
|
|
||||||
Application -> CliUtils : createProvInstance
|
|
||||||
ProvInstance <- CliUtils : create
|
|
||||||
|
|
||||||
Application -> ProvWithSudo : ensureSudoWithoutPassword
|
|
||||||
Application -> DesktopService : provisionDesktopCommand ( provInstance, desktopCliCommand )
|
|
||||||
|
|
||||||
DesktopService -> ConfigRepository : getConfig
|
|
||||||
|
|
||||||
DesktopService -> DesktopService : provisionDesktop( config )
|
|
||||||
|
|
||||||
DesktopService -> Infrastructure_functions: Various calls like:
|
|
||||||
DesktopService -> Infrastructure_functions: install ssh, gpg, git ...
|
|
||||||
DesktopService -> Infrastructure_functions: installVirtualBoxGuestAdditions
|
|
||||||
DesktopService -> Infrastructure_functions: configureNoSwappiness, ...
|
|
||||||
|
|
||||||
@enduml
|
|
||||||
```
|
|
|
@ -2,6 +2,8 @@ This repository holds the documentation of the provs framework.
|
||||||
|
|
||||||
# Design principles
|
# Design principles
|
||||||
|
|
||||||
|
For usage examples it is recommended to have a look at [provs-scripts](https://gitlab.com/domaindrivenarchitecture/provs-scripts) or [provs-ubuntu-extensions](https://gitlab.com/domaindrivenarchitecture/provs-ubuntu-extensions).
|
||||||
|
|
||||||
## "Implarative"
|
## "Implarative"
|
||||||
|
|
||||||
Configuration management tools are usually classified as either **imperative** or **declarative**.
|
Configuration management tools are usually classified as either **imperative** or **declarative**.
|
||||||
|
@ -26,3 +28,45 @@ 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._
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ release-1.2 or release-1.2.3
|
||||||
|
|
||||||
I.e.: release-X.X.Z where X, Y, Z are the major, minor resp. the patch level of the release. Z can be omitted.
|
I.e.: release-X.X.Z where X, Y, Z are the major, minor resp. the patch level of the release. Z can be omitted.
|
||||||
|
|
||||||
**Note:** Such kind of release tags should only be applied to commits in the main branch.
|
**Note:** Such kind of release tags should only be applied to commits in the master branch.
|
||||||
|
|
||||||
```
|
```
|
||||||
#adjust [version]
|
#adjust [version]
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
# ADR: We implement domain services static
|
|
||||||
|
|
||||||
Domain services can be implemented either as object (and composed like done in spring / example1 ) or with extension
|
|
||||||
function and composed static (see example2).
|
|
||||||
|
|
||||||
## example1
|
|
||||||
```kotlin
|
|
||||||
class DesktopServie(val aptApi: AptApi, val prov: Prov) {
|
|
||||||
fun provisionIdeDesktop(onlyModules: List<String>? = null) {
|
|
||||||
prov.task {
|
|
||||||
if (onlyModules == null) {
|
|
||||||
aptApi.aptInstall(OPEN_VPM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## example2
|
|
||||||
```kotlin
|
|
||||||
fun Prov.provisionIdeDesktop(onlyModules: List<String>? = null) {
|
|
||||||
if (onlyModules == null) {
|
|
||||||
aptInstall(OPEN_VPM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Decission
|
|
||||||
|
|
||||||
We use extension function and composed static.
|
|
||||||
|
|
||||||
## Reason
|
|
||||||
|
|
||||||
1. Similar to composed objects we can easily mock `aptInstall` in tests. Both solutions are equivalent.
|
|
||||||
2. Inheritance in case of composed objects we can solve by static composition.
|
|
||||||
3. Object composition we can solve by static composition.
|
|
||||||
|
|
||||||
There is no reason left to change the current implementd pattern.
|
|
|
@ -1,77 +0,0 @@
|
||||||
|
|
||||||
## Initialization
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
actor user
|
|
||||||
participant app as Application
|
|
||||||
participant ds as DesktopService
|
|
||||||
participant gtr as KnownHost
|
|
||||||
participant pa as CliArgumentsParser
|
|
||||||
participant cr as DesktopConfigRepository
|
|
||||||
participant ut as CliUtils
|
|
||||||
participant su as ProvsWithSudo
|
|
||||||
|
|
||||||
user ->> app: main
|
|
||||||
activate app
|
|
||||||
app ->> pa: parseCommands
|
|
||||||
app ->> cr: getConfig(configFileName)
|
|
||||||
app ->> ut: createProvInstance(cmd.target)
|
|
||||||
app ->> su: ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
|
|
||||||
app ->> ds: provisionDesktopCommand(cmd, config)
|
|
||||||
activate ds
|
|
||||||
ds ->> gtr: values()
|
|
||||||
gtr -->> ds: List(KnownHost)
|
|
||||||
deactivate ds
|
|
||||||
deactivate app
|
|
||||||
```
|
|
||||||
|
|
||||||
## Domain
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
|
|
||||||
namespace configuration {
|
|
||||||
|
|
||||||
class TargetCliCommand {
|
|
||||||
val target: String,
|
|
||||||
val passwordInteractive: Boolean = false
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigFileName {
|
|
||||||
fileName: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace desktop {
|
|
||||||
|
|
||||||
class DesktopCliCommand {
|
|
||||||
}
|
|
||||||
|
|
||||||
class DesktopConfig {
|
|
||||||
val ssh: SshKeyPairSource? = null,
|
|
||||||
val gpg: KeyPairSource? = null,
|
|
||||||
val gitUserName: String? = null,
|
|
||||||
val gitEmail: String? = null,
|
|
||||||
}
|
|
||||||
|
|
||||||
class DesktopType {
|
|
||||||
val name: String
|
|
||||||
}
|
|
||||||
class DesktopOnlyModule {
|
|
||||||
<<enum>>
|
|
||||||
FIREFOX, VERIFY
|
|
||||||
}
|
|
||||||
|
|
||||||
class KnownHost {
|
|
||||||
hostName: String,
|
|
||||||
hostKeys: List<HostKey>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopCliCommand "1" *-- "1" DesktopType: type
|
|
||||||
DesktopCliCommand "1" *-- "1" TargetCliCommand: target
|
|
||||||
DesktopCliCommand "1" *-- "1" ConfigFileName: configFile
|
|
||||||
DesktopCliCommand "1" *-- "..n" DesktopOnlyModule: onlyModules
|
|
||||||
|
|
||||||
```
|
|
|
@ -1,10 +0,0 @@
|
||||||
### Howto update gradle wrapper
|
|
||||||
|
|
||||||
1. To *latest* version (be aware for deprecated parts in future versions):
|
|
||||||
```shell
|
|
||||||
./gradlew wrapper --gradle-version latest
|
|
||||||
```
|
|
||||||
2. To *specific version:
|
|
||||||
```shell
|
|
||||||
./gradlew wrapper --gradle-version 8.6
|
|
||||||
```
|
|
Binary file not shown.
Before Width: | Height: | Size: 45 KiB |
BIN
doc/resources/prov-module-dependencies-5b.png
Normal file
BIN
doc/resources/prov-module-dependencies-5b.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 33 KiB |
BIN
doc/resources/provision-workplace-sequence.diagram.png
Normal file
BIN
doc/resources/provision-workplace-sequence.diagram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
BIN
doc/resources/provs-architecture-7.png
Normal file
BIN
doc/resources/provs-architecture-7.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 100 KiB |
|
@ -3,6 +3,7 @@ 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(
|
||||||
|
@ -16,3 +17,13 @@ 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)
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
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.)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 not found. Please check if path is correct.")
|
throw RuntimeException("Config file ${configFileName.fileName} not found. Please check if path is correct.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
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.printProvsVersion
|
|
||||||
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -18,47 +11,25 @@ import kotlin.system.exitProcess
|
||||||
*/
|
*/
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|
||||||
printProvsVersion()
|
|
||||||
|
|
||||||
val cmd = CliArgumentsParser("provs-desktop.jar subcommand target").parseCommand(args)
|
val cmd = CliArgumentsParser("provs-desktop.jar subcommand target").parseCommand(args)
|
||||||
if (!cmd.isValid()) {
|
if (!cmd.isValid()) {
|
||||||
println("Arguments are not valid, pls try option -h for help.")
|
println("Arguments are not valid, pls try option -h for help.")
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultConfigFileName = "desktop-config.yaml"
|
val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true)
|
||||||
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 {
|
try {
|
||||||
getConfig(configFileName)
|
provisionDesktopCommand(prov, cmd)
|
||||||
} catch (_: SerializationException) {
|
} catch (e: SerializationException) {
|
||||||
println(
|
println(
|
||||||
"Error: File \"${configFileName}\" has an invalid format and or invalid data."
|
"Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n"
|
||||||
)
|
)
|
||||||
null
|
} catch (e: FileNotFoundException) {
|
||||||
} catch (_: FileNotFoundException) {
|
|
||||||
println(
|
println(
|
||||||
"Error: File\u001b[31m $configFileName \u001b[0m was not found.\n" +
|
"Error: File\u001b[31m ${cmd.configFile?.fileName} \u001b[0m was not found.\n" +
|
||||||
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m $configFileName \u001B[0m " +
|
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${cmd.configFile?.fileName} \u001B[0m " +
|
||||||
"and change the content according to your needs."
|
"and change the content according to your needs.\n"
|
||||||
)
|
)
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config == null) {
|
|
||||||
println("No suitable config found.")
|
|
||||||
quit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
val prov = createProvInstance(cmd.target)
|
|
||||||
|
|
||||||
prov.session {
|
|
||||||
ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
|
|
||||||
provisionDesktopCommand(cmd, config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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
|
||||||
|
@ -11,7 +10,6 @@ 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())
|
||||||
|
@ -25,14 +23,12 @@ 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
|
||||||
)
|
)
|
||||||
|
@ -59,7 +55,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 = only?.let { listOf(it.name.lowercase()) }
|
onlyModules = if (only != null) listOf(only!!.name.lowercase()) else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
package org.domaindrivenarchitecture.provs.desktop.domain
|
package org.domaindrivenarchitecture.provs.desktop.domain
|
||||||
|
|
||||||
enum class DesktopOnlyModule {
|
enum class DesktopOnlyModule {
|
||||||
FIREFOX, VERIFY
|
TEAMS, FIREFOX, VERIFY
|
||||||
;
|
|
||||||
|
|
||||||
fun isIn(list: List<String>): Boolean {
|
|
||||||
return list.any { it.equals(this.name, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package org.domaindrivenarchitecture.provs.desktop.domain
|
package org.domaindrivenarchitecture.provs.desktop.domain
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopOnlyModule.FIREFOX
|
|
||||||
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopOnlyModule.VERIFY
|
|
||||||
import org.domaindrivenarchitecture.provs.desktop.infrastructure.*
|
import org.domaindrivenarchitecture.provs.desktop.infrastructure.*
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.git.provisionGit
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.git.provisionGit
|
||||||
|
@ -11,26 +9,22 @@ 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.currentUserCanSudoWithoutPassword
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo
|
||||||
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 {
|
// retrieve config
|
||||||
|
val conf = if (cmd.configFile != null) getConfig(cmd.configFile.fileName) else DesktopConfig()
|
||||||
|
|
||||||
validatePrecondition()
|
prov.provisionDesktop(
|
||||||
|
|
||||||
val only = cmd.onlyModules
|
|
||||||
if (only == null) {
|
|
||||||
provisionDesktop(
|
|
||||||
cmd.type,
|
cmd.type,
|
||||||
conf.ssh?.keyPair(),
|
conf.ssh?.keyPair(),
|
||||||
conf.gpg?.keyPair(),
|
conf.gpg?.keyPair(),
|
||||||
conf.gitUserName,
|
conf.gitUserName,
|
||||||
conf.gitEmail,
|
conf.gitEmail,
|
||||||
|
cmd.onlyModules
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
provisionOnlyModules(cmd.type, only)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -48,107 +42,36 @@ internal fun Prov.provisionDesktop(
|
||||||
gpg: KeyPair? = null,
|
gpg: KeyPair? = null,
|
||||||
gitUserName: String? = null,
|
gitUserName: String? = null,
|
||||||
gitEmail: String? = null,
|
gitEmail: String? = null,
|
||||||
|
onlyModules: List<String>?
|
||||||
) = task {
|
) = task {
|
||||||
|
validatePrecondition()
|
||||||
provisionBasicDesktop(gpg, ssh, gitUserName, gitEmail)
|
provisionBasicDesktop(gpg, ssh, gitUserName, gitEmail, onlyModules)
|
||||||
|
|
||||||
if (desktopType == DesktopType.OFFICE) {
|
if (desktopType == DesktopType.OFFICE) {
|
||||||
provisionOfficeDesktop()
|
provisionOfficeDesktop(onlyModules)
|
||||||
|
if (onlyModules == null) {
|
||||||
verifyOfficeSetup()
|
verifyOfficeSetup()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (desktopType == DesktopType.IDE) {
|
if (desktopType == DesktopType.IDE) {
|
||||||
|
if (onlyModules == null) {
|
||||||
provisionOfficeDesktop()
|
provisionOfficeDesktop()
|
||||||
provisionIdeDesktop()
|
provisionIdeDesktop()
|
||||||
verifyIdeSetup()
|
verifyIdeSetup()
|
||||||
}
|
} else {
|
||||||
}
|
provisionIdeDesktop(onlyModules)
|
||||||
|
|
||||||
internal fun Prov.provisionOnlyModules(
|
|
||||||
desktopType: DesktopType = DesktopType.BASIC,
|
|
||||||
onlyModules: List<String>
|
|
||||||
) = task {
|
|
||||||
|
|
||||||
if (FIREFOX.isIn(onlyModules)) {
|
|
||||||
installPpaFirefox()
|
|
||||||
}
|
|
||||||
if (VERIFY.isIn(onlyModules)) {
|
|
||||||
if (desktopType == DesktopType.OFFICE) {
|
|
||||||
verifyOfficeSetup()
|
|
||||||
} else if (desktopType == DesktopType.IDE) {
|
|
||||||
verifyIdeSetup()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.validatePrecondition() {
|
fun Prov.validatePrecondition() {
|
||||||
if (!currentUserCanSudoWithoutPassword()) {
|
if (!currentUserCanSudo()) {
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Prov.provisionIdeDesktop(onlyModules: List<String>? = null) {
|
||||||
fun Prov.provisionBasicDesktop(
|
if (onlyModules == null) {
|
||||||
gpg: KeyPair?,
|
|
||||||
ssh: SshKeyPair?,
|
|
||||||
gitUserName: String?,
|
|
||||||
gitEmail: String?,
|
|
||||||
) {
|
|
||||||
aptInstall(KEY_MANAGEMENT)
|
|
||||||
aptInstall(VERSION_MANAGEMENT)
|
|
||||||
aptInstall(NETWORK_TOOLS)
|
|
||||||
aptInstall(SCREEN_TOOLS)
|
|
||||||
aptInstall(KEY_MANAGEMENT_GUI)
|
|
||||||
aptInstall(PASSWORD_TOOLS)
|
|
||||||
aptInstall(OS_ANALYSIS)
|
|
||||||
aptInstall(BASH_UTILS)
|
|
||||||
aptInstall(CLIP_TOOLS)
|
|
||||||
aptPurge(
|
|
||||||
"remove-power-management xfce4-power-manager " +
|
|
||||||
"xfce4-power-manager-plugins xfce4-power-manager-data" +
|
|
||||||
"upower libimobiledevice6 libplist3 libusbmuxd6 usbmuxd bluez-cups"
|
|
||||||
)
|
|
||||||
aptPurge("abiword gnumeric")
|
|
||||||
aptPurge("popularity-contest")
|
|
||||||
|
|
||||||
provisionKeys(gpg, ssh)
|
|
||||||
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
|
|
||||||
|
|
||||||
installPpaFirefox()
|
|
||||||
installGopass()
|
|
||||||
configureGopass(publicGpgKey = gpg?.publicKey)
|
|
||||||
installGopassJsonApi()
|
|
||||||
downloadGopassBridge()
|
|
||||||
|
|
||||||
installRedshift()
|
|
||||||
configureRedshift()
|
|
||||||
|
|
||||||
configureNoSwappiness()
|
|
||||||
configureBash()
|
|
||||||
installVirtualBoxGuestAdditions()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.provisionOfficeDesktop() {
|
|
||||||
aptInstall(ZIP_UTILS)
|
|
||||||
aptInstall(SPELLCHECKING_DE)
|
|
||||||
aptInstall(BROWSER)
|
|
||||||
aptInstall(EMAIL_CLIENT)
|
|
||||||
installDeltaChat()
|
|
||||||
aptInstall(OFFICE_SUITE)
|
|
||||||
installZimWiki()
|
|
||||||
// installNextcloudClient() might not install - might need fix and working test
|
|
||||||
aptInstall(COMPARE_TOOLS)
|
|
||||||
|
|
||||||
// VSCode is also required in office VM (not only in IDE desktop) e.g. as editor
|
|
||||||
installVSCode("python", "clojure")
|
|
||||||
|
|
||||||
// optional, as installation of these tools often fail and as they are not mandatory
|
|
||||||
optional {
|
|
||||||
aptInstall(DRAWING_TOOLS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun Prov.provisionIdeDesktop() {
|
|
||||||
aptInstall(OPEN_VPM)
|
aptInstall(OPEN_VPM)
|
||||||
aptInstall(OPENCONNECT)
|
aptInstall(OPENCONNECT)
|
||||||
aptInstall(VPNC)
|
aptInstall(VPNC)
|
||||||
|
@ -160,10 +83,87 @@ fun Prov.provisionIdeDesktop() {
|
||||||
installShadowCljs()
|
installShadowCljs()
|
||||||
installDevOps()
|
installDevOps()
|
||||||
provisionPython()
|
provisionPython()
|
||||||
installHugoByDeb()
|
|
||||||
|
|
||||||
// IDEs
|
// IDEs
|
||||||
|
installVSC("python", "clojure")
|
||||||
installIntelliJ()
|
installIntelliJ()
|
||||||
|
} else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) {
|
||||||
installKubeconform()
|
verifyIdeSetup()
|
||||||
|
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
|
||||||
|
installFirefox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("unused") // used in other projects
|
||||||
|
fun Prov.provisionMSDesktop(onlyModules: List<String>?) {
|
||||||
|
if (onlyModules == null) {
|
||||||
|
installMsTeams()
|
||||||
|
} else if (onlyModules.contains(DesktopOnlyModule.TEAMS.name.lowercase())) {
|
||||||
|
installMsTeams()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.provisionOfficeDesktop(onlyModules: List<String>? = null) {
|
||||||
|
if (onlyModules == null) {
|
||||||
|
aptInstall(ZIP_UTILS)
|
||||||
|
aptInstall(BROWSER)
|
||||||
|
aptInstall(EMAIL_CLIENT)
|
||||||
|
installDeltaChat()
|
||||||
|
aptInstall(OFFICE_SUITE)
|
||||||
|
installZimWiki()
|
||||||
|
installNextcloudClient()
|
||||||
|
|
||||||
|
// optional as installation of these tools often fail and they are not considered mandatory
|
||||||
|
optional {
|
||||||
|
aptInstall(DRAWING_TOOLS)
|
||||||
|
}
|
||||||
|
|
||||||
|
aptInstall(SPELLCHECKING_DE)
|
||||||
|
} else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) {
|
||||||
|
verifyOfficeSetup()
|
||||||
|
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
|
||||||
|
installFirefox()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.provisionBasicDesktop(
|
||||||
|
gpg: KeyPair?,
|
||||||
|
ssh: SshKeyPair?,
|
||||||
|
gitUserName: String?,
|
||||||
|
gitEmail: String?,
|
||||||
|
onlyModules: List<String>?
|
||||||
|
) {
|
||||||
|
if (onlyModules == null) {
|
||||||
|
aptInstall(KEY_MANAGEMENT)
|
||||||
|
aptInstall(VERSION_MANAGEMENT)
|
||||||
|
aptInstall(NETWORK_TOOLS)
|
||||||
|
aptInstall(SCREEN_TOOLS)
|
||||||
|
aptInstall(KEY_MANAGEMENT_GUI)
|
||||||
|
aptInstall(PASSWORD_TOOLS)
|
||||||
|
aptInstall(OS_ANALYSIS)
|
||||||
|
aptInstall(BASH_UTILS)
|
||||||
|
aptInstall(CLIP_TOOLS)
|
||||||
|
aptPurge(
|
||||||
|
"remove-power-management xfce4-power-manager " +
|
||||||
|
"xfce4-power-manager-plugins xfce4-power-manager-data"
|
||||||
|
)
|
||||||
|
aptPurge("abiword gnumeric")
|
||||||
|
aptPurge("popularity-contest")
|
||||||
|
|
||||||
|
provisionKeys(gpg, ssh)
|
||||||
|
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
|
||||||
|
|
||||||
|
installFirefox()
|
||||||
|
installGopass()
|
||||||
|
installGopassBridgeJsonApi()
|
||||||
|
downloadGopassBridge()
|
||||||
|
installRedshift()
|
||||||
|
|
||||||
|
configureRedshift()
|
||||||
|
configureNoSwappiness()
|
||||||
|
configureBash()
|
||||||
|
installVirtualBoxGuestAdditions()
|
||||||
|
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
|
||||||
|
installFirefox()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.desktop.domain
|
|
||||||
|
|
||||||
typealias HostKey = String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a known host for ssh connections.
|
|
||||||
*
|
|
||||||
* @param hostName domain name or ip
|
|
||||||
* @param port (optional) to be specified if different from default port 22
|
|
||||||
* @param hostKeys list of keys, where each should contain separated by space: 1. keytype, 2. key and 3. (optionally) a comment
|
|
||||||
*
|
|
||||||
* See: https://man7.org/linux/man-pages/man8/sshd.8.html#SSH_KNOWN_HOSTS_FILE_FORMAT
|
|
||||||
*/
|
|
||||||
open class KnownHost(
|
|
||||||
val hostName: String,
|
|
||||||
val port: Int? = null,
|
|
||||||
val hostKeys: List<HostKey>
|
|
||||||
) {
|
|
||||||
constructor(hostName: String, hostKeys: List<HostKey>) : this(hostName, null, hostKeys)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val GITHUB = KnownHost(
|
|
||||||
"github.com",
|
|
||||||
listOf(
|
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl",
|
|
||||||
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=",
|
|
||||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val GITLAB = KnownHost(
|
|
||||||
"gitlab.com",
|
|
||||||
listOf(
|
|
||||||
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf",
|
|
||||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9",
|
|
||||||
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
protected val values = listOf(GITHUB, GITLAB)
|
|
||||||
|
|
||||||
fun values(): List<KnownHost> {
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.desktop.domain
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.addKnownHost
|
|
||||||
|
|
||||||
|
|
||||||
fun Prov.addKnownHosts(knownHosts: List<KnownHost> = KnownHost.values()) = task {
|
|
||||||
for (knownHost in knownHosts) {
|
|
||||||
addKnownHost(knownHost, verifyKeys = true)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
fun getConfig(filename: String): DesktopConfig = readFromFile(filename).yamlToType()
|
internal fun getConfig(filename: String = "desktop-config.yaml"): DesktopConfig = readFromFile(filename).yamlToType()
|
||||||
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
fun writeConfig(config: DesktopConfig, fileName: String = "desktop-config.yaml") = FileWriter(fileName).use { it.write(config.toYaml()) }
|
internal fun writeConfig(config: DesktopConfig, fileName: String = "desktop-config.yaml") = FileWriter(fileName).use { it.write(config.toYaml()) }
|
||||||
|
|
|
@ -7,21 +7,21 @@ 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 RESOURCE_PATH = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
|
private const val resourcePath = "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()
|
||||||
installKubectlAndTools()
|
installKubectlAndTools()
|
||||||
installYq()
|
installYq()
|
||||||
installGraalVM()
|
installAwsCredentials()
|
||||||
|
installDevOpsFolder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun Prov.installYq(
|
fun Prov.installYq(
|
||||||
version: String = "4.13.2",
|
version: String = "4.13.2",
|
||||||
sha256sum: String = "d7c89543d1437bf80fee6237eadc608d1b121c21a7cbbe79057d5086d74f8d79"
|
sha256sum: String = "d7c89543d1437bf80fee6237eadc608d1b121c21a7cbbe79057d5086d74f8d79"
|
||||||
) = task {
|
): ProvResult = task {
|
||||||
val path = "/usr/bin/"
|
val path = "/usr/bin/"
|
||||||
val filename = "yq"
|
val filename = "yq"
|
||||||
if (!checkFile(path + filename)) {
|
if (!checkFile(path + filename)) {
|
||||||
|
@ -38,85 +38,45 @@ fun Prov.installYq(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installKubectlAndTools() = task {
|
fun Prov.installKubectlAndTools(): ProvResult = task {
|
||||||
|
|
||||||
task("installKubectl") {
|
task("installKubectl") {
|
||||||
if (!checkFile(KUBE_CONFIG_CONTEXT_SCRIPT)) {
|
val kubeConfigFile = ".bashrc.d/kubectl.sh"
|
||||||
installKubectl()
|
if (!checkFile(kubeConfigFile)) {
|
||||||
configureKubectlBashCompletion()
|
// prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
|
||||||
|
cmd("sudo apt-get update")
|
||||||
|
aptInstall("apt-transport-https ca-certificates curl")
|
||||||
|
cmd("sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg")
|
||||||
|
cmd("echo \"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main\" | sudo tee /etc/apt/sources.list.d/kubernetes.list")
|
||||||
|
|
||||||
|
// kubectl and bash completion
|
||||||
|
cmd("sudo apt update")
|
||||||
|
aptInstall("kubectl")
|
||||||
|
cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true)
|
||||||
|
createDir(".bashrc.d")
|
||||||
|
createFileFromResource(kubeConfigFile, "kubectl.sh", resourcePath)
|
||||||
} else {
|
} else {
|
||||||
ProvResult(true, out = "Kubectl already installed")
|
ProvResult(true, out = "Kubectl already installed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task("installKubeconform") {
|
|
||||||
|
|
||||||
installKubeconform()
|
|
||||||
}
|
|
||||||
installDevopsScripts()
|
installDevopsScripts()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installKubeconform() = task {
|
fun Prov.installDevopsScripts() {
|
||||||
// check for latest stable release on: https://github.com/yannh/kubeconform/releases
|
|
||||||
val version = "0.6.4"
|
|
||||||
val installationPath = "/usr/local/bin/"
|
|
||||||
val tmpDir = "~/tmp"
|
|
||||||
val filename = "kubeconform-linux-amd64"
|
|
||||||
val packedFilename = "$filename.tar.gz"
|
|
||||||
|
|
||||||
if ( !chk("kubeconform -v") || "v$version" != cmd("kubeconform -v").out?.trim() ) {
|
|
||||||
downloadFromURL(
|
|
||||||
"https://github.com/yannh/kubeconform/releases/download/v$version/$packedFilename",
|
|
||||||
path = tmpDir,
|
|
||||||
sha256sum = "2b4ebeaa4d5ac4843cf8f7b7e66a8874252b6b71bc7cbfc4ef1cbf85acec7c07"
|
|
||||||
)
|
|
||||||
cmd("sudo tar -xzf $packedFilename -C $installationPath", tmpDir)
|
|
||||||
} else {
|
|
||||||
ProvResult(true, out = "Kubeconform $version already installed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.installKubectl() = task {
|
|
||||||
|
|
||||||
// see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
|
|
||||||
val kubectlVersion = "1.27.4"
|
|
||||||
val tmpDir = "~/tmp"
|
|
||||||
|
|
||||||
// prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
|
|
||||||
optional {
|
|
||||||
cmd("sudo apt-get update")
|
|
||||||
}
|
|
||||||
aptInstall("apt-transport-https ca-certificates curl")
|
|
||||||
createDir(tmpDir)
|
|
||||||
downloadFromURL(
|
|
||||||
"https://dl.k8s.io/release/v$kubectlVersion/bin/linux/amd64/kubectl",
|
|
||||||
path = tmpDir,
|
|
||||||
// from https://dl.k8s.io/v1.27.4/bin/linux/amd64/kubectl.sha256
|
|
||||||
sha256sum = "4685bfcf732260f72fce58379e812e091557ef1dfc1bc8084226c7891dd6028f"
|
|
||||||
)
|
|
||||||
cmd("sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl", dir = tmpDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.configureKubectlBashCompletion() = task {
|
|
||||||
cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true)
|
|
||||||
createDir(".bashrc.d")
|
|
||||||
createFileFromResource(KUBE_CONFIG_CONTEXT_SCRIPT, "kubectl.sh", RESOURCE_PATH)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.installDevopsScripts() = task {
|
|
||||||
|
|
||||||
task("install ssh helper") {
|
task("install ssh helper") {
|
||||||
createFileFromResource(
|
createFileFromResource(
|
||||||
"/usr/local/bin/sshu.sh",
|
"/usr/local/bin/sshu.sh",
|
||||||
"sshu.sh",
|
"sshu.sh",
|
||||||
RESOURCE_PATH,
|
resourcePath,
|
||||||
"555",
|
"555",
|
||||||
sudo = true
|
sudo = true
|
||||||
)
|
)
|
||||||
createFileFromResource(
|
createFileFromResource(
|
||||||
"/usr/local/bin/ssht.sh",
|
"/usr/local/bin/ssht.sh",
|
||||||
"ssht.sh",
|
"ssht.sh",
|
||||||
RESOURCE_PATH,
|
resourcePath,
|
||||||
"555",
|
"555",
|
||||||
sudo = true
|
sudo = true
|
||||||
)
|
)
|
||||||
|
@ -127,7 +87,7 @@ fun Prov.installDevopsScripts() = task {
|
||||||
createFileFromResource(
|
createFileFromResource(
|
||||||
k3sContextFile,
|
k3sContextFile,
|
||||||
"k3s-create-context.sh",
|
"k3s-create-context.sh",
|
||||||
RESOURCE_PATH,
|
resourcePath,
|
||||||
"555",
|
"555",
|
||||||
sudo = true
|
sudo = true
|
||||||
)
|
)
|
||||||
|
@ -138,14 +98,13 @@ fun Prov.installDevopsScripts() = task {
|
||||||
createFileFromResource(
|
createFileFromResource(
|
||||||
k3sConnectFile,
|
k3sConnectFile,
|
||||||
"k3s-connect.sh",
|
"k3s-connect.sh",
|
||||||
RESOURCE_PATH,
|
resourcePath,
|
||||||
"555",
|
"555",
|
||||||
sudo = true
|
sudo = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun Prov.installTerraform(): ProvResult = task {
|
||||||
fun Prov.installTerraform() = task {
|
|
||||||
val dir = "/usr/lib/tfenv/"
|
val dir = "/usr/lib/tfenv/"
|
||||||
|
|
||||||
if (!checkDir(dir)) {
|
if (!checkDir(dir)) {
|
||||||
|
@ -155,6 +114,47 @@ fun Prov.installTerraform() = 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.4.6", sudo = true)
|
cmd("tfenv install latest:^1.0.8", sudo = true)
|
||||||
cmd("tfenv use latest:^1.4.6", sudo = true)
|
cmd("tfenv use latest:^1.0.8", sudo = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------- AWS credentials file -----------------------------------------------
|
||||||
|
fun Prov.installAwsCredentials(id: String = "REPLACE_WITH_YOUR_ID", key: String = "REPLACE_WITH_YOUR_KEY"): ProvResult =
|
||||||
|
task {
|
||||||
|
val dir = "~/.aws"
|
||||||
|
|
||||||
|
if (!checkDir(dir)) {
|
||||||
|
createDirs(dir)
|
||||||
|
createFile("~/.aws/config", awsConfig())
|
||||||
|
createFile("~/.aws/credentials", awsCredentials(id, key))
|
||||||
|
} else {
|
||||||
|
ProvResult(true, "aws credential folder already installed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun awsConfig(): String {
|
||||||
|
return """
|
||||||
|
[default]
|
||||||
|
region = eu-central-1
|
||||||
|
output = json
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun awsCredentials(id: String, key: String): String {
|
||||||
|
return """
|
||||||
|
[default]
|
||||||
|
aws_access_key_id = $id
|
||||||
|
aws_secret_access_key = $key
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.installDevOpsFolder(): ProvResult = task {
|
||||||
|
|
||||||
|
val dir = "~/.devops/"
|
||||||
|
|
||||||
|
if (!checkDir(dir)) {
|
||||||
|
createDirs(dir)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,37 @@
|
||||||
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.Prov
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.addTextToFile
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
|
import java.io.File
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstallFromPpa
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs ppa firefox (i.e. non-snap), removing snap-firefox if existing.
|
* Installs non-snap firefox, removing a firefox snap-installation if existing
|
||||||
*/
|
*/
|
||||||
fun Prov.installPpaFirefox() = taskWithResult {
|
fun Prov.installFirefox() = task {
|
||||||
|
|
||||||
// inspired by: https://wiki.ubuntuusers.de/Firefox/Installation/PPA/
|
// inspired by: https://www.omgubuntu.co.uk/2022/04/how-to-install-firefox-deb-apt-ubuntu-22-04
|
||||||
|
|
||||||
val unattendeUpgradesForPpaFirefox = "/etc/apt/apt.conf.d/51unattended-upgrades-firefox"
|
if (chk("snap list | grep firefox")) {
|
||||||
|
|
||||||
val preCondition = checkFile(unattendeUpgradesForPpaFirefox)
|
|
||||||
if (preCondition) {
|
|
||||||
return@taskWithResult ProvResult(true, out = "Firefox already installed with ppa")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd("sudo apt-get -qy remove firefox", sudo = true)
|
|
||||||
optional("remove snap firefox") {
|
|
||||||
cmd("snap remove firefox", sudo = true)
|
cmd("snap remove firefox", sudo = true)
|
||||||
}
|
}
|
||||||
|
aptInstall("software-properties-common")
|
||||||
|
cmd("add-apt-repository -y ppa:mozillateam/ppa", sudo = true)
|
||||||
|
|
||||||
createFile("/etc/apt/preferences.d/mozillateam", mozillaTeamFileContent, sudo = true)
|
addTextToFile(
|
||||||
|
"\nPackage: *\n" +
|
||||||
aptInstallFromPpa("mozillateam", "ppa", "firefox")
|
"Pin: release o=LP-PPA-mozillateam\n" +
|
||||||
|
"Pin-Priority: 1001\n",
|
||||||
createFile(
|
File("/etc/apt/preferences.d/mozilla-firefox"),
|
||||||
unattendeUpgradesForPpaFirefox,
|
|
||||||
"Unattended-Upgrade::Allowed-Origins:: \"LP-PPA-mozillateam:\${distro_codename}\";\n",
|
|
||||||
sudo = true
|
sudo = true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
addTextToFile(
|
||||||
|
"""Unattended-Upgrade::Allowed-Origins:: "LP-PPA-mozillateam:${'$'}{distro_codename}";""",
|
||||||
|
File("/etc/apt/preferences.d/mozilla-firefox"),
|
||||||
|
sudo = true
|
||||||
|
)
|
||||||
|
|
||||||
|
aptInstall("firefox")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val mozillaTeamFileContent = """
|
|
||||||
Package: *
|
|
||||||
Pin: release o=LP-PPA-mozillateam
|
|
||||||
Pin-Priority: 100
|
|
||||||
|
|
||||||
Package: firefox*
|
|
||||||
Pin: release o=LP-PPA-mozillateam
|
|
||||||
Pin-Priority: 1001
|
|
||||||
|
|
||||||
Package: firefox*
|
|
||||||
Pin: release o=Ubuntu
|
|
||||||
Pin-Priority: -1
|
|
||||||
|
|
||||||
Package: thunderbird*
|
|
||||||
Pin: release o=LP-PPA-mozillateam
|
|
||||||
Pin-Priority: 1001
|
|
||||||
|
|
||||||
Package: thunderbird*
|
|
||||||
Pin: release o=Ubuntu
|
|
||||||
Pin-Priority: -1
|
|
||||||
""".trimIndent()
|
|
|
@ -2,19 +2,16 @@ 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.15.13", // NOTE: when adjusting, pls also adjust checksum below and version of gopass bridge json api
|
version: String = "1.12.7",
|
||||||
enforceVersion: Boolean = false,
|
enforceVersion: Boolean = false,
|
||||||
// from https://github.com/gopasspw/gopass/releases/tag/v1.15.13
|
sha256sum: String = "0824d5110ff1e68bff1ba10c1be63acb67cb1ad8e3bccddd6b6fc989608beca8" // checksum for sha256sum version 8.30 (e.g. ubuntu 20.04)
|
||||||
sha256sum: String = "409ed5617e64fa2c781d5e2807ba7fcd65bc383a4e110f410f90b590e51aec55"
|
|
||||||
) = taskWithResult {
|
) = taskWithResult {
|
||||||
|
|
||||||
if (isPackageInstalled("gopass") && !enforceVersion) {
|
if (isPackageInstalled("gopass") && !enforceVersion) {
|
||||||
|
@ -37,34 +34,29 @@ 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
|
||||||
return@taskWithResult ProvResult(checkGopassVersion(version))
|
addResultToEval(ProvResult(checkGopassVersion(version)))
|
||||||
} else {
|
} else {
|
||||||
return@taskWithResult ProvResult(false, err = "Gopass could not be installed. " + result.err)
|
addResultToEval(ProvResult(false, err = "Gopass could not be installed. " + result.err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun Prov.configureGopass(gopassRootFolder: String? = null, publicGpgKey: Secret? = null) = taskWithResult {
|
fun Prov.configureGopass(gopassRootFolder: String? = null) = taskWithResult() {
|
||||||
|
val configFile = ".config/gopass/config.yml"
|
||||||
|
val defaultRootFolder = userHome() + ".password-store"
|
||||||
|
val rootFolder = gopassRootFolder ?: defaultRootFolder
|
||||||
|
|
||||||
val configFile = ".config/gopass/config"
|
if (checkFile(configFile)) {
|
||||||
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
val defaultRootFolder = userHome() + ".password-store"
|
if ((gopassRootFolder != null) && (!gopassRootFolder.startsWith("/"))) {
|
||||||
val gopassRoot = gopassRootFolder ?: defaultRootFolder
|
return@taskWithResult ProvResult(false, err = "Gopass cannot be initialized with a relative path or path starting with ~")
|
||||||
|
}
|
||||||
// initialize root store
|
// use default
|
||||||
val fingerprint = publicGpgKey?.let { gpgFingerprint(it.plain()) }
|
createDir(rootFolder)
|
||||||
gopassInitStoreFolder(gopassRoot, fingerprint)
|
|
||||||
|
|
||||||
createDirs(".config/gopass")
|
createDirs(".config/gopass")
|
||||||
createFile(configFile, gopassConfig(gopassRoot))
|
createFile(configFile, gopassConfig(rootFolder))
|
||||||
|
|
||||||
// auto-completion
|
// auto-completion
|
||||||
configureBashForUser()
|
configureBashForUser()
|
||||||
|
@ -72,41 +64,31 @@ fun Prov.configureGopass(gopassRootFolder: String? = null, publicGpgKey: Secret?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun Prov.gopassMountStore(storeName: String, path: String) = taskWithResult {
|
fun Prov.gopassMountStore(storeName: String, path: String) = task {
|
||||||
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")
|
cmd("gopass mounts add $storeName $path")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun Prov.gopassInitStoreFolder(path: String, gpgFingerprint: String? = null ) = task {
|
@Suppress("unused")
|
||||||
createFile("$path/.gpg-id", gpgFingerprint ?: "_replace_this_by_a_fingerprint_of_a_public_gpg_key_")
|
fun Prov.gopassInitStore(storeName: String, indexOfRecepientKey: Int = 0) = task {
|
||||||
if (!checkDir(".git", path)) {
|
cmd("printf \"$indexOfRecepientKey\\n\" | gopass init --store=$storeName")
|
||||||
cmd("git init", path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun gopassConfig(gopassRoot: String): String {
|
internal fun gopassConfig(gopassRoot: String): String {
|
||||||
return """
|
return """
|
||||||
[core]
|
autoclip: true
|
||||||
parsing = true
|
autoimport: true
|
||||||
exportkeys = true
|
cliptimeout: 45
|
||||||
autoclip = true
|
exportkeys: true
|
||||||
showsafecontent = false
|
nocolor: false
|
||||||
nopager = false
|
nopager: false
|
||||||
cliptimeout = 45
|
notifications: true
|
||||||
notifications = true
|
parsing: true
|
||||||
autoimport = true
|
path: $gopassRoot
|
||||||
[age]
|
safecontent: false
|
||||||
usekeychain = false
|
mounts: {}
|
||||||
[mounts]
|
""".trimIndent() + "\n"
|
||||||
path = $gopassRoot
|
|
||||||
"""
|
|
||||||
.trimIndent() + "\n"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,11 +6,12 @@ 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 {
|
||||||
// Attention: when changing the version, you also need to change the number after /file/ in the download url below
|
val version = "0.9.0"
|
||||||
val filename = "gopass_bridge-0.9.0-fx.xpi"
|
val filename = "gopass_bridge-${version}-fx.xpi"
|
||||||
val downloadDir = "${userHome()}Downloads/"
|
val downloadDir = "${userHome()}Downloads/"
|
||||||
|
|
||||||
createDirs(downloadDir)
|
createDirs(downloadDir)
|
||||||
|
@ -18,14 +19,13 @@ 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.X.0-fx.xpi
|
// needs manual installation with: firefox Downloads/gopass_bridge-0.8.0-fx.xpi
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installGopassJsonApi() = taskWithResult {
|
fun Prov.installGopassBridgeJsonApi() = task {
|
||||||
// from https://github.com/gopasspw/gopass-jsonapi/releases/tag/v1.15.13
|
// see https://github.com/gopasspw/gopass-jsonapi
|
||||||
val sha256sum = "3162ab558301645024325ce2e419c1d67900e1faf95dc1774a36f1ebfc76389f"
|
val gopassJsonApiVersion = "1.11.1"
|
||||||
val gopassJsonApiVersion = "1.15.13"
|
val requiredGopassVersion = "1.14.4"
|
||||||
val requiredGopassVersion = "1.15.13"
|
|
||||||
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.installGopassJsonApi() = taskWithResult {
|
||||||
if (checkGopassVersion(requiredGopassVersion)) {
|
if (checkGopassVersion(requiredGopassVersion)) {
|
||||||
aptInstall("git gnupg2") // required dependencies
|
aptInstall("git gnupg2") // required dependencies
|
||||||
createDir(downloadDir)
|
createDir(downloadDir)
|
||||||
downloadFromURL(downloadUrl, filename, downloadDir, sha256sum = sha256sum)
|
downloadFromURL(downloadUrl, filename, downloadDir)
|
||||||
cmd("dpkg -i $downloadDir/$filename", sudo = true)
|
cmd("dpkg -i $downloadDir/$filename", sudo = true)
|
||||||
} else {
|
} else {
|
||||||
ProvResult(
|
ProvResult(
|
||||||
|
@ -46,56 +46,51 @@ fun Prov.installGopassJsonApi() = taskWithResult {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} 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")) {
|
||||||
ProvResult(true, out = "Version $gopassJsonApiVersion of gopass-jsonapi is already installed")
|
addResultToEval(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) && !fileContainsText(appArmorFile, gopassAccessPermission, true)) {
|
if (checkFile(appArmorFile)) {
|
||||||
replaceTextInFile(
|
addTextToFile(
|
||||||
appArmorFile, insertAfterText, "$insertAfterText $gopassAccessPermission\n"
|
"\nowner @{HOME}/.config/gopass/gopass_wrapper.sh Ux\n",
|
||||||
|
File(appArmorFile),
|
||||||
|
sudo = true
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
cmd("systemctl reload apparmor", sudo = true)
|
cmd("systemctl reload apparmor", sudo = true)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.configureGopassJsonApi() = taskWithResult {
|
fun Prov.configureGopassBridgeJsonApi() = task {
|
||||||
if (isPackageInstalled("gopass-jsonapi")) {
|
if (isPackageInstalled("gopass-jsonapi")) {
|
||||||
// configures gopass-jsonapi for firefox and chooses default for each:
|
// configure for firefox and choose 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/<user>/.config/gopass]"
|
// "In which path should gopass_wrapper.sh be installed? [/home/testuser/.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,
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.desktop.infrastructure
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
|
|
||||||
|
|
||||||
const val GRAAL_VM_VERSION = "21.0.2"
|
|
||||||
|
|
||||||
|
|
||||||
fun Prov.installGraalVM() = task {
|
|
||||||
val tmpDir = "~/tmp"
|
|
||||||
val filename = "graalvm-community-jdk-"
|
|
||||||
val additionalPartFilename = "_linux-x64_bin"
|
|
||||||
val packedFilename = "$filename$GRAAL_VM_VERSION$additionalPartFilename.tar.gz"
|
|
||||||
val extractedFilenameHunch = "graalvm-community-openjdk-"
|
|
||||||
val installationPath = "/usr/lib/jvm/"
|
|
||||||
|
|
||||||
if ( GRAAL_VM_VERSION != graalVMVersion() || !chk("ls -d $installationPath$extractedFilenameHunch$GRAAL_VM_VERSION*")) {
|
|
||||||
downloadFromURL(
|
|
||||||
"https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-$GRAAL_VM_VERSION/$packedFilename",
|
|
||||||
path = tmpDir,
|
|
||||||
sha256sum = "b048069aaa3a99b84f5b957b162cc181a32a4330cbc35402766363c5be76ae48"
|
|
||||||
)
|
|
||||||
createDirs(installationPath, sudo = true)
|
|
||||||
cmd("sudo tar -C $installationPath -xzf $packedFilename", tmpDir)
|
|
||||||
val graalInstPath = installationPath + (cmd("ls /usr/lib/jvm/|grep -e graalvm-community-openjdk-$GRAAL_VM_VERSION").out?.replace("\n", ""))
|
|
||||||
cmd("sudo ln -sf $graalInstPath/lib/svm/bin/native-image /usr/local/bin/native-image")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.graalVMVersion(): String {
|
|
||||||
return cmdNoEval("/usr/local/bin/native-image --version|awk 'NR==1 {print $2}'").out?.trim() ?: ""
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.desktop.infrastructure
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.userHome
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptPurge
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
|
|
||||||
|
|
||||||
fun Prov.installHugoByDeb() = task {
|
|
||||||
val sha256sum = "46692ac9b79d5bc01b0f847f6dcf651d8630476de63e598ef61a8da9461d45cd"
|
|
||||||
val requiredHugoVersion = "0.125.5"
|
|
||||||
val filename = "hugo_extended_0.125.5_linux-amd64.deb"
|
|
||||||
val downloadUrl = "-L https://github.com/gohugoio/hugo/releases/download/v$requiredHugoVersion/$filename"
|
|
||||||
val downloadDir = "${userHome()}Downloads"
|
|
||||||
val currentHugoVersion = cmdNoEval("hugo version").out ?: ""
|
|
||||||
|
|
||||||
if (needsHugoInstall(currentHugoVersion, requiredHugoVersion)) {
|
|
||||||
if (isHugoInstalled(currentHugoVersion)) {
|
|
||||||
if (currentHugoVersion.contains("snap")) {
|
|
||||||
cmd("snap remove hugo", sudo = true)
|
|
||||||
} else {
|
|
||||||
aptPurge("hugo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
aptInstall("gnupg2")
|
|
||||||
downloadFromURL(downloadUrl, filename, downloadDir, sha256sum = sha256sum)
|
|
||||||
cmd("dpkg -i $downloadDir/$filename", sudo = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun needsHugoInstall(currentHugoVersion: String?, requiredHugoVersion: String) : Boolean {
|
|
||||||
if (currentHugoVersion == null) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (!isHugoInstalled(currentHugoVersion)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (!isHugoExtended(currentHugoVersion)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if (isLowerHugoVersion(requiredHugoVersion, currentHugoVersion)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isHugoInstalled(hugoVersion: String?) : Boolean {
|
|
||||||
if (hugoVersion == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return hugoVersion.contains("hugo")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isHugoExtended(hugoVersion: String) : Boolean {
|
|
||||||
return hugoVersion.contains("extended")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isLowerHugoVersion(requiredHugoVersion: String, currentHugoVersion: String ) : Boolean {
|
|
||||||
val reqVersionNo = getHugoVersionNo(requiredHugoVersion)
|
|
||||||
val currentVersionNo = getHugoVersionNo(currentHugoVersion)
|
|
||||||
return when {
|
|
||||||
compareVersions(currentVersionNo, reqVersionNo).contains("lower") -> true
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun compareVersions(firstVersion : List<Int>, secondVersion: List<Int>) : String {
|
|
||||||
var result = ""
|
|
||||||
for (i in 0..2) {
|
|
||||||
when {
|
|
||||||
firstVersion[i] > secondVersion[i] -> result += " higher"
|
|
||||||
firstVersion[i] < secondVersion[i] -> result += " lower"
|
|
||||||
firstVersion[i] == secondVersion[i] -> result += " equal"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHugoVersionNo(hugoVersion: String) : List<Int> {
|
|
||||||
// hugo v0.126.1-3d40ab+extended linux/amd64 BuildDate=2024-05-15T10:42:34Z VendorInfo=snap:0.126.1
|
|
||||||
var result = hugoVersion.split(" ")[1]
|
|
||||||
result = result.split("-")[0].removePrefix("v")
|
|
||||||
return result.split(".").map { it.toInt() }
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
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")
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ val NETWORK_TOOLS = "curl wget net-tools"
|
||||||
|
|
||||||
val KEY_MANAGEMENT_GUI = "seahorse"
|
val KEY_MANAGEMENT_GUI = "seahorse"
|
||||||
|
|
||||||
val BROWSER = "chromium-browser" // firefox can be installed by installFirefox
|
val BROWSER = "firefox chromium-browser"
|
||||||
|
|
||||||
val EMAIL_CLIENT = "thunderbird"
|
val EMAIL_CLIENT = "thunderbird"
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ val OPENCONNECT = "openconnect network-manager-openconnect network-manager-openc
|
||||||
|
|
||||||
val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
|
val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
|
||||||
|
|
||||||
val JAVA = "openjdk-17-jdk jarwrapper"
|
val JAVA = "openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk jarwrapper"
|
||||||
|
|
||||||
val DRAWING_TOOLS = "inkscape dia openboard graphviz"
|
val DRAWING_TOOLS = "inkscape dia openboard graphviz"
|
||||||
|
|
||||||
|
@ -39,5 +39,3 @@ val CLOJURE_TOOLS = "leiningen"
|
||||||
val PASSWORD_TOOLS = "pwgen"
|
val PASSWORD_TOOLS = "pwgen"
|
||||||
|
|
||||||
val SCREEN_TOOLS = "scrcpy"
|
val SCREEN_TOOLS = "scrcpy"
|
||||||
|
|
||||||
val COMPARE_TOOLS = "meld"
|
|
||||||
|
|
|
@ -7,62 +7,35 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInsta
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
fun Prov.provisionPython(venvHome: String? = "~/.venv/meissa") = task {
|
fun Prov.provisionPython() = task {
|
||||||
installPython3()
|
installPython3()
|
||||||
if (venvHome != null) { configureVenv(venvHome) }
|
configureVenv()
|
||||||
installPybuilder(venvHome)
|
installPybuilder()
|
||||||
installRestClient(venvHome)
|
installRestClient()
|
||||||
installJupyterlab(venvHome)
|
installJupyterlab()
|
||||||
installLinters(venvHome)
|
|
||||||
installAsciinema(venvHome)
|
|
||||||
installPyTest(venvHome)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installPython3(): ProvResult = task {
|
fun Prov.installPython3(): ProvResult = task {
|
||||||
aptInstall("python3-venv python3-pip")
|
aptInstall("python3-venv python3-pip")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.configureVenv(venvHome: String): ProvResult = task {
|
fun Prov.configureVenv(): ProvResult = task {
|
||||||
cmd("python3 -m venv $venvHome")
|
val venvHome = "~/.venv/meissa"
|
||||||
createSymlink(File("$venvHome/bin/activate"), File("~/.bashrc.d/venv.sh"))
|
cmd("python3 -m venv " + venvHome)
|
||||||
pipInstall("pip --upgrade", venvHome)
|
cmd("source " + venvHome + "/bin/activate")
|
||||||
|
createSymlink(File(venvHome + "/bin/activate"), File("~/.bashrc.d/venv.sh"))
|
||||||
|
cmd("pip3 install pip --upgrade")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installPybuilder(venvHome: String? = null): ProvResult = task {
|
fun Prov.installPybuilder(): ProvResult = task {
|
||||||
pipInstall("pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation" +
|
cmd("pip3 install pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation python_terraform " +
|
||||||
" python_terraform dda_python_terraform boto3 pyyaml packaging inflection",
|
"boto3")
|
||||||
venvHome
|
|
||||||
)
|
|
||||||
pipInstall("--upgrade ddadevops", venvHome)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installRestClient(venvHome: String? = null): ProvResult = task {
|
fun Prov.installRestClient(): ProvResult = task {
|
||||||
pipInstall("requests", venvHome)
|
cmd("pip3 install requests")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.installJupyterlab(venvHome: String? = null): ProvResult = task {
|
fun Prov.installJupyterlab(): ProvResult = task {
|
||||||
pipInstall("jupyterlab pandas matplotlib", venvHome)
|
cmd("pip3 install jupyterlab pandas matplotlib")
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.installLinters(venvHome: String? = null): ProvResult = task {
|
|
||||||
pipInstall("flake8 mypy pylint", venvHome)
|
|
||||||
}
|
|
||||||
fun Prov.installAsciinema(venvHome: String? = null): ProvResult = task {
|
|
||||||
pipInstall("asciinema", venvHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Prov.installPyTest(venvHome: String? = null): ProvResult = task {
|
|
||||||
pipInstall("pytest", venvHome)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Prov.pipInstall(pkg: String, venvHome: String? = null) {
|
|
||||||
cmd(activateVenvCommandPrefix(venvHome) + "pip3 install $pkg")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun activateVenvCommandPrefix(venvHome: String?): String {
|
|
||||||
return if (venvHome == null) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
"source $venvHome/bin/activate && "
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,33 +6,36 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInsta
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
|
||||||
|
|
||||||
|
|
||||||
fun Prov.installVSCode(vararg options: String) = task {
|
fun Prov.installVSC(vararg options: String) = task {
|
||||||
val clojureExtensions = setOf("betterthantomorrow.calva", "DavidAnson.vscode-markdownlint")
|
val clojureExtensions =
|
||||||
val pythonExtensions = setOf("ms-python.python")
|
arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint")
|
||||||
|
val pythonExtensions = arrayListOf("ms-python.python")
|
||||||
|
|
||||||
installVSCodePrerequisites()
|
prerequisitesVSCinstall()
|
||||||
|
|
||||||
installVSCPackage()
|
installVSCPackage()
|
||||||
installVSCodiumPackage()
|
installVSCodiumPackage()
|
||||||
|
|
||||||
if (options.contains("clojure")) {
|
if (options.contains("clojure")) {
|
||||||
installVSCodeExtensions(clojureExtensions)
|
installExtensionsCode(clojureExtensions)
|
||||||
installVSCodiumExtensions(clojureExtensions)
|
installExtensionsCodium(clojureExtensions)
|
||||||
}
|
}
|
||||||
if (options.contains("python")) {
|
if (options.contains("python")) {
|
||||||
installVSCodeExtensions(pythonExtensions)
|
installExtensionsCode(pythonExtensions)
|
||||||
installVSCodiumExtensions(pythonExtensions)
|
installExtensionsCodium(pythonExtensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provisionAdditionalToolsForVSCode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun Prov.installVSCodePrerequisites() = task {
|
private fun Prov.prerequisitesVSCinstall() = task {
|
||||||
aptInstall("curl gpg unzip apt-transport-https")
|
aptInstall("curl gpg unzip apt-transport-https")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Suppress("unused") // only required for installation of vscode via apt
|
@Suppress("unused") // only required for installation of vscode via apt
|
||||||
private fun Prov.installVSCodeWithApt() = task {
|
private fun Prov.installVscWithApt() = task {
|
||||||
val packageName = "code"
|
val packageName = "code"
|
||||||
if (!isPackageInstalled(packageName)) {
|
if (!isPackageInstalled(packageName)) {
|
||||||
// see https://code.visualstudio.com/docs/setup/linux
|
// see https://code.visualstudio.com/docs/setup/linux
|
||||||
|
@ -62,7 +65,7 @@ private fun Prov.installVSCodiumPackage() = task {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun Prov.installVSCodeExtensions(extensions: Set<String>) = optional {
|
private fun Prov.installExtensionsCode(extensions: List<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")
|
||||||
|
@ -71,11 +74,20 @@ private fun Prov.installVSCodeExtensions(extensions: Set<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.installVSCodiumExtensions(extensions: Set<String>) = optional {
|
private fun Prov.installExtensionsCodium(extensions: List<String>) = optional {
|
||||||
var res = ProvResult(true)
|
var res = ProvResult(true)
|
||||||
for (ext in extensions) {
|
for (ext in extensions) {
|
||||||
res = ProvResult(res.success && cmd("codium --install-extension $ext").success)
|
res = cmd("codium --install-extension $ext")
|
||||||
}
|
}
|
||||||
res
|
res
|
||||||
// Settings can be found at $HOME/.config/VSCodium/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/")
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,65 +69,46 @@ open class Prov protected constructor(
|
||||||
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.
|
* 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 session(taskLambda: Prov.() -> ProvResult): ProvResult {
|
|
||||||
if (level > 0) {
|
|
||||||
throw IllegalStateException("A session can only be created on the top-level and may not be included in another session or task.")
|
|
||||||
}
|
|
||||||
return evaluate(ResultMode.ALL, "session") { taskLambda() }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A task is the fundamental execution unit. In the results overview it is represented by one line with a success or failure result.
|
|
||||||
* Returns success if all sub-tasks finished with success or if no sub-tasks are called at all.
|
|
||||||
*/
|
|
||||||
fun task(name: String? = null, taskLambda: Prov.() -> Unit): ProvResult {
|
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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same as task above but the lambda parameter must have a ProvResult as return type.
|
* Same as task but the provided lambda is explicitly required to provide a ProvResult to be returned.
|
||||||
* The returned ProvResult is included in the success resp. failure evaluation,
|
* The returned result is included in the evaluation.
|
||||||
* i.e. if the returned ProvResult from the lambda fails, the returned ProvResult from
|
|
||||||
* taskWithResult also fails, else success depends on potentially called sub-tasks.
|
|
||||||
*/
|
*/
|
||||||
fun taskWithResult(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
|
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 from the lambda, 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(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
|
fun requireLast(a: Prov.() -> ProvResult): ProvResult {
|
||||||
printDeprecationWarningIfLevel0("requireLast")
|
return evaluate(ResultMode.LAST) { a() }
|
||||||
return evaluate(ResultMode.LAST, name) { taskLambda() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a task, which always returns success.
|
* defines a task, which always returns success
|
||||||
*/
|
*/
|
||||||
fun optional(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
|
fun optional(a: Prov.() -> ProvResult): ProvResult {
|
||||||
printDeprecationWarningIfLevel0("optional")
|
return evaluate(ResultMode.OPTIONAL) { a() }
|
||||||
return evaluate(ResultMode.OPTIONAL, name) { taskLambda() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a task, which exits the overall execution on failure result of the taskLambda.
|
* defines a task, which exits the overall execution on failure
|
||||||
*/
|
*/
|
||||||
fun exitOnFailure(taskLambda: Prov.() -> ProvResult): ProvResult {
|
fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult {
|
||||||
printDeprecationWarningIfLevel0("exitOnFailure")
|
return evaluate(ResultMode.FAILEXIT) { a() }
|
||||||
return evaluate(ResultMode.FAILEXIT) { taskLambda() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
|
@ -273,8 +254,6 @@ open class Prov protected constructor(
|
||||||
previousLevel = -1
|
previousLevel = -1
|
||||||
exit = false
|
exit = false
|
||||||
initProgress()
|
initProgress()
|
||||||
|
|
||||||
processor.open()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-handling
|
// pre-handling
|
||||||
|
@ -333,15 +312,6 @@ 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()
|
||||||
|
@ -352,12 +322,8 @@ 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].level >= internalResults[resultIndex + 1].level)
|
return !(resultIndex < internalResults.size - 1 && internalResults[resultIndex + 1].level > internalResults[resultIndex].level)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -466,11 +432,6 @@ 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.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ data class ProvResult(val success: Boolean,
|
||||||
val exception: Exception? = null,
|
val exception: Exception? = null,
|
||||||
val exit: String? = null) {
|
val exit: String? = null) {
|
||||||
|
|
||||||
val outTrimmed: String? = out?.trim()
|
|
||||||
|
|
||||||
constructor(returnCode : Int) : this(returnCode == 0)
|
constructor(returnCode : Int) : this(returnCode == 0)
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|
|
@ -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", "optional\$default", "requireAll", "requireLast", "requireLast\$default", "inContainer")
|
val suffixes = arrayOf("optional", "requireAll", "requireLast", "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.split("$")[0]
|
suffix = method
|
||||||
}
|
}
|
||||||
inc++
|
inc++
|
||||||
method = callingFrame[i + offsetVal + inc].methodName
|
method = callingFrame[i + offsetVal + inc].methodName
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.domaindrivenarchitecture.provs.framework.core
|
||||||
|
|
||||||
import com.charleskorn.kaml.Yaml
|
import com.charleskorn.kaml.Yaml
|
||||||
import com.charleskorn.kaml.YamlConfiguration
|
import com.charleskorn.kaml.YamlConfiguration
|
||||||
|
import kotlinx.serialization.InternalSerializationApi
|
||||||
import kotlinx.serialization.serializer
|
import kotlinx.serialization.serializer
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -17,13 +18,15 @@ fun writeToFile(fileName: String, text: String) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(InternalSerializationApi::class)
|
||||||
inline fun <reified T : Any> String.yamlToType() = Yaml(configuration = YamlConfiguration(strictMode = false)).decodeFromString(
|
inline fun <reified T : Any> String.yamlToType() = Yaml(configuration = YamlConfiguration(strictMode = false)).decodeFromString(
|
||||||
serializer<T>(),
|
T::class.serializer(),
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(InternalSerializationApi::class)
|
||||||
inline fun <reified T : Any> T.toYaml() = Yaml(configuration = YamlConfiguration(strictMode = false, encodeDefaults = false)).encodeToString(
|
inline fun <reified T : Any> T.toYaml() = Yaml(configuration = YamlConfiguration(strictMode = false, encodeDefaults = false)).encodeToString(
|
||||||
serializer<T>(),
|
T::class.serializer(),
|
||||||
this
|
this
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,63 +6,88 @@ 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.
|
||||||
* Returns a local Prov instance if targetCommand.isValidLocalhost() is true resp.
|
* E.g. it returns a local Prov instance if targetCommand.isValidLocalhost() is true or
|
||||||
* 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(targetCommand: TargetCliCommand): Prov {
|
fun createProvInstance(
|
||||||
|
targetCommand: TargetCliCommand,
|
||||||
|
remoteHostSetSudoWithoutPasswordRequired: Boolean = false
|
||||||
|
): Prov {
|
||||||
if (targetCommand.isValid()) {
|
if (targetCommand.isValid()) {
|
||||||
val password: Secret? = targetCommand.remoteTarget()?.password
|
val password: Secret? = targetCommand.remoteTarget()?.password
|
||||||
|
|
||||||
return if (targetCommand.isValidLocalhost()) {
|
val remoteTarget = targetCommand.remoteTarget()
|
||||||
local()
|
if (targetCommand.isValidLocalhost()) {
|
||||||
} else if (targetCommand.isValidRemote()) {
|
return local()
|
||||||
createRemoteProvInstance(targetCommand.remoteTarget(), password)
|
} else if (targetCommand.isValidRemote() && remoteTarget != null) {
|
||||||
} else {
|
return createProvInstanceRemote(
|
||||||
throw IllegalArgumentException(
|
remoteTarget.host,
|
||||||
"Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help."
|
remoteTarget.user,
|
||||||
|
remoteTarget.password == null,
|
||||||
|
password,
|
||||||
|
remoteHostSetSudoWithoutPasswordRequired
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
println("ERROR: Invalid target (${targetCommand.target}). Please use option -h for help.")
|
println("Invalid command line options.\nPlease use option -h for help.")
|
||||||
System.out.flush()
|
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createProvInstanceRemote(
|
||||||
/**
|
host: String,
|
||||||
* Wrapper for exitProcess, which allows e.g. mocking for test purposes
|
remoteUser: String,
|
||||||
*/
|
sshWithKey: Boolean,
|
||||||
fun quit(status: Int): Nothing {
|
password: Secret?,
|
||||||
exitProcess(status)
|
remoteHostSetSudoWithoutPasswordRequired: Boolean
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun printProvsVersion() {
|
|
||||||
val version = object {}.javaClass.getResource("/version.txt")?.readText()?.trim()
|
|
||||||
println("\nProvs version: $version\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
internal fun createRemoteProvInstance(
|
|
||||||
target: TargetCliCommand.RemoteTarget?,
|
|
||||||
password: Secret? = null
|
|
||||||
): Prov {
|
): Prov {
|
||||||
return if (target != null) {
|
val prov =
|
||||||
remote(target.host, target.user, target.password ?: password)
|
if (sshWithKey) {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun getPasswordToConfigureSudoWithoutPassword(): Secret {
|
// todo: consider removal as password can be retrieved by PromptSecretSource
|
||||||
return PromptSecretSource("password to configure sudo without password.").secret()
|
internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? {
|
||||||
|
var password: Secret? = null
|
||||||
|
if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) {
|
||||||
|
password =
|
||||||
|
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret()
|
||||||
|
|
||||||
|
}
|
||||||
|
return password
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.Doc
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
|
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.platforms.UbuntuProv
|
import org.domaindrivenarchitecture.provs.framework.core.platforms.UbuntuProv
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
|
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
|
||||||
|
|
||||||
private const val DOCKER_NOT_SUPPORTED = "docker not yet supported for "
|
private const val DOCKER_NOT_SUPPORTED = "docker not yet supported for "
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ fun Prov.dockerProvideImage(image: DockerImage, skipIfExisting: Boolean = true,
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.dockerProvideImagePlatform(image, skipIfExisting, sudo)
|
return this.dockerProvideImagePlatform(image, skipIfExisting, sudo)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ fun Prov.dockerImageExists(imageName: String, sudo: Boolean = true) : Boolean {
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.dockerImageExistsPlatform(imageName, sudo)
|
return this.dockerImageExistsPlatform(imageName, sudo)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ fun Prov.provideContainer(
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.provideContainerPlatform(containerName, imageName, startMode, sudo, options, command)
|
return this.provideContainerPlatform(containerName, imageName, startMode, sudo, options, command)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ fun Prov.containerRuns(containerName: String, sudo: Boolean = true) : Boolean {
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.containerRunsPlatform(containerName, sudo)
|
return this.containerRunsPlatform(containerName, sudo)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ fun Prov.runContainer(
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.runContainerPlatform(containerName, imageName, sudo)
|
return this.runContainerPlatform(containerName, imageName, sudo)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,17 +84,16 @@ fun Prov.exitAndRmContainer(
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.exitAndRmContainerPlatform(containerName, sudo)
|
return this.exitAndRmContainerPlatform(containerName, sudo)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
fun Prov.containerExec(containerName: String, cmd: String, sudo: Boolean = true): ProvResult {
|
fun Prov.containerExec(containerName: String, cmd: String, sudo: Boolean = true): ProvResult {
|
||||||
if (this is UbuntuProv) {
|
if (this is UbuntuProv) {
|
||||||
return this.containerExecPlatform(containerName, cmd, sudo)
|
return this.containerExecPlatform(containerName, cmd, sudo)
|
||||||
} else {
|
} else {
|
||||||
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.javaClass)
|
throw RuntimeException(DOCKER_NOT_SUPPORTED + (this as UbuntuProv).javaClass)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,12 +17,12 @@ class UbuntuPlusUser(private val userName: String = "testuser") : DockerImage {
|
||||||
|
|
||||||
override fun imageText(): String {
|
override fun imageText(): String {
|
||||||
return """
|
return """
|
||||||
FROM ubuntu:22.04
|
FROM ubuntu:20.04
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
RUN apt-get update && apt-get -y install sudo
|
RUN apt-get update && apt-get -y install sudo
|
||||||
RUN useradd -m $userName && echo "$userName:$userName" | chpasswd && usermod -aG sudo $userName
|
RUN useradd -m $userName && echo "$userName:$userName" | chpasswd && adduser $userName sudo
|
||||||
RUN echo "$userName ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/$userName
|
RUN echo "$userName ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/$userName
|
||||||
|
|
||||||
USER $userName
|
USER $userName
|
||||||
|
|
|
@ -7,12 +7,19 @@ import org.domaindrivenarchitecture.provs.framework.core.processors.Processor
|
||||||
const val SHELL = "/bin/bash"
|
const val SHELL = "/bin/bash"
|
||||||
|
|
||||||
|
|
||||||
open class UbuntuProv(
|
class UbuntuProv internal constructor(
|
||||||
processor: Processor = LocalProcessor(),
|
processor: Processor = LocalProcessor(),
|
||||||
name: String? = null,
|
name: String? = null,
|
||||||
progressType: ProgressType = ProgressType.BASIC
|
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))
|
||||||
}
|
}
|
||||||
|
@ -30,17 +37,15 @@ open class UbuntuProv(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildCommand(vararg args: String): String {
|
private fun buildCommand(vararg args: String): String {
|
||||||
return if (args.size == 1) {
|
return if (args.size == 1)
|
||||||
args[0].escapeAndEncloseByDoubleQuoteForShell()
|
args[0].escapeAndEncloseByDoubleQuoteForShell()
|
||||||
} else {
|
else
|
||||||
if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) {
|
if (args.size == 3 && SHELL.equals(args[0]) && "-c".equals(args[1]))
|
||||||
SHELL + " -c " + args[2].escapeAndEncloseByDoubleQuoteForShell()
|
SHELL + " -c " + args[2].escapeAndEncloseByDoubleQuoteForShell()
|
||||||
} else {
|
else
|
||||||
args.joinToString(separator = " ")
|
args.joinToString(separator = " ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun commandWithDirAndSudo(cmd: String, dir: String?, sudo: Boolean): String {
|
private fun commandWithDirAndSudo(cmd: String, dir: String?, sudo: Boolean): String {
|
||||||
val cmdWithDir = if (dir == null) cmd else "cd $dir && $cmd"
|
val cmdWithDir = if (dir == null) cmd else "cd $dir && $cmd"
|
||||||
|
|
|
@ -5,6 +5,7 @@ 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,
|
||||||
|
@ -19,24 +20,26 @@ enum class ContainerEndMode {
|
||||||
|
|
||||||
open class ContainerUbuntuHostProcessor(
|
open class ContainerUbuntuHostProcessor(
|
||||||
private val containerName: String = "default_provs_container",
|
private val containerName: String = "default_provs_container",
|
||||||
dockerImage: String = "ubuntu",
|
@Api // suppress false positive warning
|
||||||
startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
|
private val dockerImage: String = "ubuntu",
|
||||||
|
@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,
|
||||||
sudo: Boolean = true,
|
@Api // suppress false positive warning
|
||||||
options: String = ""
|
private val sudo: Boolean = true
|
||||||
) : 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 result = a.provideContainer(containerName, dockerImage, startMode, sudo, options)
|
val r = a.provideContainer(containerName, dockerImage, startMode, sudo)
|
||||||
if (!result.success)
|
if (!r.success)
|
||||||
throw RuntimeException("Could not start docker image: " + result.toString(), result.exception)
|
throw RuntimeException("Could not start docker image: " + r.toString(), r.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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,13 @@ import org.slf4j.LoggerFactory
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.nio.file.Paths
|
|
||||||
|
|
||||||
|
|
||||||
private fun getOsName(): String {
|
private fun getOsName(): String {
|
||||||
return System.getProperty("os.name")
|
return System.getProperty("os.name")
|
||||||
}
|
}
|
||||||
|
|
||||||
open class LocalProcessor(val useHomeDirAsWorkingDir: Boolean = true) : Processor {
|
open class LocalProcessor : Processor {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@Suppress("JAVA_CLASS_ON_COMPANION")
|
@Suppress("JAVA_CLASS_ON_COMPANION")
|
||||||
|
@ -27,12 +26,7 @@ open class LocalProcessor(val useHomeDirAsWorkingDir: Boolean = true) : Processo
|
||||||
|
|
||||||
private fun workingDir() : String
|
private fun workingDir() : String
|
||||||
{
|
{
|
||||||
return if (useHomeDirAsWorkingDir) {
|
return System.getProperty("user.home") ?: File.separator
|
||||||
System.getProperty("user.home") ?: File.separator
|
|
||||||
} else {
|
|
||||||
// folder in which program was started
|
|
||||||
Paths.get("").toAbsolutePath().toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exec(vararg args: String): ProcessResult {
|
override fun exec(vararg args: String): ProcessResult {
|
||||||
|
|
|
@ -2,13 +2,10 @@ 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; otherwise, overwrite this method in the implementing class
|
// no action needed for most processors; if action is needed when closing, this method must be overwritten in the subclass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,28 +21,23 @@ 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(val host: InetAddress, val user: String, val password: Secret? = null) : Processor {
|
class RemoteProcessor(host: InetAddress, user: String, 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 var ssh = SSHClient()
|
private val ssh = SSHClient()
|
||||||
|
|
||||||
override fun open() {
|
init {
|
||||||
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) {
|
||||||
|
@ -55,9 +50,8 @@ class RemoteProcessor(val host: InetAddress, val user: String, val password: Sec
|
||||||
try {
|
try {
|
||||||
ssh.disconnect()
|
ssh.disconnect()
|
||||||
} finally {
|
} finally {
|
||||||
val errorMag = "Error when initializing ssh (Host, username, password or ssh-key might be wrong) "
|
log.error("Got exception when initializing ssh (Username, password or ssh-key might be wrong): " + e.message)
|
||||||
log.error(errorMag + e.message)
|
throw RuntimeException("Error when initializing ssh (Username, password or ssh-key might be wrong) ", e)
|
||||||
throw RuntimeException(errorMag, e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -93,9 +87,9 @@ class RemoteProcessor(val host: InetAddress, val user: String, val password: Sec
|
||||||
var session: Session? = null
|
var session: Session? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session = ssh.startSession() ?: throw IllegalStateException("ERROR: Could not start ssh session.")
|
session = ssh.startSession()
|
||||||
|
|
||||||
val cmd: Command = session.exec(cmdString)
|
val cmd: Command = session!!.exec(cmdString)
|
||||||
val out = BufferedReader(InputStreamReader(cmd.inputStream)).use { it.readText() }
|
val out = BufferedReader(InputStreamReader(cmd.inputStream)).use { it.readText() }
|
||||||
val err = BufferedReader(InputStreamReader(cmd.errorStream)).use { it.readText() }
|
val err = BufferedReader(InputStreamReader(cmd.errorStream)).use { it.readText() }
|
||||||
cmd.join(100, TimeUnit.SECONDS)
|
cmd.join(100, TimeUnit.SECONDS)
|
||||||
|
|
|
@ -201,7 +201,7 @@ fun Prov.fileContentLargeFile(file: String, sudo: Boolean = false, chunkSize: In
|
||||||
// check first chunk
|
// check first chunk
|
||||||
if (resultString == null) {
|
if (resultString == null) {
|
||||||
if (!chunkResult.success) {
|
if (!chunkResult.success) {
|
||||||
return null
|
return resultString
|
||||||
} else {
|
} else {
|
||||||
resultString = ""
|
resultString = ""
|
||||||
}
|
}
|
||||||
|
@ -251,7 +251,7 @@ fun Prov.replaceTextInFile(file: String, oldText: String, replacement: String) =
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = taskWithResult {
|
fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = task {
|
||||||
// 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) {
|
||||||
|
@ -329,16 +329,12 @@ fun Prov.deleteDir(dir: String, path: String, sudo: Boolean = false): ProvResult
|
||||||
if ("" == path) {
|
if ("" == path) {
|
||||||
throw RuntimeException("In deleteDir: path must not be empty.")
|
throw RuntimeException("In deleteDir: path must not be empty.")
|
||||||
}
|
}
|
||||||
return if (checkDir(dir, path, sudo)) {
|
|
||||||
val cmd = "cd $path && rmdir $dir"
|
val cmd = "cd $path && rmdir $dir"
|
||||||
if (!sudo) {
|
return if (!sudo) {
|
||||||
cmd(cmd)
|
cmd(cmd)
|
||||||
} else {
|
} else {
|
||||||
cmd(cmd.sudoizeCommand())
|
cmd(cmd.sudoizeCommand())
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ProvResult(true, out = "Dir to delete did not exist: $dir")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -407,7 +403,7 @@ fun Prov.fileSize(filename: String, sudo: Boolean = false): Int? {
|
||||||
|
|
||||||
|
|
||||||
private fun ensureValidPosixFilePermission(posixFilePermission: String) {
|
private fun ensureValidPosixFilePermission(posixFilePermission: String) {
|
||||||
if (!Regex("^0?[0-7]{3}$").matches(posixFilePermission)) throw IllegalArgumentException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664")
|
if (!Regex("^[0-7]{3}$").matches(posixFilePermission)) throw IllegalArgumentException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -3,6 +3,9 @@ package org.domaindrivenarchitecture.provs.framework.ubuntu.git.base
|
||||||
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.ubuntu.filesystem.base.*
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.KNOWN_HOSTS_FILE
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.trustHost
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,14 +33,34 @@ fun Prov.gitClone(
|
||||||
ProvResult(true, out = "Repo [$pathWithBasename] already exists, but might not be up-to-date.")
|
ProvResult(true, out = "Repo [$pathWithBasename] already exists, but might not be up-to-date.")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// create targetPath if not yet existing
|
// create targetPath (if not yet existing)
|
||||||
if (!checkDir(targetPath)) {
|
if (!checkDir(targetPath)) {
|
||||||
createDirs(targetPath)
|
createDirs(targetPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that all output of git clone on Linux is shown in stderr (normal progress info AND errors),
|
|
||||||
// which might be confusing in the logfile.
|
|
||||||
cmd("cd $targetPath && git clone $repoSource ${targetFolderName ?: ""}")
|
cmd("cd $targetPath && git clone $repoSource ${targetFolderName ?: ""}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.trustGithub() = task {
|
||||||
|
// current fingerprints from https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints
|
||||||
|
val fingerprints = setOf(
|
||||||
|
"SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com", // (RSA)
|
||||||
|
// supported beginning September 14, 2021:
|
||||||
|
"SHA256:p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM github.com", // (ECDSA)
|
||||||
|
"SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com" // (Ed25519)
|
||||||
|
)
|
||||||
|
trustHost("github.com", fingerprints)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.trustGitlab() = task {
|
||||||
|
// entries for known_hosts from https://docs.gitlab.com/ee/user/gitlab_com/
|
||||||
|
val gitlabFingerprints = """
|
||||||
|
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
|
||||||
|
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
|
||||||
|
gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
|
||||||
|
""".trimIndent()
|
||||||
|
addTextToFile("\n" + gitlabFingerprints + "\n", File(KNOWN_HOSTS_FILE))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,10 +20,7 @@ fun Prov.aptInstall(packages: String, ignoreAlreadyInstalled: Boolean = true): P
|
||||||
if (!allInstalled) {
|
if (!allInstalled) {
|
||||||
if (!isPackageInstalled(packages)) {
|
if (!isPackageInstalled(packages)) {
|
||||||
if (!aptInit) {
|
if (!aptInit) {
|
||||||
optional {
|
|
||||||
// may fail for some packages, but this should in general not be an issue
|
|
||||||
cmd("sudo apt-get update")
|
cmd("sudo apt-get update")
|
||||||
}
|
|
||||||
cmd("sudo apt-get install -qy apt-utils")
|
cmd("sudo apt-get install -qy apt-utils")
|
||||||
aptInit = true
|
aptInit = true
|
||||||
}
|
}
|
||||||
|
@ -76,15 +73,7 @@ fun Prov.isPackageInstalled(packageName: String): Boolean {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if a package is installed else false
|
* Removes a package including its configuration and data files
|
||||||
*/
|
|
||||||
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 {
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
package org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base
|
package org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.desktop.domain.KnownHost
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.local
|
import org.domaindrivenarchitecture.provs.framework.core.echoCommandForText
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDir
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createSecretFile
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.SshKeyPair
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.SshKeyPair
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
|
|
||||||
|
const val KNOWN_HOSTS_FILE = "~/.ssh/known_hosts"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs ssh keys for active user; ssh filenames depend on the ssh key type, e.g. for public key file: "id_rsa.pub", "id_id_ed25519.pub", etc
|
* Installs ssh keys for active user; ssh filenames depend on the ssh key type, e.g. for public key file: "id_rsa.pub", "id_id_ed25519.pub", etc
|
||||||
*/
|
*/
|
||||||
|
@ -20,66 +23,72 @@ fun Prov.configureSshKeys(sshKeys: SshKeyPair) = task {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the specified host (domain name or IP) and (optional) port is contained in the known_hosts file
|
* Checks if the specified hostname or Ip is in a known_hosts file
|
||||||
|
*
|
||||||
|
* @return whether if was found
|
||||||
*/
|
*/
|
||||||
fun Prov.isKnownHost(hostOrIp: String, port: Int? = null): Boolean {
|
fun Prov.isHostKnown(hostOrIp: String) : Boolean {
|
||||||
val hostWithPotentialPort = port?.let { formatHostForKnownHostsFile(hostOrIp, port) } ?: hostOrIp
|
return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false
|
||||||
return cmdNoEval("ssh-keygen -F $hostWithPotentialPort").out?.isNotEmpty() ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun formatHostForKnownHostsFile(hostOrIp: String, port: Int? = null): String {
|
|
||||||
return port?.let { "[$hostOrIp]:$port" } ?: hostOrIp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds ssh keys for specified host (which also can be an ip-address) to the ssh-file "known_hosts".
|
* Adds ssh keys for specified host (which also can be an ip-address) to ssh-file "known_hosts"
|
||||||
* If parameter verifyKeys is true, the keys are checked against the live keys of the host and added only if valid.
|
* Either add the specified rsaFingerprints or - if null - add automatically retrieved keys.
|
||||||
|
* Note: adding keys automatically is vulnerable to a man-in-the-middle attack, thus considered insecure and not recommended.
|
||||||
*/
|
*/
|
||||||
fun Prov.addKnownHost(knownHost: KnownHost, verifyKeys: Boolean = false) = task {
|
fun Prov.trustHost(host: String, fingerprintsOfKeysToBeAdded: Set<String>?) = taskWithResult {
|
||||||
val knownHostsFile = "~/.ssh/known_hosts"
|
if (isHostKnown(host)) {
|
||||||
|
return@taskWithResult ProvResult(true, out = "Host already known")
|
||||||
if (!checkFile(knownHostsFile)) {
|
}
|
||||||
|
if (!checkFile(KNOWN_HOSTS_FILE)) {
|
||||||
createDir(".ssh")
|
createDir(".ssh")
|
||||||
createFile(knownHostsFile, null)
|
createFile(KNOWN_HOSTS_FILE, null)
|
||||||
}
|
}
|
||||||
with(knownHost) {
|
if (fingerprintsOfKeysToBeAdded == null) {
|
||||||
for (key in hostKeys) {
|
// auto add keys
|
||||||
if (!verifyKeys) {
|
cmd("ssh-keyscan $host >> $KNOWN_HOSTS_FILE")
|
||||||
addTextToFile("\n${formatHostForKnownHostsFile(hostName, port)} $key\n", File(knownHostsFile))
|
|
||||||
} else {
|
} else {
|
||||||
val validKeys = findSshKeys(hostName, port)
|
// logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt
|
||||||
if (validKeys?.contains(key) == true) {
|
val actualKeys = findSshKeys(host)
|
||||||
val formattedHost = formatHostForKnownHostsFile(hostName, port)
|
if (actualKeys == null || actualKeys.size == 0) {
|
||||||
addTextToFile("\n$formattedHost $key\n", File(knownHostsFile))
|
return@taskWithResult ProvResult(false, out = "No valid keys found for host: $host")
|
||||||
} else {
|
}
|
||||||
addResultToEval(
|
val actualFingerprints = getFingerprintsForKeys(actualKeys)
|
||||||
ProvResult(
|
for (fingerprintToBeAdded in fingerprintsOfKeysToBeAdded) {
|
||||||
|
var indexOfKeyFound = -1
|
||||||
|
|
||||||
|
// search for fingerprint in actual fingerprints
|
||||||
|
for ((i, actualFingerprint) in actualFingerprints.withIndex()) {
|
||||||
|
if (actualFingerprint.contains(fingerprintToBeAdded)) {
|
||||||
|
indexOfKeyFound = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (indexOfKeyFound == -1) {
|
||||||
|
return@taskWithResult ProvResult(
|
||||||
false,
|
false,
|
||||||
err = "The following key of host [$hostName] could not be verified successfully: " + key
|
err = "Fingerprint ($fingerprintToBeAdded) could not be found in actual fingerprints: $actualFingerprints"
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
cmd(echoCommandForText(actualKeys.get(indexOfKeyFound) + "\n") + " >> $KNOWN_HOSTS_FILE")
|
||||||
}
|
}
|
||||||
}
|
ProvResult(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a list of valid ssh keys for the given host (host can also be an ip address),
|
* Returns a list of valid ssh keys for the given host (host can also be an ip address)
|
||||||
* keys are returned (space-separated) as keytype and key, but WITHOUT the host name.*
|
|
||||||
* If no port is specified, the keys for the default port (22) are returned.
|
|
||||||
* If no keytype is specified, keys are returned for all keytypes.
|
|
||||||
*/
|
*/
|
||||||
fun Prov.findSshKeys(host: String, port: Int? = null, keytype: String? = null): List<String>? {
|
private fun Prov.findSshKeys(host: String): List<String>? {
|
||||||
val portOption = port?.let { " -p $port " } ?: ""
|
return cmd("ssh-keyscan $host 2>/dev/null").out?.split("\n")?.filter { x -> "" != x }
|
||||||
val keytypeOption = keytype?.let { " -t $keytype " } ?: ""
|
|
||||||
val output = cmd("ssh-keyscan $portOption $keytypeOption $host 2>/dev/null").out?.trim()
|
|
||||||
return output?.split("\n")?.filter { x -> "" != x }?.map { x -> x.substringAfter(" ") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun main() {
|
|
||||||
val k = local().findSshKeys("repo.prod.meissa.de", 2222)
|
/**
|
||||||
println(k)
|
* Returns a list of fingerprints of the given sshKeys; the returning list has same size and order as the specified list of sshKeys
|
||||||
|
*/
|
||||||
|
private fun Prov.getFingerprintsForKeys(sshKeys: List<String>): List<String> {
|
||||||
|
return sshKeys.map { x -> cmd("echo \"$x\" | ssh-keygen -lf -").out ?: "" }
|
||||||
}
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.domain
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.infrastructure.createCronJob
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a schedule for a monthly reboot of the (Linux) system.
|
|
||||||
* ATTENTION: Use with care!! System will be shut down, restart might not succeed in all cases.
|
|
||||||
*/
|
|
||||||
fun Prov.scheduleMonthlyReboot() = task {
|
|
||||||
// reboot each first Tuesday a month - see https://blog.healthchecks.io/2022/09/schedule-cron-job-the-funky-way/
|
|
||||||
val schedule = "0 2 */100,1-7 * TUE"
|
|
||||||
createCronJob("50_monthly_reboot", schedule, "/sbin/shutdown", "-r now", "root")
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.infrastructure
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a cron job.
|
|
||||||
* @param cronFilename e.g. "90_my_cron"; file is created in folder /etc/cron.d/
|
|
||||||
* @param schedule in the usual cron-format, examples: "0 * * * *" for each hour, "0 3 1-7 * 1" for the first Monday each month at 3:00, etc
|
|
||||||
* @param command the executed command
|
|
||||||
* @param user the user with whom the command will be executed, if null the current user is used
|
|
||||||
*/
|
|
||||||
fun Prov.createCronJob(cronFilename: String, schedule: String, command: String, parameter: String, user: String? = null) = task {
|
|
||||||
val cronUser = user ?: whoami()
|
|
||||||
|
|
||||||
// ensure command exists
|
|
||||||
if (checkFile(command, sudo = true)) {
|
|
||||||
val cronLine = "$schedule $cronUser $command $parameter\n"
|
|
||||||
createDirs("/etc/cron.d/", sudo = true)
|
|
||||||
createFile("/etc/cron.d/$cronFilename", cronLine, "644", sudo = true, overwriteIfExisting = true)
|
|
||||||
} else {
|
|
||||||
addResultToEval(ProvResult(false, err = "$command not found."))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,25 +6,24 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
abstract class SecretSource(protected val parameter: String) {
|
abstract class SecretSource(protected val input: String) {
|
||||||
abstract fun secret() : Secret
|
abstract fun secret() : Secret
|
||||||
abstract fun secretNullable() : Secret?
|
abstract fun secretNullable() : Secret?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
enum class SecretSourceType {
|
enum class SecretSourceType() {
|
||||||
|
|
||||||
PLAIN, FILE, PROMPT, PASS, GOPASS, ENV;
|
PLAIN, FILE, PROMPT, PASS, GOPASS;
|
||||||
|
|
||||||
fun secret(parameter: String) : Secret {
|
fun secret(input: String) : Secret {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
PLAIN -> PlainSecretSource(parameter).secret()
|
PLAIN -> PlainSecretSource(input).secret()
|
||||||
FILE -> FileSecretSource(parameter).secret()
|
FILE -> FileSecretSource(input).secret()
|
||||||
PROMPT -> PromptSecretSource().secret()
|
PROMPT -> PromptSecretSource().secret()
|
||||||
PASS -> PassSecretSource(parameter).secret()
|
PASS -> PassSecretSource(input).secret()
|
||||||
GOPASS -> GopassSecretSource(parameter).secret()
|
GOPASS -> GopassSecretSource(input).secret()
|
||||||
ENV -> EnvSecretSource(parameter).secret()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Secret
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads secret from a local environment variable
|
|
||||||
*/
|
|
||||||
class EnvSecretSource(varName: String) : SecretSource(varName) {
|
|
||||||
override fun secret(): Secret {
|
|
||||||
return secretNullable() ?: throw Exception("Failed to get secret from environment variable: $parameter")
|
|
||||||
}
|
|
||||||
override fun secretNullable(): Secret? {
|
|
||||||
val secret = System.getenv(parameter)
|
|
||||||
return if (secret == null) null else Secret(secret)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,5 @@
|
||||||
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
|
||||||
|
@ -12,12 +11,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", progressType = ProgressType.NONE)
|
val p = Prov.newInstance(name = "FileSecretSource")
|
||||||
return p.getSecret("cat " + parameter) ?: 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", progressType = ProgressType.NONE)
|
val p = Prov.newInstance(name = "FileSecretSource")
|
||||||
return p.getSecret("cat " + parameter)
|
return p.getSecret("cat " + input)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
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,10 +10,10 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
|
||||||
*/
|
*/
|
||||||
class GopassSecretSource(path: String) : SecretSource(path) {
|
class GopassSecretSource(path: String) : SecretSource(path) {
|
||||||
override fun secret(): Secret {
|
override fun secret(): Secret {
|
||||||
return secretNullable() ?: throw Exception("Failed to get \"$parameter\" 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 $parameter", progressType = ProgressType.NONE)
|
val p = Prov.newInstance(name = "GopassSecretSource for $input")
|
||||||
return p.getSecret("gopass show -f $parameter", true)
|
return p.getSecret("gopass show -f $input", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
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,11 +10,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", progressType = ProgressType.NONE)
|
val p = Prov.newInstance(name = "PassSecretSource")
|
||||||
return p.getSecret("pass " + parameter) ?: 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", progressType = ProgressType.NONE)
|
val p = Prov.newInstance(name = "PassSecretSource")
|
||||||
return p.getSecret("pass " + parameter)
|
return p.getSecret("pass " + input)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,9 +6,9 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
|
||||||
|
|
||||||
class PlainSecretSource(plainSecret: String) : SecretSource(plainSecret) {
|
class PlainSecretSource(plainSecret: String) : SecretSource(plainSecret) {
|
||||||
override fun secret(): Secret {
|
override fun secret(): Secret {
|
||||||
return Secret(parameter)
|
return Secret(input)
|
||||||
}
|
}
|
||||||
override fun secretNullable(): Secret {
|
override fun secretNullable(): Secret {
|
||||||
return Secret(parameter)
|
return Secret(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,7 +8,7 @@ import javax.swing.*
|
||||||
|
|
||||||
class PasswordPanel : JPanel(FlowLayout()) {
|
class PasswordPanel : JPanel(FlowLayout()) {
|
||||||
|
|
||||||
private val passwordField = JPasswordField(30)
|
private val passwordField = JPasswordField(20)
|
||||||
private var entered = false
|
private var entered = false
|
||||||
|
|
||||||
val enteredPassword
|
val enteredPassword
|
||||||
|
@ -47,7 +47,7 @@ class PasswordPanel : JPanel(FlowLayout()) {
|
||||||
class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
|
class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
|
||||||
|
|
||||||
override fun secret(): Secret {
|
override fun secret(): Secret {
|
||||||
val password = PasswordPanel.requestPassword(parameter)
|
val password = PasswordPanel.requestPassword(input)
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
throw IllegalArgumentException("Failed to retrieve secret from prompting.")
|
throw IllegalArgumentException("Failed to retrieve secret from prompting.")
|
||||||
} else {
|
} else {
|
||||||
|
@ -56,7 +56,7 @@ class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun secretNullable(): Secret? {
|
override fun secretNullable(): Secret? {
|
||||||
val password = PasswordPanel.requestPassword(parameter)
|
val password = PasswordPanel.requestPassword(input)
|
||||||
|
|
||||||
return if(password == null) {
|
return if(password == null) {
|
||||||
null
|
null
|
||||||
|
|
|
@ -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,
|
||||||
userCanSudoWithoutPassword: Boolean = false,
|
sudo: 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 (userCanSudoWithoutPassword) {
|
if (sudo) {
|
||||||
makeUserSudoerWithoutPasswordRequired(userName)
|
makeUserSudoerWithNoSudoPasswordRequired(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.makeUserSudoerWithoutPasswordRequired(
|
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(
|
||||||
userName: String,
|
userName: String,
|
||||||
password: Secret? = null,
|
password: Secret? = null,
|
||||||
overwriteFile: Boolean = false
|
overwriteFile: Boolean = false
|
||||||
): ProvResult = taskWithResult {
|
): ProvResult = task {
|
||||||
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,10 +107,11 @@ fun Prov.makeUserSudoerWithoutPasswordRequired(
|
||||||
* 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.
|
||||||
*/
|
*/
|
||||||
fun Prov.makeCurrentUserSudoerWithoutPasswordRequired(password: Secret) = taskWithResult {
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = task {
|
||||||
val currentUser = whoami()
|
val currentUser = whoami()
|
||||||
if (currentUser != null) {
|
if (currentUser != null) {
|
||||||
makeUserSudoerWithoutPasswordRequired(currentUser, password, overwriteFile = true)
|
makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true)
|
||||||
} else {
|
} else {
|
||||||
ProvResult(false, "Current user could not be determined.")
|
ProvResult(false, "Current user could not be determined.")
|
||||||
}
|
}
|
||||||
|
@ -130,10 +131,11 @@ 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.currentUserCanSudoWithoutPassword(): Boolean {
|
fun Prov.currentUserCanSudo(): Boolean {
|
||||||
return chk("timeout 1 sudo -kS id")
|
return chk("timeout 1 sudo id")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns username of current user if it can be determined
|
* Returns username of current user if it can be determined
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
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.printProvsVersion
|
|
||||||
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
|
||||||
|
@ -18,8 +15,6 @@ import kotlin.system.exitProcess
|
||||||
*/
|
*/
|
||||||
fun main(args: Array<String>) {
|
fun main(args: Array<String>) {
|
||||||
|
|
||||||
printProvsVersion()
|
|
||||||
|
|
||||||
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
|
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
|
||||||
|
|
||||||
// validate subcommand
|
// validate subcommand
|
||||||
|
@ -28,18 +23,16 @@ fun main(args: Array<String>) {
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val cmd = CliArgumentsParser("provs-server 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.")
|
||||||
quit(1)
|
exitProcess(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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
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
|
||||||
|
@ -12,7 +11,6 @@ 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())
|
||||||
|
@ -86,7 +84,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 = only?.let { listOf(it.name.lowercase()) }
|
super.onlyModules = if (only != null) listOf(only!!.name.lowercase()) else null
|
||||||
super.reprovision = cliReprovision == true
|
super.reprovision = cliReprovision == true
|
||||||
super.parsed = true
|
super.parsed = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.domain.hetzner_csi
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionHetznerCSIForK8s
|
|
||||||
|
|
||||||
fun Prov.provisionHetznerCSI(configResolved: HetznerCSIConfigResolved) = task {
|
|
||||||
provisionHetznerCSIForK8s(configResolved.hcloudApiToken, configResolved.encryptionPassphrase)
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.domain.hetzner_csi
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Secret
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSupplier
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HetznerCSIConfig (
|
|
||||||
val hcloudApiToken: SecretSupplier,
|
|
||||||
val encryptionPassphrase: SecretSupplier,
|
|
||||||
) {
|
|
||||||
fun resolveSecret(): HetznerCSIConfigResolved = HetznerCSIConfigResolved(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HetznerCSIConfigResolved(val configUnresolved: HetznerCSIConfig) {
|
|
||||||
val hcloudApiToken: Secret = configUnresolved.hcloudApiToken.secret()
|
|
||||||
val encryptionPassphrase: Secret = configUnresolved.encryptionPassphrase.secret()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class HetznerCSIConfigHolder(
|
|
||||||
val hetzner: HetznerCSIConfig
|
|
||||||
)
|
|
|
@ -1,26 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,8 @@ package org.domaindrivenarchitecture.provs.server.domain.k3s
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class ApplicationFileName(val fileName: String) {
|
data class ApplicationFileName(val fileName: String) {
|
||||||
fun fullyQualifiedName() : String {
|
fun fullqualified() : String {
|
||||||
return File(fileName).absoluteFile.absolutePath
|
return File(fileName).absoluteFile.absolutePath
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.domain.k3s
|
package org.domaindrivenarchitecture.provs.server.domain.k3s
|
||||||
|
|
||||||
interface ApplicationFileRepository {
|
interface ApplicationFileRepository {
|
||||||
fun getFile() : ApplicationFile
|
fun assertExists(applicationFileName: ApplicationFileName?)
|
||||||
|
|
||||||
}
|
}
|
|
@ -10,8 +10,7 @@ data class K3sConfig(
|
||||||
val loopback: Loopback = Loopback(ipv4 = "192.168.5.1", ipv6 = "fc00::5:1"),
|
val loopback: Loopback = Loopback(ipv4 = "192.168.5.1", ipv6 = "fc00::5:1"),
|
||||||
val certmanager: Certmanager? = null,
|
val certmanager: Certmanager? = null,
|
||||||
val echo: Echo? = null,
|
val echo: Echo? = null,
|
||||||
val reprovision: Reprovision = false,
|
val reprovision: Reprovision = false
|
||||||
val monthlyReboot: Boolean = true,
|
|
||||||
) {
|
) {
|
||||||
fun isDualStack(): Boolean {
|
fun isDualStack(): Boolean {
|
||||||
return node.ipv6 != null && loopback.ipv6 != null
|
return node.ipv6 != null && loopback.ipv6 != null
|
||||||
|
|
|
@ -2,46 +2,25 @@ package org.domaindrivenarchitecture.provs.server.domain.k3s
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository
|
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.scheduledjobs.domain.scheduleMonthlyReboot
|
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfigResolved
|
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.provisionHetznerCSI
|
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.GrafanaAgentConfigResolved
|
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.GrafanaAgentConfigResolved
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.provisionGrafanaAgent
|
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.provisionGrafanaAgent
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.DefaultApplicationFileRepository
|
import org.domaindrivenarchitecture.provs.server.infrastructure.*
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.deprovisionK3sInfra
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.findHetznerCSIConfig
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.findK8sGrafanaConfig
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.getK3sConfig
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.installK3s
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.installK9s
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionK3sApplication
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionK3sCertManager
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionK3sEcho
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionNetwork
|
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.provisionServerCliConvenience
|
|
||||||
import kotlin.system.exitProcess
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
|
fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
|
||||||
|
|
||||||
val grafanaConfigResolved: GrafanaAgentConfigResolved? = findK8sGrafanaConfig(cli.configFileName)?.resolveSecret()
|
val grafanaConfigResolved: GrafanaAgentConfigResolved? = findK8sGrafanaConfig(cli.configFileName)?.resolveSecret()
|
||||||
val hcloudConfigResolved: HetznerCSIConfigResolved? = findHetznerCSIConfig(cli.configFileName)?.resolveSecret()
|
|
||||||
|
|
||||||
if (cli.onlyModules == null ) {
|
if (cli.onlyModules == null ) {
|
||||||
val k3sConfig: K3sConfig = getK3sConfig(cli.configFileName)
|
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 applicationFile = cli.applicationFileName?.let { DefaultApplicationFileRepository(cli.applicationFileName).getFile() }
|
val k3sConfigReprovision = k3sConfig.copy(reprovision = cli.reprovision || k3sConfig.reprovision)
|
||||||
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, hcloudConfigResolved, applicationFile)
|
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, cli.applicationFileName)
|
||||||
} else {
|
} else {
|
||||||
cli.onlyModules.forEach { module ->
|
provisionGrafana(cli.onlyModules, grafanaConfigResolved)
|
||||||
when (module.uppercase()) {
|
|
||||||
ServerOnlyModule.MONTHLY_REBOOT.name -> scheduleMonthlyReboot()
|
|
||||||
ServerOnlyModule.HETZNER_CSI.name -> provisionHetznerCSI(hcloudConfigResolved)
|
|
||||||
ServerOnlyModule.GRAFANA.name -> provisionGrafana(grafanaConfigResolved)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,9 +30,7 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
|
||||||
fun Prov.provisionK3s(
|
fun Prov.provisionK3s(
|
||||||
k3sConfig: K3sConfig,
|
k3sConfig: K3sConfig,
|
||||||
grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
|
grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
|
||||||
hetznerCSIConfigResolved: HetznerCSIConfigResolved? = null,
|
applicationFileName: ApplicationFileName? = null) = task {
|
||||||
applicationFile: ApplicationFile? = null
|
|
||||||
) = task {
|
|
||||||
|
|
||||||
if (k3sConfig.reprovision) {
|
if (k3sConfig.reprovision) {
|
||||||
deprovisionK3sInfra()
|
deprovisionK3sInfra()
|
||||||
|
@ -75,42 +52,24 @@ fun Prov.provisionK3s(
|
||||||
provisionGrafanaAgent(grafanaConfigResolved)
|
provisionGrafanaAgent(grafanaConfigResolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hetznerCSIConfigResolved != null) {
|
if (applicationFileName != null) {
|
||||||
provisionHetznerCSI(hetznerCSIConfigResolved)
|
provisionK3sApplication(applicationFileName)
|
||||||
}
|
|
||||||
|
|
||||||
if (applicationFile != null) {
|
|
||||||
provisionK3sApplication(applicationFile)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!k3sConfig.reprovision) {
|
if (!k3sConfig.reprovision) {
|
||||||
provisionServerCliConvenience()
|
provisionServerCliConvenience()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k3sConfig.monthlyReboot) {
|
|
||||||
scheduleMonthlyReboot()
|
|
||||||
}
|
|
||||||
|
|
||||||
installK9s()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Prov.provisionGrafana(
|
private fun Prov.provisionGrafana(
|
||||||
grafanaConfigResolved: GrafanaAgentConfigResolved?
|
onlyModules: List<String>?,
|
||||||
) = task {
|
grafanaConfigResolved: GrafanaAgentConfigResolved?) = task {
|
||||||
|
|
||||||
|
if (onlyModules != null && onlyModules.contains(ServerOnlyModule.GRAFANA.name.lowercase())) {
|
||||||
if (grafanaConfigResolved == null) {
|
if (grafanaConfigResolved == null) {
|
||||||
println("ERROR: Could not find grafana config.")
|
println("ERROR: Could not find grafana config.")
|
||||||
exitProcess(7)
|
exitProcess(7)
|
||||||
}
|
}
|
||||||
provisionGrafanaAgent(grafanaConfigResolved)
|
provisionGrafanaAgent(grafanaConfigResolved)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Prov.provisionHetznerCSI(
|
|
||||||
hetznerCSIConfigResolved: HetznerCSIConfigResolved?
|
|
||||||
) = task {
|
|
||||||
if (hetznerCSIConfigResolved == null) {
|
|
||||||
println("ERROR: Could not find hetznerCSI config.")
|
|
||||||
exitProcess(7)
|
|
||||||
}
|
}
|
||||||
provisionHetznerCSI(hetznerCSIConfigResolved)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.domain.k3s
|
package org.domaindrivenarchitecture.provs.server.domain.k3s
|
||||||
|
|
||||||
enum class ServerOnlyModule {
|
enum class ServerOnlyModule {
|
||||||
GRAFANA,
|
GRAFANA
|
||||||
HETZNER_CSI,
|
|
||||||
MONTHLY_REBOOT,
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.domaindrivenarchitecture.provs.server.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.ubuntu.filesystem.base.createFile
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource
|
||||||
|
|
||||||
private const val resourcePath = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
|
private const val resourcePath = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
|
||||||
|
@ -15,7 +16,6 @@ fun Prov.provisionServerCliConvenience() = task {
|
||||||
fun Prov.provisionKubectlCompletionAndAlias(): ProvResult = task {
|
fun Prov.provisionKubectlCompletionAndAlias(): ProvResult = task {
|
||||||
cmd("kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null")
|
cmd("kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null")
|
||||||
cmd("echo 'alias k=kubectl' >> ~/.bashrc")
|
cmd("echo 'alias k=kubectl' >> ~/.bashrc")
|
||||||
cmd("echo 'alias k9=\"k9s --kubeconfig /etc/kubernetes/admin.conf\"' >> ~/.bashrc")
|
|
||||||
cmd("echo 'complete -o default -F __start_kubectl k' >>~/.bashrc")
|
cmd("echo 'complete -o default -F __start_kubectl k' >>~/.bashrc")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,30 +1,14 @@
|
||||||
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 {
|
||||||
|
|
||||||
class DefaultApplicationFileRepository(val applicationFileName: ApplicationFileName) : ApplicationFileRepository {
|
override fun assertExists(applicationFileName: ApplicationFileName?) {
|
||||||
|
if (applicationFileName != null && !checkLocalFile(applicationFileName.fullqualified())) {
|
||||||
private fun assertExists(applicationFileName: String) {
|
throw RuntimeException("Application file ${applicationFileName.fileName} not found. Please check if path is correct.")
|
||||||
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.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.infrastructure
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Secret
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResourceTemplate
|
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.k3s.FileMode
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private const val hetznerCSIResourceDir = "org/domaindrivenarchitecture/provs/server/infrastructure/hetznerCSI/"
|
|
||||||
fun Prov.provisionHetznerCSIForK8s(hetznerApiToken: Secret, encryptionPassphrase: Secret) {
|
|
||||||
// CSI Driver
|
|
||||||
createFileFromResourceTemplate(
|
|
||||||
k3sManualManifestsDir + "hcloud-api-token-secret.yaml",
|
|
||||||
"hcloud-api-token-secret.template.yaml",
|
|
||||||
resourcePath = hetznerCSIResourceDir,
|
|
||||||
posixFilePermission = "644",
|
|
||||||
values = mapOf(
|
|
||||||
"HETZNER_API_TOKEN" to hetznerApiToken.plain()
|
|
||||||
))
|
|
||||||
cmd("kubectl apply -f hcloud-api-token-secret.yaml", k3sManualManifestsDir)
|
|
||||||
applyHetznerCSIFileFromResource(File(k3sManualManifestsDir, "hcloud-csi.yaml"))
|
|
||||||
|
|
||||||
// Encryption
|
|
||||||
createFileFromResourceTemplate(
|
|
||||||
k3sManualManifestsDir + "hcloud-encryption-secret.yaml",
|
|
||||||
"hcloud-encryption-secret.template.yaml",
|
|
||||||
resourcePath = hetznerCSIResourceDir,
|
|
||||||
posixFilePermission = "644",
|
|
||||||
values = mapOf(
|
|
||||||
"HETZNER_ENCRYPTION_PASSPHRASE" to encryptionPassphrase.plain()
|
|
||||||
))
|
|
||||||
cmd("kubectl apply -f hcloud-encryption-secret.yaml", k3sManualManifestsDir)
|
|
||||||
applyHetznerCSIFileFromResource(File(k3sManualManifestsDir, "hcloud-encrypted-storage-class.yaml"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Prov.createHetznerCSIFileFromResource(
|
|
||||||
file: File,
|
|
||||||
posixFilePermission: FileMode? = "644"
|
|
||||||
) = task {
|
|
||||||
createFileFromResource(
|
|
||||||
file.path,
|
|
||||||
file.name,
|
|
||||||
hetznerCSIResourceDir,
|
|
||||||
posixFilePermission,
|
|
||||||
sudo = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Prov.applyHetznerCSIFileFromResource(file: File, posixFilePermission: FileMode? = "644") = task {
|
|
||||||
createHetznerCSIFileFromResource(file, posixFilePermission)
|
|
||||||
cmd("kubectl apply -f ${file.path}", sudo = true)
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.infrastructure
|
|
||||||
|
|
||||||
import com.charleskorn.kaml.MissingRequiredPropertyException
|
|
||||||
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.readFromFile
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.toYaml
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.yamlToType
|
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfig
|
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfigHolder
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileWriter
|
|
||||||
|
|
||||||
private const val DEFAULT_CONFIG_FILE = "server-config.yaml"
|
|
||||||
|
|
||||||
fun findHetznerCSIConfig(fileName: ConfigFileName? = null): HetznerCSIConfig? {
|
|
||||||
val filePath = fileName?.fileName ?: DEFAULT_CONFIG_FILE
|
|
||||||
|
|
||||||
return if(File(filePath).exists()) {
|
|
||||||
try {
|
|
||||||
readFromFile(filePath).yamlToType<HetznerCSIConfigHolder>().hetzner
|
|
||||||
} catch (e: MissingRequiredPropertyException) {
|
|
||||||
if (e.message.contains("Property 'hetzner'")) null else throw e
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
internal fun writeConfig(config: HetznerCSIConfigHolder, fileName: String = "hetzner-config.yaml") =
|
|
||||||
FileWriter(fileName).use { it.write(config.toYaml()) }
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.server.domain
|
||||||
|
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.echoCommandForTextWithNewlinesReplaced
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.repeatTaskUntilSuccess
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a k3s server and a k3s agent as containers.
|
||||||
|
* Copies the kubeconfig from container to the default location: $HOME/.kube/config
|
||||||
|
*/
|
||||||
|
fun Prov.installK3sAsContainers(token: String = "12345678901234") = task {
|
||||||
|
cmd("docker volume create k3s-server")
|
||||||
|
provideContainer("k3s-server", "rancher/k3s", command = "server --cluster-init", options =
|
||||||
|
"-d --privileged --tmpfs /run --tmpfs /var/run " +
|
||||||
|
"-e K3S_TOKEN=$token -e K3S_KUBECONFIG_OUTPUT=./kubeconfig.yaml -e K3S_KUBECONFIG_MODE=666 " +
|
||||||
|
"-v k3s-server:/var/lib/rancher/k3s:z -p 6443:6443 -p 80:80 -p 443:443 " +
|
||||||
|
"--ulimit nproc=65535 --ulimit nofile=65535:65535")
|
||||||
|
|
||||||
|
// wait for config file
|
||||||
|
cmd("export timeout=60; while [ ! -f /var/lib/docker/volumes/k3s-server/_data/server/kubeconfig.yaml ]; do if [ \"${'$'}timeout\" == 0 ]; then echo \"ERROR: Timeout while waiting for file.\"; break; fi; sleep 1; ((timeout--)); done")
|
||||||
|
|
||||||
|
sh("""
|
||||||
|
mkdir -p ${'$'}HOME/.kube/
|
||||||
|
cp /var/lib/docker/volumes/k3s-server/_data/server/kubeconfig.yaml ${'$'}HOME/.kube/config
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a config to kubernetes.
|
||||||
|
* Prerequisite: Kubectl has to be installed
|
||||||
|
*/
|
||||||
|
fun Prov.applyK8sConfig(configAsYaml: String, kubectlCommand: String = "kubectl") = task {
|
||||||
|
repeatTaskUntilSuccess(6, 10) {
|
||||||
|
cmd(echoCommandForTextWithNewlinesReplaced(configAsYaml) + " | $kubectlCommand apply -f -")
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,14 +5,15 @@ 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.*
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
|
||||||
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.Certmanager
|
||||||
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.FileMode
|
||||||
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sConfig
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
// ----------------------------------- versions --------------------------------
|
// ----------------------------------- versions --------------------------------
|
||||||
|
|
||||||
// when updating this version, it is recommended to update also file k3s-install.sh as well as traefik.yaml in this repo
|
const val K3S_VERSION = "v1.23.6+k3s1"
|
||||||
// (both files in: src/main/resources/org/domaindrivenarchitecture/provs/server/infrastructure/k3s/)
|
|
||||||
const val K3S_VERSION = "v1.29.1+k3s2"
|
|
||||||
|
|
||||||
// ----------------------------------- directories --------------------------------
|
// ----------------------------------- directories --------------------------------
|
||||||
const val k3sManualManifestsDir = "/etc/rancher/k3s/manifests/"
|
const val k3sManualManifestsDir = "/etc/rancher/k3s/manifests/"
|
||||||
|
@ -33,12 +34,12 @@ private val k3sMiddleWareHttpsRedirect = File(k3sManualManifestsDir, "middleware
|
||||||
private val certManagerDeployment = File(k3sManualManifestsDir, "cert-manager.yaml")
|
private val certManagerDeployment = File(k3sManualManifestsDir, "cert-manager.yaml")
|
||||||
|
|
||||||
private val certManagerIssuer = File(k3sManualManifestsDir, "le-issuer.yaml")
|
private val certManagerIssuer = File(k3sManualManifestsDir, "le-issuer.yaml")
|
||||||
private val k3sEchoWithTls = File(k3sManualManifestsDir, "echo-tls.yaml")
|
private val k3sEcho = File(k3sManualManifestsDir, "echo.yaml")
|
||||||
private val k3sEchoNoTls = File(k3sManualManifestsDir, "echo-no-tls.yaml")
|
|
||||||
private val selfSignedCertificate = File(k3sManualManifestsDir, "selfsigned-certificate.yaml")
|
private val selfSignedCertificate = File(k3sManualManifestsDir, "selfsigned-certificate.yaml")
|
||||||
|
|
||||||
private val localPathProvisionerConfig = File(k3sManualManifestsDir, "local-path-provisioner-config.yaml")
|
private val localPathProvisionerConfig = File(k3sManualManifestsDir, "local-path-provisioner-config.yaml")
|
||||||
|
|
||||||
|
|
||||||
// ----------------------------------- public functions --------------------------------
|
// ----------------------------------- public functions --------------------------------
|
||||||
|
|
||||||
fun Prov.testConfigExists(): Boolean {
|
fun Prov.testConfigExists(): Boolean {
|
||||||
|
@ -51,11 +52,7 @@ fun Prov.deprovisionK3sInfra() = task {
|
||||||
deleteFile(certManagerDeployment.path, sudo = true)
|
deleteFile(certManagerDeployment.path, sudo = true)
|
||||||
deleteFile(certManagerIssuer.path, sudo = true)
|
deleteFile(certManagerIssuer.path, sudo = true)
|
||||||
deleteFile(k3sKubeConfig.path, sudo = true)
|
deleteFile(k3sKubeConfig.path, sudo = true)
|
||||||
|
cmd("k3s-uninstall.sh")
|
||||||
val k3sUninstallScript = "k3s-uninstall.sh"
|
|
||||||
if (chk("which $k3sUninstallScript")) {
|
|
||||||
cmd(k3sUninstallScript)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,35 +78,25 @@ 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"
|
||||||
require(k3sConfig.node.ipv6 != null && k3sConfig.loopback.ipv6 != null)
|
k3sConfigMap = k3sConfigMap.plus("node_ipv6" to k3sConfig.node.ipv6!!)
|
||||||
k3sConfigMap = k3sConfigMap
|
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6!!)
|
||||||
.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(
|
createK3sFileFromResourceTemplate(k3sConfigFile, k3sConfigMap, alternativeResourceTemplate = File(k3sConfigResourceFileName))
|
||||||
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-0.13.7-native-manifest.yaml"))
|
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-namespace.yaml"))
|
||||||
|
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.10.2-manifest.yaml"))
|
||||||
repeatTaskUntilSuccess(10, 10) {
|
|
||||||
applyK3sFileFromResourceTemplate(
|
applyK3sFileFromResourceTemplate(
|
||||||
File(k3sManualManifestsDir, "metallb-config.yaml"),
|
File(k3sManualManifestsDir, "metallb-config.yaml"),
|
||||||
k3sConfigMap,
|
k3sConfigMap,
|
||||||
alternativeResourceName = File(metallbConfigResourceFileName)
|
alternativeResourceName = File(metallbConfigResourceFileName)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-l2advertisement.yaml"))
|
|
||||||
|
|
||||||
// traefik
|
// traefik
|
||||||
if (k3sConfig.isDualStack()) {
|
if (k3sConfig.isDualStack()) {
|
||||||
|
@ -123,9 +110,8 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
|
||||||
applyK3sFileFromResource(k3sMiddleWareHttpsRedirect)
|
applyK3sFileFromResource(k3sMiddleWareHttpsRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
// other
|
|
||||||
applyK3sFileFromResource(localPathProvisionerConfig)
|
applyK3sFileFromResource(localPathProvisionerConfig)
|
||||||
|
// TODO: jem 2022-11-25: Why do we need sudo here??
|
||||||
cmd("kubectl set env deployment -n kube-system local-path-provisioner DEPLOY_DATE=\"$(date)\"", sudo = true)
|
cmd("kubectl set env deployment -n kube-system local-path-provisioner DEPLOY_DATE=\"$(date)\"", sudo = true)
|
||||||
|
|
||||||
cmd("ln -sf $k3sKubeConfig " + k8sCredentialsDir + "admin.conf", sudo = true)
|
cmd("ln -sf $k3sKubeConfig " + k8sCredentialsDir + "admin.conf", sudo = true)
|
||||||
|
@ -151,8 +137,7 @@ fun Prov.provisionK3sCertManager(certmanager: Certmanager) = task {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null, withTls: Boolean = false) = task {
|
fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null) = task {
|
||||||
if (withTls) {
|
|
||||||
val endpointName = endpoint?.name?.lowercase()
|
val endpointName = endpoint?.name?.lowercase()
|
||||||
|
|
||||||
val issuer = if (endpointName == null) {
|
val issuer = if (endpointName == null) {
|
||||||
|
@ -161,15 +146,13 @@ fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null, w
|
||||||
} else {
|
} else {
|
||||||
endpointName
|
endpointName
|
||||||
}
|
}
|
||||||
applyK3sFileFromResourceTemplate(k3sEchoWithTls, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
|
|
||||||
} else {
|
applyK3sFileFromResourceTemplate(k3sEcho, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
|
||||||
applyK3sFileFromResource(k3sEchoNoTls)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Prov.provisionK3sApplication(applicationFile: ApplicationFile) = task {
|
fun Prov.provisionK3sApplication(applicationFileName: ApplicationFileName) = task {
|
||||||
copyFileFromLocal(
|
copyFileFromLocal(
|
||||||
fullyQualifiedLocalFilename = applicationFile.id.fullyQualifiedName(),
|
fullyQualifiedLocalFilename = applicationFileName.fullqualified(),
|
||||||
fullyQualifiedFilename = k3sManualManifestsDir + "application.yaml",
|
fullyQualifiedFilename = k3sManualManifestsDir + "application.yaml",
|
||||||
posixFilePermission = "644",
|
posixFilePermission = "644",
|
||||||
sudo = true
|
sudo = true
|
||||||
|
@ -233,5 +216,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",))
|
||||||
}
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.infrastructure
|
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
|
|
||||||
|
|
||||||
|
|
||||||
const val K9S_VERSION = "v0.32.5"
|
|
||||||
|
|
||||||
|
|
||||||
fun Prov.installK9s() = task {
|
|
||||||
if (cmdNoEval("k9s version").out?.contains(K9S_VERSION) != true) {
|
|
||||||
downloadFromURL("https://github.com/derailed/k9s/releases/download/$K9S_VERSION/k9s_linux_amd64.deb", "k9s_linux_amd64.deb", "/tmp")
|
|
||||||
cmd("sudo dpkg -i k9s_linux_amd64.deb", "/tmp")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,12 +16,11 @@ 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
|
||||||
)
|
)
|
|
@ -1,5 +1,10 @@
|
||||||
package org.domaindrivenarchitecture.provs.syspec.infrastructure
|
package org.domaindrivenarchitecture.provs.syspec.infrastructure
|
||||||
|
|
||||||
|
import aws.sdk.kotlin.services.s3.S3Client
|
||||||
|
import aws.sdk.kotlin.services.s3.model.ListObjectsRequest
|
||||||
|
import aws.sdk.kotlin.services.s3.model.ListObjectsResponse
|
||||||
|
import aws.smithy.kotlin.runtime.time.Instant
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkDir
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkDir
|
||||||
|
@ -8,6 +13,7 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackag
|
||||||
import org.domaindrivenarchitecture.provs.syspec.domain.*
|
import org.domaindrivenarchitecture.provs.syspec.domain.*
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
import java.time.Duration
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -23,6 +29,7 @@ fun Prov.verifySpecConfig(conf: SyspecConfig) = task {
|
||||||
conf.netcat?.let { task("NetcatSpecs") { for (spec in conf.netcat) verify(spec) } }
|
conf.netcat?.let { task("NetcatSpecs") { for (spec in conf.netcat) verify(spec) } }
|
||||||
conf.socket?.let { task("SocketSpecs") { for (spec in conf.socket) verify(spec) } }
|
conf.socket?.let { task("SocketSpecs") { for (spec in conf.socket) verify(spec) } }
|
||||||
conf.certificate?.let { task("CertificateFileSpecs") { for (spec in conf.certificate) verify(spec) } }
|
conf.certificate?.let { task("CertificateFileSpecs") { for (spec in conf.certificate) verify(spec) } }
|
||||||
|
conf.s3?.let { task("CertificateFileSpecs") { for (spec in conf.s3) verify(spec) } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------- verification functions for individual specs --------------------------------
|
// ------------------------------- verification functions for individual specs --------------------------------
|
||||||
|
@ -105,6 +112,27 @@ fun Prov.verify(cert: CertificateFileSpec) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(s3ObjectSpec: S3ObjectSpec) {
|
||||||
|
val (bucket, prefix, maxAge) = s3ObjectSpec
|
||||||
|
val expectedAge = Duration.ofHours(s3ObjectSpec.age)
|
||||||
|
|
||||||
|
val latestObject = getS3Objects(bucket, prefix).contents?.maxByOrNull { it.lastModified ?: Instant.fromEpochSeconds(0) }
|
||||||
|
|
||||||
|
if (latestObject == null) {
|
||||||
|
verify(false, "Could not retrieve an s3 object with prefix $prefix")
|
||||||
|
} else {
|
||||||
|
// convert to java.time.Instant for easier comparison
|
||||||
|
val lastModified = java.time.Instant.ofEpochSecond(latestObject.lastModified?.epochSeconds ?: 0)
|
||||||
|
val actualAge = Duration.between(lastModified, java.time.Instant.now())
|
||||||
|
|
||||||
|
verify(
|
||||||
|
actualAge <= expectedAge,
|
||||||
|
"Age is ${actualAge.toHours()} h (expected: <= $maxAge) for latest file with prefix \"$prefix\" " +
|
||||||
|
"--- modified date: $lastModified - size: ${(latestObject.size)} B - key: ${latestObject.key}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// -------------------------- helper functions ---------------------------------
|
// -------------------------- helper functions ---------------------------------
|
||||||
|
|
||||||
|
@ -187,3 +215,14 @@ private fun Prov.verifyCertExpiration(enddate: String?, certName: String, expira
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getS3Objects(bucketName: String, prefixIn: String): ListObjectsResponse {
|
||||||
|
|
||||||
|
val request = ListObjectsRequest { bucket = bucketName; prefix = prefixIn }
|
||||||
|
|
||||||
|
return runBlocking {
|
||||||
|
S3Client { region = "eu-central-1" }.use { s3 ->
|
||||||
|
s3.listObjects(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ function usage() {
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
local cluster_name="${1}";
|
local cluster_name="${1}";
|
||||||
local domain_name="${2:-meissa.de}";
|
local domain_name="${2:-meissa-gmbh.de}";
|
||||||
|
|
||||||
/usr/local/bin/k3s-create-context.sh ${cluster_name} ${domain_name}
|
/usr/local/bin/k3s-create-context.sh ${cluster_name} ${domain_name}
|
||||||
kubectl config use-context ${cluster_name}
|
kubectl config use-context ${cluster_name}
|
||||||
|
|
|
@ -4,9 +4,8 @@ set -o noglob
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
local cluster_name="${1}"; shift
|
local cluster_name="${1}"; shift
|
||||||
local domain_name="${1:-meissa.de}"; shift
|
|
||||||
|
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.${domain_name} -L 8002:localhost:8002 -L 6443:192.168.5.1:6443
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.meissa-gmbh.de -L 8002:localhost:8002 -L 6443:192.168.5.1:6443
|
||||||
}
|
}
|
||||||
|
|
||||||
main $1
|
main $1
|
||||||
|
|
|
@ -4,9 +4,8 @@ set -o noglob
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
local cluster_name="${1}"; shift
|
local cluster_name="${1}"; shift
|
||||||
local domain_name="${1:-meissa.de}"; shift
|
|
||||||
|
|
||||||
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.${domain_name}
|
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.meissa-gmbh.de
|
||||||
}
|
}
|
||||||
|
|
||||||
main $1
|
main $1
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: hcloud
|
|
||||||
namespace: kube-system
|
|
||||||
stringData:
|
|
||||||
token: $HETZNER_API_TOKEN
|
|
|
@ -1,401 +0,0 @@
|
||||||
# Version 2.6.0
|
|
||||||
# Source: hcloud-csi/templates/controller/serviceaccount.yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-controller
|
|
||||||
namespace: "kube-system"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
automountServiceAccountToken: true
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/core/storageclass.yaml
|
|
||||||
kind: StorageClass
|
|
||||||
apiVersion: storage.k8s.io/v1
|
|
||||||
metadata:
|
|
||||||
name: hcloud-volumes
|
|
||||||
annotations:
|
|
||||||
storageclass.kubernetes.io/is-default-class: "true"
|
|
||||||
provisioner: csi.hetzner.cloud
|
|
||||||
volumeBindingMode: WaitForFirstConsumer
|
|
||||||
allowVolumeExpansion: true
|
|
||||||
reclaimPolicy: "Delete"
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/controller/clusterrole.yaml
|
|
||||||
kind: ClusterRole
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-controller
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
rules:
|
|
||||||
# attacher
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [persistentvolumes]
|
|
||||||
verbs: [get, list, watch, update, patch]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [nodes]
|
|
||||||
verbs: [get, list, watch]
|
|
||||||
- apiGroups: [csi.storage.k8s.io]
|
|
||||||
resources: [csinodeinfos]
|
|
||||||
verbs: [get, list, watch]
|
|
||||||
- apiGroups: [storage.k8s.io]
|
|
||||||
resources: [csinodes]
|
|
||||||
verbs: [get, list, watch]
|
|
||||||
- apiGroups: [storage.k8s.io]
|
|
||||||
resources: [volumeattachments]
|
|
||||||
verbs: [get, list, watch, update, patch]
|
|
||||||
- apiGroups: [storage.k8s.io]
|
|
||||||
resources: [volumeattachments/status]
|
|
||||||
verbs: [patch]
|
|
||||||
# provisioner
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [secrets]
|
|
||||||
verbs: [get, list]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [persistentvolumes]
|
|
||||||
verbs: [get, list, watch, create, delete, patch]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [persistentvolumeclaims, persistentvolumeclaims/status]
|
|
||||||
verbs: [get, list, watch, update, patch]
|
|
||||||
- apiGroups: [storage.k8s.io]
|
|
||||||
resources: [storageclasses]
|
|
||||||
verbs: [get, list, watch]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [events]
|
|
||||||
verbs: [list, watch, create, update, patch]
|
|
||||||
- apiGroups: [snapshot.storage.k8s.io]
|
|
||||||
resources: [volumesnapshots]
|
|
||||||
verbs: [get, list]
|
|
||||||
- apiGroups: [snapshot.storage.k8s.io]
|
|
||||||
resources: [volumesnapshotcontents]
|
|
||||||
verbs: [get, list]
|
|
||||||
# resizer
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [pods]
|
|
||||||
verbs: [get, list, watch]
|
|
||||||
# node
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: [events]
|
|
||||||
verbs: [get, list, watch, create, update, patch]
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/controller/clusterrolebinding.yaml
|
|
||||||
kind: ClusterRoleBinding
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-controller
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
roleRef:
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
kind: ClusterRole
|
|
||||||
name: hcloud-csi-controller
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: hcloud-csi-controller
|
|
||||||
namespace: "kube-system"
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/controller/service.yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-controller-metrics
|
|
||||||
namespace: "kube-system"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: metrics
|
|
||||||
port: 9189
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/node/service.yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-node-metrics
|
|
||||||
namespace: "kube-system"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: node
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: metrics
|
|
||||||
port: 9189
|
|
||||||
selector:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: node
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/node/daemonset.yaml
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: DaemonSet
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-node
|
|
||||||
namespace: "kube-system"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: node
|
|
||||||
app: hcloud-csi
|
|
||||||
spec:
|
|
||||||
updateStrategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: hcloud-csi
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: node
|
|
||||||
app: hcloud-csi
|
|
||||||
spec:
|
|
||||||
|
|
||||||
affinity:
|
|
||||||
nodeAffinity:
|
|
||||||
requiredDuringSchedulingIgnoredDuringExecution:
|
|
||||||
nodeSelectorTerms:
|
|
||||||
- matchExpressions:
|
|
||||||
- key: instance.hetzner.cloud/is-root-server
|
|
||||||
operator: NotIn
|
|
||||||
values:
|
|
||||||
- "true"
|
|
||||||
tolerations:
|
|
||||||
- effect: NoExecute
|
|
||||||
operator: Exists
|
|
||||||
- effect: NoSchedule
|
|
||||||
operator: Exists
|
|
||||||
- key: CriticalAddonsOnly
|
|
||||||
operator: Exists
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1001
|
|
||||||
initContainers:
|
|
||||||
containers:
|
|
||||||
- name: csi-node-driver-registrar
|
|
||||||
image: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.7.0
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
args:
|
|
||||||
- --kubelet-registration-path=/var/lib/kubelet/plugins/csi.hetzner.cloud/socket
|
|
||||||
volumeMounts:
|
|
||||||
- name: plugin-dir
|
|
||||||
mountPath: /run/csi
|
|
||||||
- name: registration-dir
|
|
||||||
mountPath: /registration
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
- name: liveness-probe
|
|
||||||
image: registry.k8s.io/sig-storage/livenessprobe:v2.9.0
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /run/csi
|
|
||||||
name: plugin-dir
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
- name: hcloud-csi-driver
|
|
||||||
image: docker.io/hetznercloud/hcloud-csi-driver:v2.6.0 # x-release-please-version
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
command: [/bin/hcloud-csi-driver-node]
|
|
||||||
volumeMounts:
|
|
||||||
- name: kubelet-dir
|
|
||||||
mountPath: /var/lib/kubelet
|
|
||||||
mountPropagation: "Bidirectional"
|
|
||||||
- name: plugin-dir
|
|
||||||
mountPath: /run/csi
|
|
||||||
- name: device-dir
|
|
||||||
mountPath: /dev
|
|
||||||
securityContext:
|
|
||||||
privileged: true
|
|
||||||
env:
|
|
||||||
- name: CSI_ENDPOINT
|
|
||||||
value: unix:///run/csi/socket
|
|
||||||
- name: METRICS_ENDPOINT
|
|
||||||
value: "0.0.0.0:9189"
|
|
||||||
- name: ENABLE_METRICS
|
|
||||||
value: "true"
|
|
||||||
ports:
|
|
||||||
- containerPort: 9189
|
|
||||||
name: metrics
|
|
||||||
- name: healthz
|
|
||||||
protocol: TCP
|
|
||||||
containerPort: 9808
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 5
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 2
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 3
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: healthz
|
|
||||||
volumes:
|
|
||||||
- name: kubelet-dir
|
|
||||||
hostPath:
|
|
||||||
path: /var/lib/kubelet
|
|
||||||
type: Directory
|
|
||||||
- name: plugin-dir
|
|
||||||
hostPath:
|
|
||||||
path: /var/lib/kubelet/plugins/csi.hetzner.cloud/
|
|
||||||
type: DirectoryOrCreate
|
|
||||||
- name: registration-dir
|
|
||||||
hostPath:
|
|
||||||
path: /var/lib/kubelet/plugins_registry/
|
|
||||||
type: Directory
|
|
||||||
- name: device-dir
|
|
||||||
hostPath:
|
|
||||||
path: /dev
|
|
||||||
type: Directory
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/controller/deployment.yaml
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: hcloud-csi-controller
|
|
||||||
namespace: "kube-system"
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
app: hcloud-csi-controller
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
strategy:
|
|
||||||
type: RollingUpdate
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: hcloud-csi-controller
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app.kubernetes.io/name: hcloud-csi
|
|
||||||
app.kubernetes.io/instance: hcloud-csi
|
|
||||||
app.kubernetes.io/component: controller
|
|
||||||
app: hcloud-csi-controller
|
|
||||||
spec:
|
|
||||||
serviceAccountName: hcloud-csi-controller
|
|
||||||
|
|
||||||
securityContext:
|
|
||||||
fsGroup: 1001
|
|
||||||
initContainers:
|
|
||||||
containers:
|
|
||||||
- name: csi-attacher
|
|
||||||
image: registry.k8s.io/sig-storage/csi-attacher:v4.1.0
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
args:
|
|
||||||
- --default-fstype=ext4
|
|
||||||
volumeMounts:
|
|
||||||
- name: socket-dir
|
|
||||||
mountPath: /run/csi
|
|
||||||
|
|
||||||
- name: csi-resizer
|
|
||||||
image: registry.k8s.io/sig-storage/csi-resizer:v1.7.0
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
volumeMounts:
|
|
||||||
- name: socket-dir
|
|
||||||
mountPath: /run/csi
|
|
||||||
|
|
||||||
- name: csi-provisioner
|
|
||||||
image: registry.k8s.io/sig-storage/csi-provisioner:v3.4.0
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
args:
|
|
||||||
- --feature-gates=Topology=true
|
|
||||||
- --default-fstype=ext4
|
|
||||||
volumeMounts:
|
|
||||||
- name: socket-dir
|
|
||||||
mountPath: /run/csi
|
|
||||||
|
|
||||||
- name: liveness-probe
|
|
||||||
image: registry.k8s.io/sig-storage/livenessprobe:v2.9.0
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
volumeMounts:
|
|
||||||
- mountPath: /run/csi
|
|
||||||
name: socket-dir
|
|
||||||
|
|
||||||
- name: hcloud-csi-driver
|
|
||||||
image: docker.io/hetznercloud/hcloud-csi-driver:v2.6.0 # x-release-please-version
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
command: [/bin/hcloud-csi-driver-controller]
|
|
||||||
env:
|
|
||||||
- name: CSI_ENDPOINT
|
|
||||||
value: unix:///run/csi/socket
|
|
||||||
- name: METRICS_ENDPOINT
|
|
||||||
value: "0.0.0.0:9189"
|
|
||||||
- name: ENABLE_METRICS
|
|
||||||
value: "true"
|
|
||||||
- name: KUBE_NODE_NAME
|
|
||||||
valueFrom:
|
|
||||||
fieldRef:
|
|
||||||
apiVersion: v1
|
|
||||||
fieldPath: spec.nodeName
|
|
||||||
- name: HCLOUD_TOKEN
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: hcloud
|
|
||||||
key: token
|
|
||||||
resources:
|
|
||||||
limits: {}
|
|
||||||
requests: {}
|
|
||||||
ports:
|
|
||||||
- name: metrics
|
|
||||||
containerPort: 9189
|
|
||||||
- name: healthz
|
|
||||||
protocol: TCP
|
|
||||||
containerPort: 9808
|
|
||||||
livenessProbe:
|
|
||||||
failureThreshold: 5
|
|
||||||
initialDelaySeconds: 10
|
|
||||||
periodSeconds: 2
|
|
||||||
successThreshold: 1
|
|
||||||
timeoutSeconds: 3
|
|
||||||
httpGet:
|
|
||||||
path: /healthz
|
|
||||||
port: healthz
|
|
||||||
volumeMounts:
|
|
||||||
- name: socket-dir
|
|
||||||
mountPath: /run/csi
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- name: socket-dir
|
|
||||||
emptyDir: {}
|
|
||||||
---
|
|
||||||
# Source: hcloud-csi/templates/core/csidriver.yaml
|
|
||||||
apiVersion: storage.k8s.io/v1
|
|
||||||
kind: CSIDriver
|
|
||||||
metadata:
|
|
||||||
name: csi.hetzner.cloud
|
|
||||||
spec:
|
|
||||||
attachRequired: true
|
|
||||||
fsGroupPolicy: File
|
|
||||||
podInfoOnMount: true
|
|
||||||
volumeLifecycleModes:
|
|
||||||
- Persistent
|
|
|
@ -1,11 +0,0 @@
|
||||||
apiVersion: storage.k8s.io/v1
|
|
||||||
kind: StorageClass
|
|
||||||
metadata:
|
|
||||||
name: hcloud-volumes-encrypted
|
|
||||||
provisioner: csi.hetzner.cloud
|
|
||||||
reclaimPolicy: Delete
|
|
||||||
volumeBindingMode: WaitForFirstConsumer
|
|
||||||
allowVolumeExpansion: true
|
|
||||||
parameters:
|
|
||||||
csi.storage.k8s.io/node-publish-secret-name: encryption-secret
|
|
||||||
csi.storage.k8s.io/node-publish-secret-namespace: kube-system
|
|
|
@ -1,7 +0,0 @@
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
name: encryption-secret
|
|
||||||
namespace: kube-system
|
|
||||||
stringData:
|
|
||||||
encryption-passphrase: $HETZNER_ENCRYPTION_PASSPHRASE
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue