Compare commits

..

No commits in common. "main" and "release-0.0.555" have entirely different histories.

160 changed files with 2094 additions and 8366 deletions

1
.gitignore vendored
View file

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

View file

@ -1,112 +1,157 @@
image: openjdk:11-jdk-slim
stages: stages:
- build - build
- test - test
- package - package
- publish - publish
- release
.kotlin-job: &kotlin before_script:
image: domaindrivenarchitecture/ddadevops-kotlin:4.10.7
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .gradle/wrapper
- .gradle/caches
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 ----------"

View file

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

View file

@ -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="" />

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ release-1.2 or release-1.2.3
I.e.: release-X.X.Z where X, Y, Z are the major, minor resp. the patch level of the release. Z can be omitted. 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]

View file

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

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,8 +11,8 @@ import java.io.FileWriter
* Returns DesktopConfig; data for config is read from specified file. * Returns DesktopConfig; data for config is read from specified file.
* Throws exceptions FileNotFoundException, SerializationException if file is not found resp. cannot be parsed. * Throws exceptions FileNotFoundException, SerializationException if file is not found resp. cannot be parsed.
*/ */
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()) }

View file

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

View file

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

View file

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

View file

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

View file

@ -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() ?: ""
}

View file

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

View file

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

View file

@ -8,7 +8,7 @@ val NETWORK_TOOLS = "curl wget net-tools"
val KEY_MANAGEMENT_GUI = "seahorse" val KEY_MANAGEMENT_GUI = "seahorse"
val BROWSER = "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"

View file

@ -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 && "
}
} }

View file

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

View file

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

View file

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

View file

@ -20,7 +20,7 @@ internal fun getCallingMethodName(): String? {
val offsetVal = 1 val offsetVal = 1
val exclude = arrayOf("task", "task\$default", "taskWithResult\$default", "taskWithResult", "def", "def\$default", "record", "invoke", "invoke0", "evaluate", "evaluate\$default", ) val exclude = arrayOf("task", "task\$default", "taskWithResult\$default", "taskWithResult", "def", "def\$default", "record", "invoke", "invoke0", "evaluate", "evaluate\$default", )
// suffixes are also ignored as method names but will be added as suffix in the evaluation results // suffixes are also ignored as method names but will be added as suffix in the evaluation results
val suffixes = arrayOf("optional", "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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.Doc
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.* import org.domaindrivenarchitecture.provs.framework.core.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)
} }
} }

View file

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

View file

@ -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,16 +37,14 @@ 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 {

View file

@ -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))
} }
@ -54,7 +57,7 @@ open class ContainerUbuntuHostProcessor(
return s.escapeAndEncloseByDoubleQuoteForShell() return s.escapeAndEncloseByDoubleQuoteForShell()
} }
private fun buildCommand(vararg args: String): String { private fun buildCommand(vararg args: String) : String {
return if (args.size == 1) quoteString(args[0]) else return if (args.size == 1) quoteString(args[0]) else
if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) SHELL + " -c " + quoteString(args[2]) if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) SHELL + " -c " + quoteString(args[2])
else args.joinToString(separator = " ") else args.joinToString(separator = " ")

View file

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

View file

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

View file

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

View file

@ -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")
} }
/** /**

View file

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

View 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 {

View file

@ -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 ?: "" }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,15 +25,15 @@ fun Prov.userExists(userName: String): Boolean {
fun Prov.createUser( fun Prov.createUser(
userName: String, userName: String,
password: Secret? = null, password: Secret? = null,
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
*/ */

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,8 +16,7 @@ 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")
} }
fun Prov.provisionVimrc(): ProvResult = task { fun Prov.provisionVimrc(): ProvResult = task {

View file

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

View file

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

View file

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

View file

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

View file

@ -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/"
@ -24,8 +25,8 @@ private const val k3sResourceDir = "org/domaindrivenarchitecture/provs/server/in
// ----------------------------------- files -------------------------------- // ----------------------------------- files --------------------------------
private val k3sInstallScript = File("/usr/local/bin/k3s-install.sh") private val k3sInstallScript = File( "/usr/local/bin/k3s-install.sh")
private val k3sConfigFile = File("/etc/rancher/k3s/config.yaml") private val k3sConfigFile = File( "/etc/rancher/k3s/config.yaml")
private val k3sKubeConfig = File("/etc/rancher/k3s/k3s.yaml") private val k3sKubeConfig = File("/etc/rancher/k3s/k3s.yaml")
private val k3sTraefikWorkaround = File(k3sManualManifestsDir, "traefik.yaml") private val k3sTraefikWorkaround = File(k3sManualManifestsDir, "traefik.yaml")
@ -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",))
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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