Compare commits

..

No commits in common. "main" and "0.33.0" have entirely different histories.
main ... 0.33.0

75 changed files with 265 additions and 1324 deletions

1
.gitignore vendored
View file

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

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager">
<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="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />

View file

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

View file

@ -1,20 +1,20 @@
# provs
[![pipeline status](https://gitlab.com/domaindrivenarchitecture/provs/badges/master/pipeline.svg)](https://gitlab.com/domaindrivenarchitecture/provs/-/commits/master)
[<img src="https://domaindrivenarchitecture.org/img/delta-chat.svg" width=20 alt="DeltaChat"> chat over e-mail](mailto:buero@meissa-gmbh.de?subject=community-chat) | [<img src="https://meissa.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
provs provides cli-based tools for
* provisioning desktop software for different desktop types:
* basic
* office
* IDE
* provisioning a desktop (various kinds)
* provisioning a k3s server
* performing system checks
Tasks can be run locally or remotely.
## Status
under development - though we already set up a few IDEs and servers with provs.
## Try out
### Prerequisites
@ -28,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
* Preferably into `/usr/local/bin` or any other folder where executables can be found by the system
* Make the jar-file executable e.g. by `chmod +x provs-desktop.jar`
* Check with `provs-desktop.jar -h` to show help information
###### Build the binaries
#### Build the binaries
Instead of downloading the binaries you can build them yourself
@ -107,24 +106,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
```
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
```
To provision the grafana agent only to an existing k8s system, ensure that the config (as above) is available and execute:
```bash
provs-server.jar k3s myuser@myhost.com -o grafana
```
Reprovisioning the server can easily be done using the -r or --reprovision option.
```bash
@ -163,9 +144,7 @@ Or to get help for subcommands e.g.
provs-desktop.jar ide -h
provs-server.jar k3s -h
```
## Development & mirrors
Development happens at: https://repo.prod.meissa.de/meissa/provs
Mirrors are:
@ -173,17 +152,3 @@ Mirrors are:
* 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,5 +1,5 @@
buildscript {
ext.kotlin_version_no = "1.8.20"
ext.kotlin_version_no = "1.7.20"
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
repositories {
@ -16,7 +16,7 @@ plugins {
apply plugin: "maven-publish"
version = "0.38.6-SNAPSHOT"
version = "0.33.0"
group = "org.domaindrivenarchitecture.provs"
@ -35,6 +35,11 @@ java {
}
}
jar {
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
}
test {
// set properties for the tests
def propertiesForTests = ["testdockerwithoutsudo"]
@ -64,19 +69,24 @@ compileTestJava.options.debugOptions.debugLevel = "source,lines,vars"
dependencies {
api("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version_no")
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('com.charleskorn.kaml:kaml:0.54.0')
api("org.slf4j:slf4j-api:1.7.36")
api('ch.qos.logback:logback-classic:1.4.14')
api('ch.qos.logback:logback-core:1.4.14')
api('ch.qos.logback:logback-classic:1.2.11')
api('ch.qos.logback:logback-core:1.2.11')
implementation("com.hierynomus:sshj:0.38.0")
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version_no")
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('io.mockk:mockk:1.12.3')
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
}
@ -161,22 +171,6 @@ tasks.register('installlocally') {
}
// create binaries and install into /usr/local/bin
// PREREQUISITE: graalvm / native-image must be installed - see https://www.graalvm.org/
tasks.register('binariesInstall') {
dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
doLast {
println "Building binaries ..."
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-desktop.jar") }
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-server.jar") }
exec { commandLine("sh", "-c", "cd build/libs/ && native-image --no-fallback --initialize-at-build-time=kotlin.DeprecationLevel -H:+UnlockExperimentalVMOptions -H:IncludeResources=\".*org/domaindrivenarchitecture/provs/.*(conf|ssh_config|sshd_config|sh|vimrc|xml|yaml)\" -jar provs-syspec.jar") }
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-desktop /usr/local/bin/") }
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-server /usr/local/bin/") }
exec { commandLine("sh", "-c", "sudo cp build/libs/provs-syspec /usr/local/bin/") }
}
}
// publish to repo.prod.meissa.de with task "publishLibraryPublicationToMeissaRepository" -- (using pattern "publishLibraryPublicationTo<MAVEN REPOSITORY NAME>Repository")
publishing {
publications {

View file

@ -8,7 +8,7 @@ name = "provs"
PROJECT_ROOT_PATH = "."
version = "0.38.3-dev"
version = "0.33.0"
@init
@ -131,11 +131,6 @@ def publish_release(project):
build.publish_artifacts()
@task
def inst(project):
run("./gradlew inst", shell=True)
def increase_version_number(project, release_type):
build = get_devops_build(project)
build.update_release_type(release_type)

View file

@ -1,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.provisionK3s() = 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.
@ -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)
* **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
In the following link you can find an example of a sequence diagram when provisioning a desktop:
[ProvisionDesktopSequence.md](ProvisionDesktopSequence.md)
## Create a provs jar-file
* Clone this repo
* Build the jar-file by `./gradlew uberjarDesktop`
* In folder build/libs you'll find the file `provs-desktop.jar`
This uberjar is a Java jar-file including all required dependencies.

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.

View file

@ -2,6 +2,8 @@ This repository holds the documentation of the provs framework.
# Design principles
For usage examples it is recommended to have a look at [provs-scripts](https://gitlab.com/domaindrivenarchitecture/provs-scripts) or [provs-ubuntu-extensions](https://gitlab.com/domaindrivenarchitecture/provs-ubuntu-extensions).
## "Implarative"
Configuration management tools are usually classified as either **imperative** or **declarative**.

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

View file

@ -38,8 +38,8 @@ fun main(args: Array<String>) {
null
} catch (e: FileNotFoundException) {
println(
"Error: File\u001b[31m $configFileName \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m $configFileName \u001B[0m " +
"Error: File\u001b[31m ${configFileName} \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${configFileName} \u001B[0m " +
"and change the content according to your needs."
)
null

View file

@ -104,8 +104,7 @@ fun Prov.provisionBasicDesktop(
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"
"xfce4-power-manager-plugins xfce4-power-manager-data"
)
aptPurge("abiword gnumeric")
aptPurge("popularity-contest")
@ -135,13 +134,10 @@ fun Prov.provisionOfficeDesktop() {
installDeltaChat()
aptInstall(OFFICE_SUITE)
installZimWiki()
// installNextcloudClient() might not install - might need fix and working test
installNextcloudClient()
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, as installation of these tools often fail and as they are not considered mandatory
optional {
aptInstall(DRAWING_TOOLS)
}
@ -160,10 +156,8 @@ fun Prov.provisionIdeDesktop() {
installShadowCljs()
installDevOps()
provisionPython()
installHugoByDeb()
// IDEs
installVSC("python", "clojure")
installIntelliJ()
installKubeconform()
}

View file

@ -15,13 +15,13 @@ fun Prov.installDevOps() = task {
installTerraform()
installKubectlAndTools()
installYq()
installGraalVM()
}
fun Prov.installYq(
version: String = "4.13.2",
sha256sum: String = "d7c89543d1437bf80fee6237eadc608d1b121c21a7cbbe79057d5086d74f8d79"
) = task {
): ProvResult = task {
val path = "/usr/bin/"
val filename = "yq"
if (!checkFile(path + filename)) {
@ -38,7 +38,7 @@ fun Prov.installYq(
}
}
fun Prov.installKubectlAndTools() = task {
fun Prov.installKubectlAndTools(): ProvResult = task {
task("installKubectl") {
if (!checkFile(KUBE_CONFIG_CONTEXT_SCRIPT)) {
@ -49,34 +49,10 @@ fun Prov.installKubectlAndTools() = task {
}
}
task("installKubeconform") {
installKubeconform()
}
installDevopsScripts()
}
fun Prov.installKubeconform() = task {
// check for latest stable release on: https://github.com/yannh/kubeconform/releases
val version = "0.6.4"
val installationPath = "/usr/local/bin/"
val tmpDir = "~/tmp"
val filename = "kubeconform-linux-amd64"
val packedFilename = "$filename.tar.gz"
if ( !chk("kubeconform -v") || "v$version" != cmd("kubeconform -v").out?.trim() ) {
downloadFromURL(
"https://github.com/yannh/kubeconform/releases/download/v$version/$packedFilename",
path = tmpDir,
sha256sum = "2b4ebeaa4d5ac4843cf8f7b7e66a8874252b6b71bc7cbfc4ef1cbf85acec7c07"
)
cmd("sudo tar -xzf $packedFilename -C $installationPath", tmpDir)
} else {
ProvResult(true, out = "Kubeconform $version already installed")
}
}
fun Prov.installKubectl() = task {
fun Prov.installKubectl(): ProvResult = task {
// see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
val kubectlVersion = "1.27.4"
@ -91,19 +67,19 @@ fun Prov.installKubectl() = task {
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"
// from https://dl.k8s.io/v1.23.0/bin/linux/amd64/kubectl.sha256
sha256sum = "2d0f5ba6faa787878b642c151ccb2c3390ce4c1e6c8e2b59568b3869ba407c4f"
)
cmd("sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl", dir = tmpDir)
}
fun Prov.configureKubectlBashCompletion() = task {
fun Prov.configureKubectlBashCompletion(): ProvResult = task {
cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true)
createDir(".bashrc.d")
createFileFromResource(KUBE_CONFIG_CONTEXT_SCRIPT, "kubectl.sh", RESOURCE_PATH)
}
fun Prov.installDevopsScripts() = task {
fun Prov.installDevopsScripts() {
task("install ssh helper") {
createFileFromResource(
@ -145,7 +121,7 @@ fun Prov.installDevopsScripts() = task {
}
}
fun Prov.installTerraform() = task {
fun Prov.installTerraform(): ProvResult = task {
val dir = "/usr/lib/tfenv/"
if (!checkDir(dir)) {
@ -158,3 +134,34 @@ fun Prov.installTerraform() = task {
cmd("tfenv install latest:^1.4.6", sudo = true)
cmd("tfenv use latest:^1.4.6", 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()
}

View file

@ -11,10 +11,9 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFrom
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.15.5",
enforceVersion: Boolean = false,
// from https://github.com/gopasspw/gopass/releases/tag/v1.15.13
sha256sum: String = "409ed5617e64fa2c781d5e2807ba7fcd65bc383a4e110f410f90b590e51aec55"
sha256sum: String = "23ec10015c2643f22cb305859eb36d671094d463d2eb1798cc675e7bb06f4b39"
) = taskWithResult {
if (isPackageInstalled("gopass") && !enforceVersion) {

View file

@ -22,10 +22,10 @@ fun Prov.downloadGopassBridge() = task {
}
fun Prov.installGopassJsonApi() = taskWithResult {
// from https://github.com/gopasspw/gopass-jsonapi/releases/tag/v1.15.13
val sha256sum = "3162ab558301645024325ce2e419c1d67900e1faf95dc1774a36f1ebfc76389f"
val gopassJsonApiVersion = "1.15.13"
val requiredGopassVersion = "1.15.13"
// see https://github.com/gopasspw/gopass-jsonapi
val sha256sum = "ec9976e39a468428ae2eb1e2e0b9ceccba7f60d66b8097e2425b0c07f4fed108"
val gopassJsonApiVersion = "1.15.5"
val requiredGopassVersion = "1.15.5"
val filename = "gopass-jsonapi_${gopassJsonApiVersion}_linux_amd64.deb"
val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassJsonApiVersion/$filename"
val downloadDir = "${userHome()}Downloads"

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

@ -30,7 +30,7 @@ val OPENCONNECT = "openconnect network-manager-openconnect network-manager-openc
val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
val JAVA = "openjdk-17-jdk jarwrapper"
val JAVA = "openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk jarwrapper"
val DRAWING_TOOLS = "inkscape dia openboard graphviz"

View file

@ -6,33 +6,33 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInsta
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 pythonExtensions = setOf("ms-python.python")
installVSCodePrerequisites()
prerequisitesVSCinstall()
installVSCPackage()
installVSCodiumPackage()
if (options.contains("clojure")) {
installVSCodeExtensions(clojureExtensions)
installVSCodiumExtensions(clojureExtensions)
installExtensionsCode(clojureExtensions)
installExtensionsCodium(clojureExtensions)
}
if (options.contains("python")) {
installVSCodeExtensions(pythonExtensions)
installVSCodiumExtensions(pythonExtensions)
installExtensionsCode(pythonExtensions)
installExtensionsCodium(pythonExtensions)
}
}
private fun Prov.installVSCodePrerequisites() = task {
private fun Prov.prerequisitesVSCinstall() = task {
aptInstall("curl gpg unzip apt-transport-https")
}
@Suppress("unused") // only required for installation of vscode via apt
private fun Prov.installVSCodeWithApt() = task {
private fun Prov.installVscWithApt() = task {
val packageName = "code"
if (!isPackageInstalled(packageName)) {
// see https://code.visualstudio.com/docs/setup/linux
@ -62,7 +62,7 @@ private fun Prov.installVSCodiumPackage() = task {
}
private fun Prov.installVSCodeExtensions(extensions: Set<String>) = optional {
private fun Prov.installExtensionsCode(extensions: Set<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = cmd("code --install-extension $ext")
@ -71,11 +71,11 @@ private fun Prov.installVSCodeExtensions(extensions: Set<String>) = optional {
// Settings can be found at $HOME/.config/Code/User/settings.json
}
private fun Prov.installVSCodiumExtensions(extensions: Set<String>) = optional {
private fun Prov.installExtensionsCodium(extensions: Set<String>) = optional {
var res = ProvResult(true)
for (ext in extensions) {
res = ProvResult(res.success && cmd("codium --install-extension $ext").success)
res = cmd("codium --install-extension $ext")
}
res
// Settings can be found at $HOME/.config/VSCodium/User/settings.json
// Settings can be found at $HOME/.config/Code/User/settings.json
}

View file

@ -80,8 +80,8 @@ open class Prov protected constructor(
}
/**
* 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.
* A task is the base execution unit in provs. In the results overview it is represented by one line resp. result (of either success or failure).
* Returns success if no sub-tasks are called or if all subtasks finish with success.
*/
fun task(name: String? = null, taskLambda: Prov.() -> Unit): ProvResult {
printDeprecationWarningIfLevel0("task")
@ -89,10 +89,8 @@ open class Prov protected constructor(
}
/**
* Same as task above but the lambda parameter must have a ProvResult as return type.
* The returned ProvResult is included in the success resp. failure evaluation,
* i.e. if the returned ProvResult from the lambda fails, the returned ProvResult from
* taskWithResult also fails, else success depends on potentially called sub-tasks.
* Same as task but the provided lambda is explicitly required to provide a ProvResult to be returned.
* The returned result is included in the evaluation.
*/
fun taskWithResult(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("taskWithResult")
@ -100,27 +98,27 @@ open class Prov protected constructor(
}
/**
* 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(name: String? = null, a: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("requireLast")
return evaluate(ResultMode.LAST, name) { taskLambda() }
return evaluate(ResultMode.LAST, name) { a() }
}
/**
* 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(name: String? = null, a: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("optional")
return evaluate(ResultMode.OPTIONAL, name) { taskLambda() }
return evaluate(ResultMode.OPTIONAL, name) { a() }
}
/**
* 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) { taskLambda() }
return evaluate(ResultMode.FAILEXIT) { a() }
}
/**

View file

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

View file

@ -2,6 +2,7 @@ package org.domaindrivenarchitecture.provs.framework.core
import com.charleskorn.kaml.Yaml
import com.charleskorn.kaml.YamlConfiguration
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.serializer
import java.io.BufferedReader
import java.io.File
@ -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(
serializer<T>(),
T::class.serializer(),
this
)
@OptIn(InternalSerializationApi::class)
inline fun <reified T : Any> T.toYaml() = Yaml(configuration = YamlConfiguration(strictMode = false, encodeDefaults = false)).encodeToString(
serializer<T>(),
T::class.serializer(),
this
)

View file

@ -6,7 +6,7 @@ import org.domaindrivenarchitecture.provs.framework.core.docker.dockerimages.Doc
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
import org.domaindrivenarchitecture.provs.framework.core.platforms.UbuntuProv
import org.domaindrivenarchitecture.provs.framework.core.processors.ContainerStartMode
import org.domaindrivenarchitecture.provs.framework.core.docker.platforms.*
private const val DOCKER_NOT_SUPPORTED = "docker not yet supported for "
@ -17,7 +17,7 @@ fun Prov.dockerProvideImage(image: DockerImage, skipIfExisting: Boolean = true,
if (this is UbuntuProv) {
return this.dockerProvideImagePlatform(image, skipIfExisting, sudo)
} else {
throw RuntimeException(DOCKER_NOT_SUPPORTED + this.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) {
return this.dockerImageExistsPlatform(imageName, sudo)
} 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) {
return this.provideContainerPlatform(containerName, imageName, startMode, sudo, options, command)
} 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) {
return this.containerRunsPlatform(containerName, sudo)
} 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) {
return this.runContainerPlatform(containerName, imageName, sudo)
} 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) {
return this.exitAndRmContainerPlatform(containerName, sudo)
} 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 {
if (this is UbuntuProv) {
return this.containerExecPlatform(containerName, cmd, sudo)
} 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 {
return """
FROM ubuntu:22.04
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
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
USER $userName

View file

@ -7,10 +7,10 @@ import org.domaindrivenarchitecture.provs.framework.core.processors.Processor
const val SHELL = "/bin/bash"
open class UbuntuProv(
class UbuntuProv internal constructor(
processor: Processor = LocalProcessor(),
name: String? = null,
progressType: ProgressType = ProgressType.BASIC
progressType: ProgressType
) : Prov(processor, name, progressType) {
override fun cmd(cmd: String, dir: String?, sudo: Boolean): ProvResult = taskWithResult {
@ -30,15 +30,13 @@ open class UbuntuProv(
}
private fun buildCommand(vararg args: String): String {
return if (args.size == 1) {
return if (args.size == 1)
args[0].escapeAndEncloseByDoubleQuoteForShell()
} else {
if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) {
else
if (args.size == 3 && SHELL.equals(args[0]) && "-c".equals(args[1]))
SHELL + " -c " + args[2].escapeAndEncloseByDoubleQuoteForShell()
} else {
else
args.joinToString(separator = " ")
}
}
}
}

View file

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

View file

@ -93,9 +93,9 @@ class RemoteProcessor(val host: InetAddress, val user: String, val password: Sec
var session: Session? = null
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 err = BufferedReader(InputStreamReader(cmd.errorStream)).use { it.readText() }
cmd.join(100, TimeUnit.SECONDS)

View file

@ -201,7 +201,7 @@ fun Prov.fileContentLargeFile(file: String, sudo: Boolean = false, chunkSize: In
// check first chunk
if (resultString == null) {
if (!chunkResult.success) {
return null
return resultString
} else {
resultString = ""
}
@ -329,15 +329,11 @@ fun Prov.deleteDir(dir: String, path: String, sudo: Boolean = false): ProvResult
if ("" == path) {
throw RuntimeException("In deleteDir: path must not be empty.")
}
return if (checkDir(dir, path, sudo)) {
val cmd = "cd $path && rmdir $dir"
if (!sudo) {
cmd(cmd)
} else {
cmd(cmd.sudoizeCommand())
}
val cmd = "cd $path && rmdir $dir"
return if (!sudo) {
cmd(cmd)
} else {
ProvResult(true, out = "Dir to delete did not exist: $dir")
cmd(cmd.sudoizeCommand())
}
}

View file

@ -6,25 +6,24 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.
@Serializable
abstract class SecretSource(protected val parameter: String) {
abstract class SecretSource(protected val input: String) {
abstract fun secret() : Secret
abstract fun secretNullable() : Secret?
}
@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) {
PLAIN -> PlainSecretSource(parameter).secret()
FILE -> FileSecretSource(parameter).secret()
PLAIN -> PlainSecretSource(input).secret()
FILE -> FileSecretSource(input).secret()
PROMPT -> PromptSecretSource().secret()
PASS -> PassSecretSource(parameter).secret()
GOPASS -> GopassSecretSource(parameter).secret()
ENV -> EnvSecretSource(parameter).secret()
PASS -> PassSecretSource(input).secret()
GOPASS -> GopassSecretSource(input).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

@ -13,11 +13,11 @@ class FileSecretSource(fqFileName: String) : SecretSource(fqFileName) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "FileSecretSource", progressType = ProgressType.NONE)
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? {
val p = Prov.newInstance(name = "FileSecretSource", progressType = ProgressType.NONE)
return p.getSecret("cat " + parameter)
return p.getSecret("cat " + input)
}
}

View file

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

View file

@ -12,10 +12,10 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
class PassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
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? {
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
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) {
override fun secret(): Secret {
return Secret(parameter)
return Secret(input)
}
override fun secretNullable(): Secret {
return Secret(parameter)
return Secret(input)
}
}

View file

@ -47,7 +47,7 @@ class PasswordPanel : JPanel(FlowLayout()) {
class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
override fun secret(): Secret {
val password = PasswordPanel.requestPassword(parameter)
val password = PasswordPanel.requestPassword(input)
if (password == null) {
throw IllegalArgumentException("Failed to retrieve secret from prompting.")
} else {
@ -56,7 +56,7 @@ class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text)
}
override fun secretNullable(): Secret? {
val password = PasswordPanel.requestPassword(parameter)
val password = PasswordPanel.requestPassword(input)
return if(password == null) {
null

View file

@ -1,7 +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) =
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

@ -2,8 +2,6 @@ package org.domaindrivenarchitecture.provs.server.domain.k3s
import org.domaindrivenarchitecture.provs.configuration.infrastructure.DefaultConfigFileRepository
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfigResolved
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.provisionHetznerCSI
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.GrafanaAgentConfigResolved
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.provisionGrafanaAgent
import org.domaindrivenarchitecture.provs.server.infrastructure.*
@ -13,7 +11,6 @@ import kotlin.system.exitProcess
fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
val grafanaConfigResolved: GrafanaAgentConfigResolved? = findK8sGrafanaConfig(cli.configFileName)?.resolveSecret()
val hcloudConfigResolved: HetznerCSIConfigResolved? = findHetznerCSIConfig(cli.configFileName)?.resolveSecret()
if (cli.onlyModules == null) {
val k3sConfig: K3sConfig = getK3sConfig(cli.configFileName)
@ -21,10 +18,9 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
val k3sConfigReprovision = k3sConfig.copy(reprovision = cli.reprovision || k3sConfig.reprovision)
val applicationFile = cli.applicationFileName?.let { DefaultApplicationFileRepository(cli.applicationFileName).getFile() }
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, hcloudConfigResolved, applicationFile)
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, applicationFile)
} else {
provisionGrafana(cli.onlyModules, grafanaConfigResolved)
provisionHetznerCSI(cli.onlyModules, hcloudConfigResolved)
}
}
@ -34,7 +30,6 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
fun Prov.provisionK3s(
k3sConfig: K3sConfig,
grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
hetznerCSIConfigResolved: HetznerCSIConfigResolved? = null,
applicationFile: ApplicationFile? = null
) = task {
@ -58,10 +53,6 @@ fun Prov.provisionK3s(
provisionGrafanaAgent(grafanaConfigResolved)
}
if (hetznerCSIConfigResolved != null) {
provisionHetznerCSI(hetznerCSIConfigResolved)
}
if (applicationFile != null) {
provisionK3sApplication(applicationFile)
}
@ -69,8 +60,6 @@ fun Prov.provisionK3s(
if (!k3sConfig.reprovision) {
provisionServerCliConvenience()
}
installK9s()
}
private fun Prov.provisionGrafana(
@ -86,18 +75,3 @@ private fun Prov.provisionGrafana(
provisionGrafanaAgent(grafanaConfigResolved)
}
}
private fun Prov.provisionHetznerCSI(
onlyModules: List<String>?,
hetznerCSIConfigResolved: HetznerCSIConfigResolved?
) = task {
if (onlyModules != null && onlyModules.contains(ServerOnlyModule.HETZNER_CSI.name.lowercase())) {
if (hetznerCSIConfigResolved == null) {
println("ERROR: Could not find grafana config.")
exitProcess(7)
}
provisionHetznerCSI(hetznerCSIConfigResolved)
}
}

View file

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

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.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createFileFromResource
private const val resourcePath = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
@ -15,8 +16,7 @@ fun Prov.provisionServerCliConvenience() = task {
fun Prov.provisionKubectlCompletionAndAlias(): ProvResult = task {
cmd("kubectl completion bash | sudo tee /etc/bash_completion.d/kubectl > /dev/null")
cmd("echo 'alias k=kubectl' >> ~/.bashrc")
cmd("echo '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 {

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

@ -39,6 +39,7 @@ private val selfSignedCertificate = File(k3sManualManifestsDir, "selfsigned-cert
private val localPathProvisionerConfig = File(k3sManualManifestsDir, "local-path-provisioner-config.yaml")
// ----------------------------------- public functions --------------------------------
fun Prov.testConfigExists(): Boolean {
@ -123,7 +124,6 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
applyK3sFileFromResource(k3sMiddleWareHttpsRedirect)
}
// other
applyK3sFileFromResource(localPathProvisionerConfig)
cmd("kubectl set env deployment -n kube-system local-path-provisioner DEPLOY_DATE=\"$(date)\"", sudo = true)

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

@ -1,5 +1,10 @@
package org.domaindrivenarchitecture.provs.syspec.infrastructure
import aws.sdk.kotlin.services.s3.S3Client
import aws.sdk.kotlin.services.s3.model.ListObjectsRequest
import aws.sdk.kotlin.services.s3.model.ListObjectsResponse
import aws.smithy.kotlin.runtime.time.Instant
import kotlinx.coroutines.runBlocking
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkDir
@ -8,6 +13,7 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackag
import org.domaindrivenarchitecture.provs.syspec.domain.*
import java.text.ParseException
import java.text.SimpleDateFormat
import java.time.Duration
import java.util.*
import java.util.concurrent.TimeUnit
@ -23,6 +29,7 @@ fun Prov.verifySpecConfig(conf: SyspecConfig) = task {
conf.netcat?.let { task("NetcatSpecs") { for (spec in conf.netcat) verify(spec) } }
conf.socket?.let { task("SocketSpecs") { for (spec in conf.socket) verify(spec) } }
conf.certificate?.let { task("CertificateFileSpecs") { for (spec in conf.certificate) verify(spec) } }
conf.s3?.let { task("CertificateFileSpecs") { for (spec in conf.s3) verify(spec) } }
}
// ------------------------------- verification functions for individual specs --------------------------------
@ -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 ---------------------------------
@ -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() {
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}
kubectl config use-context ${cluster_name}

View file

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

View file

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

View file

@ -2,8 +2,9 @@ kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: echo-ingress
annotations:
kubernetes.io/ingress.class: "traefik"
spec:
ingressClassName: traefik
rules:
- http:
paths:

View file

@ -3,9 +3,9 @@ apiVersion: networking.k8s.io/v1
metadata:
name: echo-ingress
annotations:
kubernetes.io/ingress.class: "traefik"
cert-manager.io/cluster-issuer: ${issuer_name}
spec:
ingressClassName: traefik
rules:
- host: ${fqdn}
http:

View file

@ -41,5 +41,3 @@ spec:
effect: "NoSchedule"
service:
ipFamilyPolicy: "PreferDualStack"
annotations:
metallb.universe.tf/allow-shared-ip: shared-ip-service-group

View file

@ -24,7 +24,7 @@ class ProvWithSudoKtTest {
fun test_ensureSudoWithoutPassword_local_Prov() {
mockkStatic(::getPasswordToConfigureSudoWithoutPassword)
every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("testuser")
every { getPasswordToConfigureSudoWithoutPassword() } returns Secret("testuserpw")
// given
val containerName = "prov-test-sudo-no-pw"
@ -58,7 +58,7 @@ class ProvWithSudoKtTest {
// given
val containerName = "prov-test-sudo-no-pw-ssh"
val password = Secret("testuser")
val password = Secret("testuserpw")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(

View file

@ -1,7 +1,6 @@
package org.domaindrivenarchitecture.provs.desktop.domain
import io.mockk.*
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.desktop.infrastructure.installPpaFirefox
import org.domaindrivenarchitecture.provs.desktop.infrastructure.verifyIdeSetup
import org.domaindrivenarchitecture.provs.desktop.infrastructure.verifyOfficeSetup
@ -38,8 +37,10 @@ internal class DesktopServiceKtTest {
// when
Assertions.assertThrows(Exception::class.java) {
prov.provisionDesktopCommand(
DesktopCliCommand(DesktopType.BASIC, TargetCliCommand("testuser@somehost"), null), DesktopConfig() // dummy data
prov.provisionDesktop(
DesktopType.BASIC,
gitUserName = "testuser",
gitEmail = "testuser@test.org",
)
}
}

View file

@ -1,7 +1,10 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.getResourceAsText
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.createDirs
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.fileContainsText
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
@ -45,17 +48,4 @@ internal class DevOpsKtTest {
// then
assertTrue(res.success)
}
@ExtensiveContainerTest
fun installKubeconform() {
// given
val prov = defaultTestContainer()
// when
val res = prov.installKubeconform()
// then
assertTrue(res.success)
assertTrue(prov.checkFile("/usr/local/bin/kubeconform"))
}
}

View file

@ -1,21 +1,17 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.KeyPair
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.configureGpgKeys
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.GopassSecretSource
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.privateGPGSnakeoilKey
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeCurrentUserSudoerWithoutPasswordRequired
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.domaindrivenarchitecture.provs.test_keys.publicGPGSnakeoilKey
import org.junit.jupiter.api.Assertions.assertFalse
@ -60,32 +56,24 @@ internal class GopassKtTest {
}
@Test
@Disabled // This is an integration test, which needs preparation:
// Pls change user, host and remote connection (choose connection either by password or by ssh key)
// then remove tag @Disabled to be able to run this test.
// PREREQUISITE: remote machine needs openssh-server installed
@Disabled // Integrationtest; change user, host and keys, then remove this line to run this test
fun test_install_and_configure_Gopass_and_GopassBridgeJsonApi() {
// host and user
val host = "192.168.56.154"
// settings to change
val host = "192.168.56.135"
val user = "xxx"
val pubKey = GopassSecretSource("path-to/pub.key").secret()
val privateKey = GopassSecretSource("path-to/priv.key").secret()
// connection by password
val pw = PromptSecretSource("Pw for $user").secret()
val prov = remote(host, user, pw)
prov.makeCurrentUserSudoerWithoutPasswordRequired(pw) // may be commented out if user can already sudo without password
// or alternatively use connection by ssh key if the public key is already available remotely
// val prov = remote(host, user)
val pubKey = Secret(publicGPGSnakeoilKey())
val privateKey = Secret(privateGPGSnakeoilKey())
// given
val prov = remote(host, user)
// when
val res = prov.task {
configureGpgKeys(
KeyPair(pubKey, privateKey),
KeyPair(
pubKey,
privateKey
),
trust = true,
skipIfExistin = true
)

View file

@ -1,27 +0,0 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.*
class GraalVMKtTest {
@ExtensiveContainerTest
fun installGraalVM() {
// given
val prov = defaultTestContainer()
// when
val res = prov.task {
installGraalVM()
installGraalVM() // test repeatability
}
// then
assertTrue(res.success)
assertTrue(GRAAL_VM_VERSION == prov.graalVMVersion())
assertTrue(prov.checkFile("/usr/local/bin/native-image"))
}
}

View file

@ -1,42 +0,0 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.docker.exitAndRmContainer
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class HugoTest {
@ExtensiveContainerTest
fun test_installHugoByDeb() {
// given
local().exitAndRmContainer("provs_test")
val prov = defaultTestContainer()
// when
val res = prov.installHugoByDeb()
// then
assertTrue(res.success)
}
@Test
fun test_needsHugoInstall() {
// given
val hugoNull = null
val hugoLow = "hugo v0.0.0-abc+extended linux/amd64 BuildDate=0000-00-00 VendorInfo=snap:0.0.0"
val hugoMajHigh = "hugo v1.0.0-abc+extended linux/amd64 BuildDate=0000-00-00 VendorInfo=snap:1.0.0"
val hugoMinHigh = "hugo v0.1.0-abc+extended linux/amd64 BuildDate=0000-00-00 VendorInfo=snap:0.1.0"
val hugoPatHigh = "hugo v0.0.1-abc+extended linux/amd64 BuildDate=0000-00-00 VendorInfo=snap:0.0.1"
assertTrue(needsHugoInstall(hugoNull, hugoPatHigh))
assertTrue(needsHugoInstall(hugoLow, hugoPatHigh))
assertTrue(needsHugoInstall(hugoLow, hugoMinHigh))
assertTrue(needsHugoInstall(hugoLow, hugoMajHigh))
assertFalse(needsHugoInstall(hugoMajHigh, hugoLow))
assertFalse(needsHugoInstall(hugoMinHigh, hugoLow))
assertFalse(needsHugoInstall(hugoPatHigh, hugoLow))
}
}

View file

@ -1,7 +1,9 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import com.charleskorn.kaml.InvalidPropertyValueException
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType
import org.domaindrivenarchitecture.provs.server.infrastructure.getK3sConfig
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
@ -32,7 +34,7 @@ internal class K3SDesktopConfigRepositoryKtTest {
val exception = assertThrows<InvalidPropertyValueException> {
getConfig("src/test/resources/invalid-desktop-config.yaml")
}
assertEquals("Value for 'sourceType' is invalid: Value 'xxx' is not a valid option, permitted choices are: ENV, FILE, GOPASS, PASS, PLAIN, PROMPT", exception.message)
assertEquals("Value for 'sourceType' is invalid: Value 'xxx' is not a valid option, permitted choices are: FILE, GOPASS, PASS, PLAIN, PROMPT", exception.message)
}
@Test

View file

@ -3,10 +3,8 @@ package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ExtensiveContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
internal class NextcloudClientTest {
@Disabled // test is hanging sometimes
@ExtensiveContainerTest
fun test_installNextcloudClient() {
// when

View file

@ -1,8 +1,7 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
@ -10,19 +9,14 @@ import org.junit.jupiter.api.Test
internal class VSCodeKtTest {
@Test
@Disabled("Remote testing by updating connection details below, then enable test and run it manually.")
// Remark: VSCode installs with snap, which does not run in container and cannot be tested by container test.
fun installVSCode() {
@Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker")
fun installVSC() {
// given
val prov = remote("192.168.56.153", "xx", PromptSecretSource("Remote password").secret()) // machine needs openssh-server installed and sudo possible without pw
prov.aptInstall("xvfb libgbm-dev libasound2")
val a = defaultTestContainer()
a.aptInstall("xvfb libgbm-dev libasound2")
// when
val res = prov.task {
installVSCode("python", "clojure")
cmd("code -v")
cmd("codium -v")
}
val res = a.installVSC("python", "clojure")
// then
assertTrue(res.success)

View file

@ -103,8 +103,7 @@ internal class UbuntuProvTest {
// then
assertFalse(result.success)
val expectedMsg = "a password is required"
assertTrue(result.err?.contains(expectedMsg) ?: false, "Error: [$expectedMsg] is not found in [${result.err}]")
assertEquals("sudo: no tty present and no askpass program specified\n", result.err)
}
}
@ -120,12 +119,12 @@ class UbuntuUserNeedsPasswordForSudo(private val userName: String = "testuser")
override fun imageText(): String {
return """
FROM ubuntu:22.04
FROM ubuntu:18.04
ARG DEBIAN_FRONTEND=noninteractive
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
USER $userName
CMD /bin/bash

View file

@ -34,7 +34,7 @@ class ContainerUbuntuHostProcessorTest {
// given
val containerName = "prov-test-ssh-with-container"
val password = Secret("testuser")
val password = Secret("testuserpw")
val prov = Prov.newInstance(
ContainerUbuntuHostProcessor(
@ -57,8 +57,8 @@ class ContainerUbuntuHostProcessorTest {
val remoteProvBySsh = remote(ipOfContainer, "testuser", password)
// when
val firstSessionResult = remoteProvBySsh.cmd("echo 1") // connect (to container) by ssh via ip
val secondSessionResult = remoteProvBySsh.cmd("echo 2") // second connect after first connection has been closed
val firstSessionResult = remoteProvBySsh.cmd("echo 1")
val secondSessionResult = remoteProvBySsh.cmd("echo 1")
// then
assertTrue(firstSessionResult.success)

View file

@ -6,7 +6,6 @@ import org.domaindrivenarchitecture.provs.framework.core.escapeProcentForPrintf
import org.domaindrivenarchitecture.provs.framework.core.escapeSingleQuoteForShell
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.nio.file.Paths
internal class LocalProcessorTest {
@ -25,18 +24,6 @@ internal class LocalProcessorTest {
assertTrue(res.out == text)
}
@Test
fun cmd_in_folder_where_program_was_started() {
// given
val prov = Prov.newInstance(LocalProcessor(false))
// when
val pwd = prov.cmd("pwd").outTrimmed
// then
assertEquals(Paths.get("").toAbsolutePath().toString(), pwd)
}
@Test
fun cmd_with_nested_shell_and_printf() {
@ -72,7 +59,7 @@ internal class LocalProcessorTest {
@Test
fun cmd_forUnknownCommand_resultWithError() {
fun cmd_forUnkownCommand_resultWithError() {
// given
val prov = Prov.newInstance()

View file

@ -184,7 +184,6 @@ internal class FilesystemKtTest {
val res7 = prov.createDirs("test/testdir")
val res8 = prov.checkDir("testdir", "~/test")
prov.deleteDir("testdir", "~/test/")
val res9 = prov.deleteDir("notexistingdirdir", "~/")
// then
assertFalse(res1)
@ -195,7 +194,6 @@ internal class FilesystemKtTest {
assertFalse(res6)
assertTrue(res7.success)
assertTrue(res8)
assertTrue(res9.success)
}

View file

@ -1,14 +0,0 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
internal class EnvSecretSourceTest {
@Test
@Disabled // set env variable "envtest=envtestval" externally e.g. in IDE and run manually
fun secret() {
assertEquals("envtestval", EnvSecretSource("envtest").secret().plain())
}
}

View file

@ -1,35 +0,0 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSupplier
import org.domaindrivenarchitecture.provs.server.domain.hetzner_csi.HetznerCSIConfig
import org.domaindrivenarchitecture.provs.server.domain.k8s_grafana_agent.GrafanaAgentConfig
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
internal class HetznerCSIRepositoryKtTest {
@Test
fun findHetznerCSIConfig_returns_config() {
// when
val config = findHetznerCSIConfig(ConfigFileName("src/test/resources/k3s-server-config-with-hetzner.yaml"))
// then
assertEquals(
HetznerCSIConfig(
hcloudApiToken = SecretSupplier(SecretSourceType.GOPASS, "path/to/apitoken"),
encryptionPassphrase = SecretSupplier(SecretSourceType.GOPASS, "path/to/encryption"),
), config
)
}
@Test
fun findHetznerCSIConfig_returns_null_if_no_hetzner_data_available() {
// when
val config = findHetznerCSIConfig(ConfigFileName("src/test/resources/k3s-server-config.yaml"))
// then
assertEquals(null, config)
}
}

View file

@ -1,7 +1,11 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDir
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createSecretFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sConfig
import org.domaindrivenarchitecture.provs.server.domain.k3s.Node
import org.domaindrivenarchitecture.provs.server.domain.k3s.provisionK3s
@ -10,7 +14,6 @@ import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.lang.Thread.sleep
class K3sKtTest {
@Test // Extensive test, takes several minutes

View file

@ -1,23 +0,0 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.test.defaultTestContainer
import org.domaindrivenarchitecture.provs.test.tags.ContainerTest
import org.junit.jupiter.api.Assertions.assertTrue
class K9sKtTest {
@ContainerTest
fun installK9s() {
// given
val prov = defaultTestContainer()
// when
val res = prov.task {
installK9s()
installK9s() // test repeatability
}
// then
assertTrue(res.success)
}
}

View file

@ -1,18 +0,0 @@
fqdn: statistics.test.meissa-gmbh.de
node:
ipv4: 162.55.164.138
ipv6: 2a01:4f8:c010:672f::1
certmanager:
email: admin@meissa-gmbh.de
letsencryptEndpoint: prod
echo: true
reprovision: true
hetzner:
hcloudApiToken:
source: "GOPASS" # PLAIN, GOPASS or PROMPT
parameter: "path/to/apitoken" # the api key for the hetzner cloud
encryptionPassphrase:
source: "GOPASS" # PLAIN, GOPASS or PROMPT
parameter: "path/to/encryption" # the encryption passphrase for created volumes

View file

@ -16,9 +16,7 @@ const val defaultTestContainerName = "provs_test"
private lateinit var prov: Prov
fun defaultTestContainer(startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE): Prov {
if (!::prov.isInitialized || !testLocal().containerRuns(defaultTestContainerName) || (startMode == ContainerStartMode.CREATE_NEW_KILL_EXISTING)) {
prov = initDefaultTestContainer(startMode)
}
if (!::prov.isInitialized || !testLocal().containerRuns(defaultTestContainerName) || (startMode == ContainerStartMode.CREATE_NEW_KILL_EXISTING)) { prov = initDefaultTestContainer(startMode) }
return prov
}