Compare commits

..

No commits in common. "main" and "release-0.0.1" 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
/desktop-config.yaml
/syspec-config.yaml
/.kotlin/

View file

@ -1,112 +1,157 @@
image: openjdk:11-jdk-slim
stages:
- build
- test
- package
- publish
- release
.kotlin-job: &kotlin
image: domaindrivenarchitecture/ddadevops-kotlin:4.10.7
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .gradle/wrapper
- .gradle/caches
before_script:
- echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x gradlew
- export RELEASE_ARTIFACT_TOKEN=$MEISSA_REPO_BUERO_RW
- echo "------ commit info ---------------"
- echo $CI_COMMIT_TAG
- 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]+$/'
before_script:
- echo "---------- Start CI ----------"
- export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x gradlew
- echo "------ commit tag ---------------"
- echo $CI_COMMIT_TAG
- echo $CI_COMMIT_REF_NAME
cache:
paths:
- .gradle/wrapper
- .gradle/caches
build:
<<: *kotlin
stage: build
script:
- pyb build
- echo "---------- build stage ----------"
- ./gradlew assemble
artifacts:
paths:
- build/libs/*.jar
expire_in: 1 week
variables:
DOCKER_TLS_CERTDIR: "/certs"
test:
<<: *tag_only
stage: test
image: docker:24.0.5
image: docker:latest
services:
- docker:24.0.5-dind
- docker:dind
dependencies:
- build
before_script:
- 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:
- echo "---------- TEST -------------"
- apk update && apk add bash openjdk11 git
- apk update && apk add bash openjdk11
- export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
- docker build --pull -t "$CI_REGISTRY_IMAGE" .
- docker run --privileged -dit --name provs_test -v /var/run/docker.sock:/var/run/docker.sock $CI_REGISTRY_IMAGE
- docker inspect -f '{{.State.Running}}' provs_test
- ./gradlew -x assemble test -Dtestdockerwithoutsudo=true -DexcludeTags=extensivecontainertest,nonci
- ./gradlew -x assemble test -Dtestdockerwithoutsudo=true -DexcludeTags=extensivecontainertest
artifacts:
when: on_failure
paths:
- build/reports/*
- build/reports/tests/test
reports:
junit: build/test-results/test/TEST-*.xml
package:
<<: *kotlin
<<: *tag_only
.fatjars:
stage: package
rules:
# Do no allow manually triggered pipelines to prevent duplicates!
# Instead rerun the pipeline created with the last push
- if: $CI_PIPELINE_SOURCE != "push"
when: never
# Only execute when a valid version tag like v1.0, 2.3 or similar is given
# Required is always one point like 1.0
- if: $CI_COMMIT_TAG =~ /^v[0-9]+[.][0-9]+([.][0-9]+)?$/
before_script:
- echo $CI_JOB_ID
# Writing FATJAR_JOB_ID variable to environment file, as variable is needed in the release stage.
- echo FATJAR_JOB_ID=$CI_JOB_ID >> generate_executables.env
script:
- 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:
paths:
- build/libs/*.jar
- build/libs/*.lst
publish-maven-package-to-gitlab:
<<: *kotlin
<<: *tag_only
publish-released-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 publishLibraryPublicationToGitlabRepository
- ./gradlew -x assemble -x test jar
- ./gradlew -x assemble -x test publish
artifacts:
paths:
- build/libs/*.jar
publish-maven-package-to-meissa:
<<: *kotlin
<<: *tag_only
release:
image: registry.gitlab.com/gitlab-org/release-cli:latest
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:
- ./gradlew -x assemble -x test publishLibraryPublicationToMeissaRepository
release-to-meissa:
<<: *kotlin
<<: *tag_only
stage: release
allow_failure: true
script:
- pyb publish_release
- apk --no-cache add curl
- |
release-cli create --name "Release $CI_COMMIT_TAG" --tag-name $CI_COMMIT_TAG \
--assets-link "{\"name\":\"provs-desktop.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-desktop.jar\"}" \
--assets-link "{\"name\":\"provs-server.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-server.jar\"}" \
--assets-link "{\"name\":\"provs-syspec.jar\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/provs-syspec.jar\"}" \
--assets-link "{\"name\":\"sha256sum.lst\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/sha256sum.lst\"}" \
--assets-link "{\"name\":\"sha512sum.lst\",\"url\":\"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/${CI_JOB_ID}/artifacts/file/build/libs/sha512sum.lst\"}" \
after_script:
- echo "---------- End CI ----------"

View file

@ -1,8 +1,8 @@
<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" />
<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" />
<method v="2">
<option name="Make" enabled="true" />

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,9 @@
# 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:testuser" | 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
@ -61,6 +60,7 @@ After having installed `provs-desktop.jar` (see prerequisites) execute:
* `-o` for only executing one action, e.g.
* `-o verify` for verifying your installation
* `-o firefox` to install firefox from apt on ubuntu
* `-o teams` to install MS-Teams
#### Example
@ -90,39 +90,6 @@ node:
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:
```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
```
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
@ -184,27 +145,3 @@ 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:
* 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 {
ext.kotlin_version_no = "1.8.20"
ext.kotlin_version = "1.7.0"
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
repositories {
mavenCentral()
repositories { mavenCentral() }
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}
plugins {
id "org.jetbrains.kotlin.jvm" version "$kotlin_version_no"
id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlin_version_no"
id "java"
id "java-test-fixtures"
}
apply plugin: "org.jetbrains.kotlin.jvm"
apply plugin: "java-library"
apply plugin: "java-test-fixtures"
apply plugin: "maven-publish"
apply plugin: "kotlinx-serialization"
version = "0.39.5-SNAPSHOT"
group = "org.domaindrivenarchitecture.provs"
version = "0.17.1-SNAPSHOT"
repositories {
mavenCentral()
@ -26,15 +26,12 @@ repositories {
java {
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
withSourcesJar()
withJavadocJar()
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
test {
// set properties for the tests
def propertiesForTests = ["testdockerwithoutsudo"]
@ -60,28 +57,39 @@ compileJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestFixturesJava.options.debugOptions.debugLevel = "source,lines,vars"
compileTestJava.options.debugOptions.debugLevel = "source,lines,vars"
// https://stackoverflow.com/questions/21904269/configure-gradle-to-publish-sources-and-javadoc
java {
withSourcesJar()
withJavadocJar()
}
dependencies {
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('com.charleskorn.kaml:kaml:0.54.0')
api('com.charleskorn.kaml:kaml:0.43.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")
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")
}
tasks.register('uberjarDesktop', Jar) {
task uberjarDesktop(type: Jar) {
from sourceSets.main.output
@ -102,7 +110,7 @@ tasks.register('uberjarDesktop', Jar) {
}
tasks.register('uberjarServer', Jar) {
task uberjarServer(type: Jar) {
from sourceSets.main.output
@ -123,7 +131,7 @@ tasks.register('uberjarServer', Jar) {
}
tasks.register('uberjarSyspec', Jar) {
task uberjarSyspec(type: Jar) {
from sourceSets.main.output
@ -147,7 +155,7 @@ def projectRoot = rootProject.projectDir
// copy jar to /usr/local/bin and make it executable
// Remark: to be able to use it you must have jarwrapper installed (sudo apt install jarwrapper)
tasks.register('installlocally') {
task installlocally {
dependsOn(uberjarServer, uberjarDesktop, uberjarSyspec)
doLast {
exec { commandLine("sh", "-c", "sudo apt-get update & sudo apt-get install jarwrapper") }
@ -160,94 +168,34 @@ tasks.register('installlocally') {
}
}
// create binaries and install into /usr/local/bin
// PREREQUISITE: graalvm / native-image must be installed - see https://www.graalvm.org/
// 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/") }
}
task sourceJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
archiveClassifier.set("sources")
}
// publish to repo.prod.meissa.de with task "publishLibraryPublicationToMeissaRepository" -- (using pattern "publishLibraryPublicationTo<MAVEN REPOSITORY NAME>Repository")
publishing {
publications {
library(MavenPublication) {
groupId 'org.domaindrivenarchitecture'
artifactId 'provs'
from components.java
}
}
repositories {
maven {
name = "gitlab"
url = "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
credentials(HttpHeaderCredentials) {
name = "Job-Token"
value = System.getenv("CI_JOB_TOKEN")
}
authentication {
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 {
value = "token " + tokenFromEnv
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)
}
if (System.getenv("CI_JOB_TOKEN") != null) {
// see https://docs.gitlab.com/ee/user/packages/maven_repository/index.html
maven {
url "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/packages/maven"
name "GitLab"
credentials(HttpHeaderCredentials) {
name = "Job-Token"
value = System.getenv("CI_JOB_TOKEN")
}
authentication {
header(HttpHeaderAuthentication)
}
}
authentication {
header(HttpHeaderAuthentication)
}
} else {
mavenLocal()
}
}
}
// 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.
@ -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:
Find below an example of a sequence diagram when provisioning a desktop workplace:
[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.
![img.png](resources/provision-workplace-sequence.diagram.png)

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
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**.
@ -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
## 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.
**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]

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.ArgType
import kotlinx.cli.default
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
open class CliTargetParser(name: String) : ArgParser(name) {
val target by argument(
@ -16,3 +17,13 @@ open class CliTargetParser(name: String) : ArgParser(name) {
"prompt for password for remote target",
).default(false)
}
fun parseTarget(
programName: String = "provs",
args: Array<String>
): TargetCliCommand {
val parser = CliTargetParser(programName)
parser.parse(args)
return TargetCliCommand(parser.target, parser.passwordInteractive)
}

View file

@ -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 {
override fun assertExists(configFileName: ConfigFileName?) {
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
import kotlinx.serialization.SerializationException
import org.domaindrivenarchitecture.provs.configuration.application.ensureSudoWithoutPassword
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopConfig
import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktopCommand
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import org.domaindrivenarchitecture.provs.framework.core.cli.printProvsVersion
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import java.io.FileNotFoundException
import java.nio.file.Files
import kotlin.io.path.Path
import kotlin.system.exitProcess
/**
@ -18,47 +11,25 @@ import kotlin.system.exitProcess
*/
fun main(args: Array<String>) {
printProvsVersion()
val cmd = CliArgumentsParser("provs-desktop.jar subcommand target").parseCommand(args)
if (!cmd.isValid()) {
println("Arguments are not valid, pls try option -h for help.")
exitProcess(1)
}
val defaultConfigFileName = "desktop-config.yaml"
val config = if ((cmd.configFile == null) && !Files.isRegularFile(Path(defaultConfigFileName))) {
println("ATTENTION: No config provided => using an empty config.")
DesktopConfig()
} else {
val configFileName = cmd.configFile?.fileName ?: defaultConfigFileName
try {
getConfig(configFileName)
} catch (_: SerializationException) {
println(
"Error: File \"${configFileName}\" has an invalid format and or invalid data."
)
null
} catch (_: FileNotFoundException) {
println(
"Error: File\u001b[31m $configFileName \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m $configFileName \u001B[0m " +
"and change the content according to your needs."
)
null
}
}
val prov = createProvInstance(cmd.target, remoteHostSetSudoWithoutPasswordRequired = true)
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)
try {
provisionDesktopCommand(prov, cmd)
} catch (e: SerializationException) {
println(
"Error: File \"${cmd.configFile?.fileName}\" has an invalid format and or invalid data.\n"
)
} catch (e: FileNotFoundException) {
println(
"Error: File\u001b[31m ${cmd.configFile?.fileName} \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m desktop-config-example.yaml \u001B[0m to file \u001B[31m ${cmd.configFile?.fileName} \u001B[0m " +
"and change the content according to your needs.\n"
)
}
}

View file

@ -1,7 +1,6 @@
package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
@ -11,7 +10,6 @@ import org.domaindrivenarchitecture.provs.desktop.domain.DesktopOnlyModule
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType
@OptIn(ExperimentalCli::class)
open class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<DesktopSubcommand> = listOf(Basic(), Office(), Ide())
@ -25,14 +23,12 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
val module = modules.first { it.parsed }
val targetCliCommand = TargetCliCommand(
target,
passwordInteractive
)
return DesktopCliCommand(
DesktopType.valueOf(module.name.uppercase()),
targetCliCommand,
TargetCliCommand(
target,
passwordInteractive
),
module.configFileName,
module.onlyModules
)
@ -59,7 +55,7 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
override fun execute() {
configFileName = cliConfigFileName?.let { ConfigFileName(it) }
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
enum class DesktopOnlyModule {
FIREFOX, VERIFY
;
fun isIn(list: List<String>): Boolean {
return list.any { it.equals(this.name, ignoreCase = true) }
}
TEAMS, FIREFOX, VERIFY
}

View file

@ -1,7 +1,5 @@
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.framework.core.Prov
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.base.gpgFingerprint
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
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()
val only = cmd.onlyModules
if (only == null) {
provisionDesktop(
cmd.type,
conf.ssh?.keyPair(),
conf.gpg?.keyPair(),
conf.gitUserName,
conf.gitEmail,
)
} else {
provisionOnlyModules(cmd.type, only)
}
prov.provisionDesktop(
cmd.type,
conf.ssh?.keyPair(),
conf.gpg?.keyPair(),
conf.gitUserName,
conf.gitEmail,
cmd.onlyModules
)
}
@ -48,122 +42,128 @@ internal fun Prov.provisionDesktop(
gpg: KeyPair? = null,
gitUserName: String? = null,
gitEmail: String? = null,
onlyModules: List<String>?
) = task {
provisionBasicDesktop(gpg, ssh, gitUserName, gitEmail)
validatePrecondition()
provisionBasicDesktop(gpg, ssh, gitUserName, gitEmail, onlyModules)
if (desktopType == DesktopType.OFFICE) {
provisionOfficeDesktop()
verifyOfficeSetup()
provisionOfficeDesktop(onlyModules)
if (onlyModules == null) {
verifyOfficeSetup()
}
}
if (desktopType == DesktopType.IDE) {
provisionOfficeDesktop()
provisionIdeDesktop()
verifyIdeSetup()
}
}
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) {
if (onlyModules == null) {
provisionOfficeDesktop()
provisionIdeDesktop()
verifyIdeSetup()
} else {
provisionIdeDesktop(onlyModules)
}
}
}
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")
}
}
fun Prov.provisionIdeDesktop(onlyModules: List<String>? = null) {
if (onlyModules == null) {
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
// DevEnvs
installDocker()
aptInstall(JAVA)
aptInstall(CLOJURE_TOOLS)
installShadowCljs()
installDevOps()
provisionPython()
// IDEs
installVSC("python", "clojure")
installIntelliJ()
} else if (onlyModules.contains(DesktopOnlyModule.VERIFY.name.lowercase())) {
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>?
) {
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")
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()) })
provisionKeys(gpg, ssh)
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
installPpaFirefox()
installGopass()
configureGopass(publicGpgKey = gpg?.publicKey)
installGopassJsonApi()
downloadGopassBridge()
installFirefox()
installGopass()
installGopassBridgeJsonApi()
downloadGopassBridge()
installRedshift()
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)
configureRedshift()
configureNoSwappiness()
configureBash()
installVirtualBoxGuestAdditions()
} else if (onlyModules.contains(DesktopOnlyModule.FIREFOX.name.lowercase())) {
installFirefox()
}
}
fun Prov.provisionIdeDesktop() {
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
// DevEnvs
installDocker()
aptInstall(JAVA)
aptInstall(CLOJURE_TOOLS)
installShadowCljs()
installDevOps()
provisionPython()
installHugoByDeb()
// IDEs
installIntelliJ()
installKubeconform()
}

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.
* 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")
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
private const val RESOURCE_PATH = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
private const val KUBE_CONFIG_CONTEXT_SCRIPT = ".bashrc.d/kubectl.sh"
private const val resourcePath = "org/domaindrivenarchitecture/provs/desktop/infrastructure"
fun Prov.installDevOps() = task {
installTerraform()
installKubectlAndTools()
installYq()
installGraalVM()
installAwsCredentials()
installDevOpsFolder()
}
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,85 +38,45 @@ fun Prov.installYq(
}
}
fun Prov.installKubectlAndTools() = task {
fun Prov.installKubectlAndTools(): ProvResult = task {
task("installKubectl") {
if (!checkFile(KUBE_CONFIG_CONTEXT_SCRIPT)) {
installKubectl()
configureKubectlBashCompletion()
val kubeConfigFile = ".bashrc.d/kubectl.sh"
if (!checkFile(kubeConfigFile)) {
// prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/
cmd("sudo apt-get update")
aptInstall("apt-transport-https ca-certificates curl")
cmd("sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg")
cmd("echo \"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main\" | sudo tee /etc/apt/sources.list.d/kubernetes.list")
// kubectl and bash completion
cmd("sudo apt update")
aptInstall("kubectl")
cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true)
createDir(".bashrc.d")
createFileFromResource(kubeConfigFile, "kubectl.sh", resourcePath)
} else {
ProvResult(true, out = "Kubectl already installed")
}
}
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 {
// 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 {
fun Prov.installDevopsScripts() {
task("install ssh helper") {
createFileFromResource(
"/usr/local/bin/sshu.sh",
"sshu.sh",
RESOURCE_PATH,
resourcePath,
"555",
sudo = true
)
createFileFromResource(
"/usr/local/bin/ssht.sh",
"ssht.sh",
RESOURCE_PATH,
resourcePath,
"555",
sudo = true
)
@ -127,7 +87,7 @@ fun Prov.installDevopsScripts() = task {
createFileFromResource(
k3sContextFile,
"k3s-create-context.sh",
RESOURCE_PATH,
resourcePath,
"555",
sudo = true
)
@ -138,14 +98,13 @@ fun Prov.installDevopsScripts() = task {
createFileFromResource(
k3sConnectFile,
"k3s-connect.sh",
RESOURCE_PATH,
resourcePath,
"555",
sudo = true
)
}
}
fun Prov.installTerraform() = task {
fun Prov.installTerraform(): ProvResult = task {
val dir = "/usr/lib/tfenv/"
if (!checkDir(dir)) {
@ -155,6 +114,47 @@ fun Prov.installTerraform() = task {
cmd("ln -s " + dir + "bin/* /usr/local/bin", sudo = true)
}
cmd("tfenv install", sudo = true)
cmd("tfenv install latest:^1.4.6", sudo = true)
cmd("tfenv use latest:^1.4.6", sudo = true)
cmd("tfenv install latest:^1.0.8", 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
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.createFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstallFromPpa
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.addTextToFile
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import java.io.File
/**
* 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"
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") {
if (chk("snap list | grep firefox")) {
cmd("snap remove firefox", sudo = true)
}
aptInstall("software-properties-common")
cmd("add-apt-repository -y ppa:mozillateam/ppa", sudo = true)
createFile("/etc/apt/preferences.d/mozillateam", mozillaTeamFileContent, sudo = true)
aptInstallFromPpa("mozillateam", "ppa", "firefox")
createFile(
unattendeUpgradesForPpaFirefox,
"Unattended-Upgrade::Allowed-Origins:: \"LP-PPA-mozillateam:\${distro_codename}\";\n",
addTextToFile(
"\nPackage: *\n" +
"Pin: release o=LP-PPA-mozillateam\n" +
"Pin-Priority: 1001\n",
File("/etc/apt/preferences.d/mozilla-firefox"),
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.ProvResult
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerprint
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
fun Prov.installGopass(
version: String = "1.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,
// from https://github.com/gopasspw/gopass/releases/tag/v1.15.13
sha256sum: String = "409ed5617e64fa2c781d5e2807ba7fcd65bc383a4e110f410f90b590e51aec55"
sha256sum: String = "0824d5110ff1e68bff1ba10c1be63acb67cb1ad8e3bccddd6b6fc989608beca8" // checksum for sha256sum version 8.30 (e.g. ubuntu 20.04)
) = taskWithResult {
if (isPackageInstalled("gopass") && !enforceVersion) {
@ -37,34 +34,29 @@ fun Prov.installGopass(
if (result.success) {
cmd("sudo dpkg -i $path/gopass_${version}_linux_amd64.deb")
// Cross-check if installation was successful
return@taskWithResult ProvResult(checkGopassVersion(version))
addResultToEval(ProvResult(checkGopassVersion(version)))
} 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 ((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")){
if (checkFile(configFile)) {
return@taskWithResult ProvResult(true, out = "Gopass already configured in file $configFile")
}
val defaultRootFolder = userHome() + ".password-store"
val gopassRoot = gopassRootFolder ?: defaultRootFolder
// initialize root store
val fingerprint = publicGpgKey?.let { gpgFingerprint(it.plain()) }
gopassInitStoreFolder(gopassRoot, fingerprint)
if ((gopassRootFolder != null) && (!gopassRootFolder.startsWith("/"))) {
return@taskWithResult ProvResult(false, err = "Gopass cannot be initialized with a relative path or path starting with ~")
}
// use default
createDir(rootFolder)
createDirs(".config/gopass")
createFile(configFile, gopassConfig(gopassRoot))
createFile(configFile, gopassConfig(rootFolder))
// auto-completion
configureBashForUser()
@ -72,41 +64,31 @@ fun Prov.configureGopass(gopassRootFolder: String? = null, publicGpgKey: Secret?
}
fun Prov.gopassMountStore(storeName: String, path: String) = taskWithResult {
val mounts = cmdNoEval("gopass mounts").out ?: return@taskWithResult ProvResult(false, err = "could not determine gopass mounts")
if (mounts.contains(storeName)) {
ProvResult(true, out = "Store $storeName already mounted.")
} else {
cmd("gopass mounts add $storeName $path")
}
fun Prov.gopassMountStore(storeName: String, path: String) = task {
cmd("gopass mounts add $storeName $path")
}
fun Prov.gopassInitStoreFolder(path: String, gpgFingerprint: String? = null ) = task {
createFile("$path/.gpg-id", gpgFingerprint ?: "_replace_this_by_a_fingerprint_of_a_public_gpg_key_")
if (!checkDir(".git", path)) {
cmd("git init", path)
}
@Suppress("unused")
fun Prov.gopassInitStore(storeName: String, indexOfRecepientKey: Int = 0) = task {
cmd("printf \"$indexOfRecepientKey\\n\" | gopass init --store=$storeName")
}
internal fun gopassConfig(gopassRoot: String): String {
return """
[core]
parsing = true
exportkeys = true
autoclip = true
showsafecontent = false
nopager = false
cliptimeout = 45
notifications = true
autoimport = true
[age]
usekeychain = false
[mounts]
path = $gopassRoot
"""
.trimIndent() + "\n"
autoclip: true
autoimport: true
cliptimeout: 45
exportkeys: true
nocolor: false
nopager: false
notifications: true
parsing: true
path: $gopassRoot
safecontent: false
mounts: {}
""".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.isPackageInstalled
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
import java.io.File
fun Prov.downloadGopassBridge() = task {
// Attention: when changing the version, you also need to change the number after /file/ in the download url below
val filename = "gopass_bridge-0.9.0-fx.xpi"
val version = "0.9.0"
val filename = "gopass_bridge-${version}-fx.xpi"
val downloadDir = "${userHome()}Downloads/"
createDirs(downloadDir)
@ -18,14 +19,13 @@ fun Prov.downloadGopassBridge() = task {
"-L https://addons.mozilla.org/firefox/downloads/file/3630534/$filename",
downloadDir + filename
)
// needs manual installation with: firefox Downloads/gopass_bridge-0.X.0-fx.xpi
// needs manual installation with: firefox Downloads/gopass_bridge-0.8.0-fx.xpi
}
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"
fun Prov.installGopassBridgeJsonApi() = task {
// see https://github.com/gopasspw/gopass-jsonapi
val gopassJsonApiVersion = "1.11.1"
val requiredGopassVersion = "1.14.4"
val filename = "gopass-jsonapi_${gopassJsonApiVersion}_linux_amd64.deb"
val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassJsonApiVersion/$filename"
val downloadDir = "${userHome()}Downloads"
@ -36,7 +36,7 @@ fun Prov.installGopassJsonApi() = taskWithResult {
if (checkGopassVersion(requiredGopassVersion)) {
aptInstall("git gnupg2") // required dependencies
createDir(downloadDir)
downloadFromURL(downloadUrl, filename, downloadDir, sha256sum = sha256sum)
downloadFromURL(downloadUrl, filename, downloadDir)
cmd("dpkg -i $downloadDir/$filename", sudo = true)
} else {
ProvResult(
@ -46,56 +46,51 @@ fun Prov.installGopassJsonApi() = taskWithResult {
)
}
} else {
ProvResult(
false,
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
addResultToEval(
ProvResult(
false,
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
)
)
}
} else {
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 {
ProvResult(
false,
err = "gopass-jsonapi (version $gopassJsonApiVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
" Upgrading gopass-jsonapi is currently not supported by provs."
addResultToEval(
ProvResult(
false,
err = "gopass-jsonapi (version $gopassJsonApiVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
" Upgrading gopass-jsonapi is currently not supported by provs."
)
)
}
}
}
/**
* 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 {
fun Prov.configureGopassWrapperShForFirefox() = task {
val appArmorFile = "/etc/apparmor.d/usr.bin.firefox"
val gopassAccessPermission = "owner @{HOME}/.config/gopass/gopass_wrapper.sh Ux,"
val insertAfterText = "# per-user firefox configuration\n"
if (checkFile(appArmorFile) && !fileContainsText(appArmorFile, gopassAccessPermission, true)) {
replaceTextInFile(
appArmorFile, insertAfterText, "$insertAfterText $gopassAccessPermission\n"
if (checkFile(appArmorFile)) {
addTextToFile(
"\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")) {
// configures gopass-jsonapi for firefox and chooses default for each:
// * "Install for all users? [y/N/q]",
// * "In which path should gopass_wrapper.sh be installed? [/home/<user>/.config/gopass]"
// * "Wrapper Script for gopass_wrapper.sh ..."
//
// I.e. creates file "gopass_wrapper.sh" in "/home/<user>/.config/gopass" as well as
// the manifest file "/home/<user>/.mozilla/native-messaging-hosts/com.justwatch.gopass.json"
// configure for firefox and choose default for each:
// "Install for all users? [y/N/q]",
// "In which path should gopass_wrapper.sh be installed? [/home/testuser/.config/gopass]"
// "Wrapper Script for gopass_wrapper.sh ..."
configureGopassWrapperShForFirefox()
cmd("printf \"\\n\\n\\n\" | gopass-jsonapi configure --browser firefox")
configureApparmorForGopassWrapperShForFirefox()
} else {
ProvResult(
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 BROWSER = "chromium-browser" // firefox can be installed by installFirefox
val BROWSER = "firefox chromium-browser"
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 JAVA = "openjdk-17-jdk jarwrapper"
val JAVA = "openjdk-8-jdk openjdk-11-jdk openjdk-17-jdk jarwrapper"
val DRAWING_TOOLS = "inkscape dia openboard graphviz"
@ -39,5 +39,3 @@ val CLOJURE_TOOLS = "leiningen"
val PASSWORD_TOOLS = "pwgen"
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
fun Prov.provisionPython(venvHome: String? = "~/.venv/meissa") = task {
fun Prov.provisionPython() = task {
installPython3()
if (venvHome != null) { configureVenv(venvHome) }
installPybuilder(venvHome)
installRestClient(venvHome)
installJupyterlab(venvHome)
installLinters(venvHome)
installAsciinema(venvHome)
installPyTest(venvHome)
configureVenv()
installPybuilder()
installRestClient()
installJupyterlab()
}
fun Prov.installPython3(): ProvResult = task {
aptInstall("python3-venv python3-pip")
}
fun Prov.configureVenv(venvHome: String): ProvResult = task {
cmd("python3 -m venv $venvHome")
createSymlink(File("$venvHome/bin/activate"), File("~/.bashrc.d/venv.sh"))
pipInstall("pip --upgrade", venvHome)
fun Prov.configureVenv(): ProvResult = task {
val venvHome = "~/.venv/meissa"
cmd("python3 -m venv " + venvHome)
cmd("source " + venvHome + "/bin/activate")
createSymlink(File(venvHome + "/bin/activate"), File("~/.bashrc.d/venv.sh"))
cmd("pip3 install pip --upgrade")
}
fun Prov.installPybuilder(venvHome: String? = null): ProvResult = task {
pipInstall("pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation" +
" python_terraform dda_python_terraform boto3 pyyaml packaging inflection",
venvHome
)
pipInstall("--upgrade ddadevops", venvHome)
fun Prov.installPybuilder(): ProvResult = task {
cmd("pip3 install pybuilder ddadevops pypandoc mockito coverage unittest-xml-reporting deprecation python_terraform " +
"boto3")
}
fun Prov.installRestClient(venvHome: String? = null): ProvResult = task {
pipInstall("requests", venvHome)
fun Prov.installRestClient(): ProvResult = task {
cmd("pip3 install requests")
}
fun Prov.installJupyterlab(venvHome: String? = null): ProvResult = task {
pipInstall("jupyterlab pandas matplotlib", venvHome)
}
fun Prov.installLinters(venvHome: String? = null): ProvResult = task {
pipInstall("flake8 mypy pylint", venvHome)
}
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 && "
}
fun Prov.installJupyterlab(): ProvResult = task {
cmd("pip3 install jupyterlab pandas matplotlib")
}

View file

@ -6,33 +6,36 @@ 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 {
val clojureExtensions = setOf("betterthantomorrow.calva", "DavidAnson.vscode-markdownlint")
val pythonExtensions = setOf("ms-python.python")
fun Prov.installVSC(vararg options: String) = task {
val clojureExtensions =
arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint")
val pythonExtensions = arrayListOf("ms-python.python")
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)
}
provisionAdditionalToolsForVSCode()
}
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 +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)
for (ext in extensions) {
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
}
private fun Prov.installVSCodiumExtensions(extensions: Set<String>) = optional {
private fun Prov.installExtensionsCodium(extensions: List<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
}
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>()
/**
* 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.
*/
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 {
printDeprecationWarningIfLevel0("task")
return evaluate(ResultMode.ALL, name) { taskLambda(); ProvResult(true) }
}
/**
* 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")
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 {
printDeprecationWarningIfLevel0("requireLast")
return evaluate(ResultMode.LAST, name) { taskLambda() }
fun requireLast(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.LAST) { a() }
}
/**
* Defines a task, which always returns success.
* defines a task, which always returns success
*/
fun optional(name: String? = null, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("optional")
return evaluate(ResultMode.OPTIONAL, name) { taskLambda() }
fun optional(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.OPTIONAL) { 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 {
printDeprecationWarningIfLevel0("exitOnFailure")
return evaluate(ResultMode.FAILEXIT) { taskLambda() }
fun exitOnFailure(a: Prov.() -> ProvResult): ProvResult {
return evaluate(ResultMode.FAILEXIT) { a() }
}
/**
* Runs the provided task in the specified (running) container
*/
fun taskInContainer(containerName: String, taskLambda: Prov.() -> ProvResult): ProvResult {
printDeprecationWarningIfLevel0("taskInContainer")
runInContainerWithName = containerName
val res = evaluate(ResultMode.ALL) { taskLambda() }
runInContainerWithName = null
@ -273,8 +254,6 @@ open class Prov protected constructor(
previousLevel = -1
exit = false
initProgress()
processor.open()
}
// pre-handling
@ -333,15 +312,6 @@ open class Prov protected constructor(
internalResults[resultIndex].provResult = returnValue
// Add failure result to output if not yet included,
// which is the case if the result was not part of another subtask but created and returned by the lambda itself.
// Success results do not need to be added here as they don't change the overall success evaluation,
// whereas the failure results may have a useful error message, which should be in the output.
// Only direct result objects are added, but not result objects that were passed from a subtask as they are already handled in the subtask.
if (!resultOfTaskLambda.success && (resultIndex < internalResults.size - 1) && (resultOfTaskLambda != internalResults[resultIndex + 1].provResult)) {
internalResults.add(ResultLine(level + 1, "<<returned result>>", resultOfTaskLambda))
}
if (level == 0) {
endProgress()
processor.close()
@ -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 {
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 exit: String? = null) {
val outTrimmed: String? = out?.trim()
constructor(returnCode : Int) : this(returnCode == 0)
override fun toString(): String {

View file

@ -20,7 +20,7 @@ internal fun getCallingMethodName(): String? {
val offsetVal = 1
val exclude = arrayOf("task", "task\$default", "taskWithResult\$default", "taskWithResult", "def", "def\$default", "record", "invoke", "invoke0", "evaluate", "evaluate\$default", )
// suffixes are also ignored as method names but will be added as suffix in the evaluation results
val suffixes = arrayOf("optional", "optional\$default", "requireAll", "requireLast", "requireLast\$default", "inContainer")
val suffixes = arrayOf("optional", "requireAll", "requireLast", "inContainer")
var suffix = ""
val callingFrame = Thread.currentThread().stackTrace
@ -30,7 +30,7 @@ internal fun getCallingMethodName(): String? {
var inc = 0
while ((method in exclude) or (method in suffixes)) {
if (method in suffixes && suffix == "") {
suffix = method.split("$")[0]
suffix = method
}
inc++
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.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,63 +6,88 @@ import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.remote
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
import kotlin.system.exitProcess
/**
* Returns a Prov instance according to the targetCommand.
* 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.
*
* 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()) {
val password: Secret? = targetCommand.remoteTarget()?.password
return if (targetCommand.isValidLocalhost()) {
local()
} else if (targetCommand.isValidRemote()) {
createRemoteProvInstance(targetCommand.remoteTarget(), password)
} else {
throw IllegalArgumentException(
"Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help."
val remoteTarget = targetCommand.remoteTarget()
if (targetCommand.isValidLocalhost()) {
return local()
} else if (targetCommand.isValidRemote() && remoteTarget != null) {
return createProvInstanceRemote(
remoteTarget.host,
remoteTarget.user,
remoteTarget.password == null,
password,
remoteHostSetSudoWithoutPasswordRequired
)
} else {
throw IllegalArgumentException("Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help.")
}
} else {
println("ERROR: Invalid target (${targetCommand.target}). Please use option -h for help.")
System.out.flush()
println("Invalid command line options.\nPlease use option -h for help.")
exitProcess(1)
}
}
/**
* Wrapper for exitProcess, which allows e.g. mocking for test purposes
*/
fun quit(status: Int): Nothing {
exitProcess(status)
}
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
private fun createProvInstanceRemote(
host: String,
remoteUser: String,
sshWithKey: Boolean,
password: Secret?,
remoteHostSetSudoWithoutPasswordRequired: Boolean
): Prov {
return if (target != null) {
remote(target.host, target.user, target.password ?: password)
} else {
throw IllegalArgumentException(
"Error: no valid remote target (host & user) was specified!"
)
val prov =
if (sshWithKey) {
remote(host, remoteUser)
} else {
require(
password != null,
{ "No password available for provisioning without ssh keys. Either specify provisioning by ssh-keys or provide password." })
remote(host, remoteUser, password)
}
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 {
return PromptSecretSource("password to configure sudo without password.").secret()
// todo: consider removal as password can be retrieved by PromptSecretSource
internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? {
var password: Secret? = null
if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) {
password =
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret()
}
return password
}

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,12 +7,19 @@ 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) {
init {
val user = cmdNoLog("whoami").out?.trim()
if ("root" != user && !cmdNoLog("timeout 1 sudo id").success) {
println("IMPORTANT INFO:\nUser $user cannot sudo without entering a password, i.e. some functions may fail!\nIf you need to run functions with sudo, please ensure $user can sudo without password.")
}
}
override fun cmd(cmd: String, dir: String?, sudo: Boolean): ProvResult = taskWithResult {
exec(SHELL, "-c", commandWithDirAndSudo(cmd, dir, sudo))
}
@ -30,15 +37,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,6 +5,7 @@ import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.docker.provideContainer
import org.domaindrivenarchitecture.provs.framework.core.escapeAndEncloseByDoubleQuoteForShell
import org.domaindrivenarchitecture.provs.framework.core.platforms.SHELL
import org.domaindrivenarchitecture.provs.framework.core.tags.Api
enum class ContainerStartMode {
USE_RUNNING_ELSE_CREATE,
@ -19,24 +20,26 @@ enum class ContainerEndMode {
open class ContainerUbuntuHostProcessor(
private val containerName: String = "default_provs_container",
dockerImage: String = "ubuntu",
startMode: ContainerStartMode = ContainerStartMode.USE_RUNNING_ELSE_CREATE,
@Api // suppress false positive warning
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,
sudo: Boolean = true,
options: String = ""
@Api // suppress false positive warning
private val sudo: Boolean = true
) : Processor {
private val hostShell = "/bin/bash"
private val dockerCmd = if (sudo) "sudo docker " else "docker "
private var localExecution = LocalProcessor()
private var a = Prov.newInstance(name = "LocalProcessor for Docker operations", progressType = ProgressType.NONE)
init {
val result = a.provideContainer(containerName, dockerImage, startMode, sudo, options)
if (!result.success)
throw RuntimeException("Could not start docker image: " + result.toString(), result.exception)
val r = a.provideContainer(containerName, dockerImage, startMode, sudo)
if (!r.success)
throw RuntimeException("Could not start docker image: " + r.toString(), r.exception)
}
private val hostShell = "/bin/bash"
override fun exec(vararg args: String): ProcessResult {
return localExecution.exec(hostShell, "-c", dockerCmd + "exec $containerName " + buildCommand(*args))
}
@ -54,7 +57,7 @@ open class ContainerUbuntuHostProcessor(
return s.escapeAndEncloseByDoubleQuoteForShell()
}
private fun buildCommand(vararg args: String): String {
private fun buildCommand(vararg args: String) : String {
return if (args.size == 1) quoteString(args[0]) else
if (args.size == 3 && SHELL == args[0] && "-c" == args[1]) SHELL + " -c " + quoteString(args[2])
else args.joinToString(separator = " ")

View file

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

@ -2,13 +2,10 @@ package org.domaindrivenarchitecture.provs.framework.core.processors
interface Processor {
fun open() {
// no action needed for most processors; otherwise, overwrite this method in the implementing class
}
fun exec(vararg args: String): ProcessResult
fun execNoLog(vararg args: String): ProcessResult
fun close() {
// no action needed for most processors; 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.
* 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 {
@Suppress("JAVA_CLASS_ON_COMPANION")
private val log = LoggerFactory.getLogger(javaClass.enclosingClass)
}
private var ssh = SSHClient()
private val ssh = SSHClient()
override fun open() {
init {
try {
// always create a new instance as old one might be closed
ssh = SSHClient()
log.info("Connecting to $host with user: $user with " + if (password != null) "password" else "ssh-key")
ssh.loadKnownHosts()
// Attention: host key is not verified
ssh.addHostKeyVerifier(PromiscuousVerifier())
ssh.connectTimeout = 30000 // ms
ssh.connect(host)
if (password != null) {
@ -55,9 +50,8 @@ class RemoteProcessor(val host: InetAddress, val user: String, val password: Sec
try {
ssh.disconnect()
} finally {
val errorMag = "Error when initializing ssh (Host, username, password or ssh-key might be wrong) "
log.error(errorMag + e.message)
throw RuntimeException(errorMag, e)
log.error("Got exception when initializing ssh (Username, password or ssh-key might be wrong): " + e.message)
throw RuntimeException("Error when initializing ssh (Username, password or ssh-key might be wrong) ", e)
}
}
}
@ -93,9 +87,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 = ""
}
@ -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
val content = fileContent(file, true)
if (content != null) {
@ -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())
}
}
@ -407,7 +403,7 @@ fun Prov.fileSize(filename: String, sudo: Boolean = false): Int? {
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.ProvResult
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.")
}
} else {
// create targetPath if not yet existing
// create targetPath (if not yet existing)
if (!checkDir(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 ?: ""}")
}
}
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 (!isPackageInstalled(packages)) {
if (!aptInit) {
optional {
// may fail for some packages, but this should in general not be an issue
cmd("sudo apt-get update")
}
cmd("sudo apt-get update")
cmd("sudo apt-get install -qy apt-utils")
aptInit = true
}
@ -76,15 +73,7 @@ fun Prov.isPackageInstalled(packageName: String): Boolean {
/**
* Returns true if a package is installed else false
*/
fun Prov.checkPackageInstalled(packageName: String): ProvResult = taskWithResult {
cmd("dpkg -s $packageName")
}
/**
* Removes a package including its configuration and data file
* Removes a package including its configuration and data files
*/
@Suppress("unused") // used externally
fun Prov.aptPurge(packageName: String): Boolean {

View file

@ -1,14 +1,17 @@
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.ProvResult
import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.*
import org.domaindrivenarchitecture.provs.framework.core.echoCommandForText
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 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
*/
@ -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 {
val hostWithPotentialPort = port?.let { formatHostForKnownHostsFile(hostOrIp, port) } ?: hostOrIp
return cmdNoEval("ssh-keygen -F $hostWithPotentialPort").out?.isNotEmpty() ?: false
}
fun formatHostForKnownHostsFile(hostOrIp: String, port: Int? = null): String {
return port?.let { "[$hostOrIp]:$port" } ?: hostOrIp
fun Prov.isHostKnown(hostOrIp: String) : Boolean {
return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false
}
/**
* Adds ssh keys for specified host (which also can be an ip-address) to the 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.
* Adds ssh keys for specified host (which also can be an ip-address) to ssh-file "known_hosts"
* 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 {
val knownHostsFile = "~/.ssh/known_hosts"
if (!checkFile(knownHostsFile)) {
createDir(".ssh")
createFile(knownHostsFile, null)
fun Prov.trustHost(host: String, fingerprintsOfKeysToBeAdded: Set<String>?) = taskWithResult {
if (isHostKnown(host)) {
return@taskWithResult ProvResult(true, out = "Host already known")
}
with(knownHost) {
for (key in hostKeys) {
if (!verifyKeys) {
addTextToFile("\n${formatHostForKnownHostsFile(hostName, port)} $key\n", File(knownHostsFile))
} else {
val validKeys = findSshKeys(hostName, port)
if (validKeys?.contains(key) == true) {
val formattedHost = formatHostForKnownHostsFile(hostName, port)
addTextToFile("\n$formattedHost $key\n", File(knownHostsFile))
} else {
addResultToEval(
ProvResult(
false,
err = "The following key of host [$hostName] could not be verified successfully: " + key
)
)
if (!checkFile(KNOWN_HOSTS_FILE)) {
createDir(".ssh")
createFile(KNOWN_HOSTS_FILE, null)
}
if (fingerprintsOfKeysToBeAdded == null) {
// auto add keys
cmd("ssh-keyscan $host >> $KNOWN_HOSTS_FILE")
} else {
// logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt
val actualKeys = findSshKeys(host)
if (actualKeys == null || actualKeys.size == 0) {
return@taskWithResult ProvResult(false, out = "No valid keys found for host: $host")
}
val actualFingerprints = getFingerprintsForKeys(actualKeys)
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,
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),
* 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.
* Returns a list of valid ssh keys for the given host (host can also be an ip address)
*/
fun Prov.findSshKeys(host: String, port: Int? = null, keytype: String? = null): List<String>? {
val portOption = port?.let { " -p $port " } ?: ""
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(" ") }
private fun Prov.findSshKeys(host: String): List<String>? {
return cmd("ssh-keyscan $host 2>/dev/null").out?.split("\n")?.filter { x -> "" != x }
}
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
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

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

View file

@ -1,6 +1,5 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
@ -11,10 +10,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")
return p.getSecret("gopass show -f $input", true)
}
}

View file

@ -1,6 +1,5 @@
package org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources
import org.domaindrivenarchitecture.provs.framework.core.ProgressType
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
@ -11,11 +10,11 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSource
*/
class PassSecretSource(path: String) : SecretSource(path) {
override fun secret(): Secret {
val p = Prov.newInstance(name = "PassSecretSource", progressType = ProgressType.NONE)
return p.getSecret("pass " + parameter) ?: throw Exception("Failed to get secret.")
val p = Prov.newInstance(name = "PassSecretSource")
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)
val p = Prov.newInstance(name = "PassSecretSource")
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

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

@ -25,15 +25,15 @@ fun Prov.userExists(userName: String): Boolean {
fun Prov.createUser(
userName: String,
password: Secret? = null,
userCanSudoWithoutPassword: Boolean = false,
sudo: Boolean = false,
copyAuthorizedSshKeysFromCurrentUser: Boolean = false
): ProvResult = task {
if (!userExists(userName)) {
cmd("sudo adduser --gecos \"First Last,RoomNumber,WorkPhone,HomePhone\" --disabled-password --home /home/$userName $userName")
}
password?.let { cmdNoLog("sudo echo \"$userName:${password.plain()}\" | sudo chpasswd") } ?: ProvResult(true)
if (userCanSudoWithoutPassword) {
makeUserSudoerWithoutPasswordRequired(userName)
if (sudo) {
makeUserSudoerWithNoSudoPasswordRequired(userName)
}
val authorizedKeysFile = userHome() + ".ssh/authorized_keys"
if (copyAuthorizedSshKeysFromCurrentUser && checkFile(authorizedKeysFile)) {
@ -85,11 +85,11 @@ fun Prov.deleteUser(userName: String, deleteHomeDir: Boolean = false): ProvResul
* The current (executing) user must already be a sudoer. If he is a sudoer with password required then
* his password must be provided.
*/
fun Prov.makeUserSudoerWithoutPasswordRequired(
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(
userName: String,
password: Secret? = null,
overwriteFile: Boolean = false
): ProvResult = taskWithResult {
): ProvResult = task {
val userSudoFile = "/etc/sudoers.d/$userName"
if (!checkFile(userSudoFile) || overwriteFile) {
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.
* 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()
if (currentUser != null) {
makeUserSudoerWithoutPasswordRequired(currentUser, password, overwriteFile = true)
makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true)
} else {
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.
*/
@Suppress("unused")
fun Prov.currentUserCanSudoWithoutPassword(): Boolean {
return chk("timeout 1 sudo -kS id")
fun Prov.currentUserCanSudo(): Boolean {
return chk("timeout 1 sudo id")
}
/**
* Returns username of current user if it can be determined
*/

View file

@ -1,9 +1,6 @@
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.printProvsVersion
import org.domaindrivenarchitecture.provs.framework.core.cli.quit
import org.domaindrivenarchitecture.provs.server.domain.ServerType
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sCliCommand
import org.domaindrivenarchitecture.provs.server.domain.k3s.provisionK3sCommand
@ -18,8 +15,6 @@ import kotlin.system.exitProcess
*/
fun main(args: Array<String>) {
printProvsVersion()
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
// validate subcommand
@ -28,18 +23,16 @@ fun main(args: Array<String>) {
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
if (!cmd.isValidTarget()) {
println("Remote or localhost not valid, please try -h for help.")
quit(1)
exitProcess(1)
}
val prov = createProvInstance(cmd.target)
prov.provisionK3sCommand(cmd as K3sCliCommand)
prov.session {
ensureSudoWithoutPassword(cmd.target.remoteTarget()?.password)
provisionK3sCommand(cmd as K3sCliCommand)
}
}

View file

@ -1,7 +1,6 @@
package org.domaindrivenarchitecture.provs.server.application
import kotlinx.cli.ArgType
import kotlinx.cli.ExperimentalCli
import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
@ -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.ServerOnlyModule
@OptIn(ExperimentalCli::class)
class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<ServerSubcommand> = listOf(K3s(), K3d())
@ -86,7 +84,7 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) {
override fun execute() {
super.configFileName = cliConfigFileName?.let { ConfigFileName(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.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
class ApplicationFileName(val fileName: String) {
fun fullyQualifiedName() : String {
data class ApplicationFileName(val fileName: String) {
fun fullqualified() : String {
return File(fileName).absoluteFile.absolutePath
}
}

View file

@ -1,6 +1,5 @@
package org.domaindrivenarchitecture.provs.server.domain.k3s
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 certmanager: Certmanager? = null,
val echo: Echo? = null,
val reprovision: Reprovision = false,
val monthlyReboot: Boolean = true,
val reprovision: Reprovision = false
) {
fun isDualStack(): Boolean {
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.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.provisionGrafanaAgent
import org.domaindrivenarchitecture.provs.server.infrastructure.DefaultApplicationFileRepository
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 org.domaindrivenarchitecture.provs.server.infrastructure.*
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) {
if (cli.onlyModules == null ) {
val k3sConfig: K3sConfig = getK3sConfig(cli.configFileName)
DefaultApplicationFileRepository().assertExists(cli.applicationFileName)
DefaultConfigFileRepository().assertExists(cli.configFileName)
val k3sConfigReprovision = k3sConfig.copy(reprovision = cli.reprovision || k3sConfig.reprovision)
val applicationFile = cli.applicationFileName?.let { DefaultApplicationFileRepository(cli.applicationFileName).getFile() }
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, hcloudConfigResolved, applicationFile)
val k3sConfigReprovision = k3sConfig.copy(reprovision = cli.reprovision || k3sConfig.reprovision)
provisionK3s(k3sConfigReprovision, grafanaConfigResolved, cli.applicationFileName)
} else {
cli.onlyModules.forEach { module ->
when (module.uppercase()) {
ServerOnlyModule.MONTHLY_REBOOT.name -> scheduleMonthlyReboot()
ServerOnlyModule.HETZNER_CSI.name -> provisionHetznerCSI(hcloudConfigResolved)
ServerOnlyModule.GRAFANA.name -> provisionGrafana(grafanaConfigResolved)
}
}
provisionGrafana(cli.onlyModules, grafanaConfigResolved)
}
}
@ -51,9 +30,7 @@ fun Prov.provisionK3sCommand(cli: K3sCliCommand) = task {
fun Prov.provisionK3s(
k3sConfig: K3sConfig,
grafanaConfigResolved: GrafanaAgentConfigResolved? = null,
hetznerCSIConfigResolved: HetznerCSIConfigResolved? = null,
applicationFile: ApplicationFile? = null
) = task {
applicationFileName: ApplicationFileName? = null) = task {
if (k3sConfig.reprovision) {
deprovisionK3sInfra()
@ -75,42 +52,24 @@ fun Prov.provisionK3s(
provisionGrafanaAgent(grafanaConfigResolved)
}
if (hetznerCSIConfigResolved != null) {
provisionHetznerCSI(hetznerCSIConfigResolved)
}
if (applicationFile != null) {
provisionK3sApplication(applicationFile)
if (applicationFileName != null) {
provisionK3sApplication(applicationFileName)
}
if (!k3sConfig.reprovision) {
provisionServerCliConvenience()
}
if (k3sConfig.monthlyReboot) {
scheduleMonthlyReboot()
}
installK9s()
}
private fun Prov.provisionGrafana(
grafanaConfigResolved: GrafanaAgentConfigResolved?
) = task {
if (grafanaConfigResolved == null) {
println("ERROR: Could not find grafana config.")
exitProcess(7)
}
provisionGrafanaAgent(grafanaConfigResolved)
}
onlyModules: List<String>?,
grafanaConfigResolved: GrafanaAgentConfigResolved?) = task {
private fun Prov.provisionHetznerCSI(
hetznerCSIConfigResolved: HetznerCSIConfigResolved?
) = task {
if (hetznerCSIConfigResolved == null) {
println("ERROR: Could not find hetznerCSI config.")
if (onlyModules != null && onlyModules.contains(ServerOnlyModule.GRAFANA.name.lowercase())) {
if (grafanaConfigResolved == null) {
println("ERROR: Could not find grafana config.")
exitProcess(7)
}
provisionHetznerCSI(hetznerCSIConfigResolved)
provisionGrafanaAgent(grafanaConfigResolved)
}
}

View file

@ -1,7 +1,5 @@
package org.domaindrivenarchitecture.provs.server.domain.k3s
enum class ServerOnlyModule {
GRAFANA,
HETZNER_CSI,
MONTHLY_REBOOT,
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,30 +1,14 @@
package org.domaindrivenarchitecture.provs.server.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.getLocalFileContent
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkLocalFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFile
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileName
import org.domaindrivenarchitecture.provs.server.domain.k3s.ApplicationFileRepository
class DefaultApplicationFileRepository : ApplicationFileRepository {
class DefaultApplicationFileRepository(val applicationFileName: ApplicationFileName) : ApplicationFileRepository {
private fun assertExists(applicationFileName: String) {
if (!checkLocalFile(applicationFileName)) {
throw RuntimeException("Application file not found. Please check if path is correct.")
}
}
override fun getFile(): ApplicationFile {
assertExists(applicationFileName.fullyQualifiedName())
val applicationFileContents = getLocalFileContent(applicationFileName.fullyQualifiedName())
val applicationFile = ApplicationFile(applicationFileName, applicationFileContents)
return if (applicationFile.isValid()) {
applicationFile
} else {
throw RuntimeException("Application file was invalid.")
override fun assertExists(applicationFileName: ApplicationFileName?) {
if (applicationFileName != null && !checkLocalFile(applicationFileName.fullqualified())) {
throw RuntimeException("Application file ${applicationFileName.fileName} not found. Please check if path is correct.")
}
}
}

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.ubuntu.filesystem.base.*
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
// ----------------------------------- versions --------------------------------
// when updating this version, it is recommended to update also file k3s-install.sh as well as traefik.yaml in this repo
// (both files in: src/main/resources/org/domaindrivenarchitecture/provs/server/infrastructure/k3s/)
const val K3S_VERSION = "v1.29.1+k3s2"
const val K3S_VERSION = "v1.23.6+k3s1"
// ----------------------------------- directories --------------------------------
const val k3sManualManifestsDir = "/etc/rancher/k3s/manifests/"
@ -24,8 +25,8 @@ private const val k3sResourceDir = "org/domaindrivenarchitecture/provs/server/in
// ----------------------------------- files --------------------------------
private val k3sInstallScript = File("/usr/local/bin/k3s-install.sh")
private val k3sConfigFile = File("/etc/rancher/k3s/config.yaml")
private val k3sInstallScript = File( "/usr/local/bin/k3s-install.sh")
private val k3sConfigFile = File( "/etc/rancher/k3s/config.yaml")
private val k3sKubeConfig = File("/etc/rancher/k3s/k3s.yaml")
private val k3sTraefikWorkaround = File(k3sManualManifestsDir, "traefik.yaml")
@ -33,12 +34,12 @@ private val k3sMiddleWareHttpsRedirect = File(k3sManualManifestsDir, "middleware
private val certManagerDeployment = File(k3sManualManifestsDir, "cert-manager.yaml")
private val certManagerIssuer = File(k3sManualManifestsDir, "le-issuer.yaml")
private val k3sEchoWithTls = File(k3sManualManifestsDir, "echo-tls.yaml")
private val k3sEchoNoTls = File(k3sManualManifestsDir, "echo-no-tls.yaml")
private val k3sEcho = File(k3sManualManifestsDir, "echo.yaml")
private val selfSignedCertificate = File(k3sManualManifestsDir, "selfsigned-certificate.yaml")
private val localPathProvisionerConfig = File(k3sManualManifestsDir, "local-path-provisioner-config.yaml")
// ----------------------------------- public functions --------------------------------
fun Prov.testConfigExists(): Boolean {
@ -51,11 +52,7 @@ fun Prov.deprovisionK3sInfra() = task {
deleteFile(certManagerDeployment.path, sudo = true)
deleteFile(certManagerIssuer.path, sudo = true)
deleteFile(k3sKubeConfig.path, sudo = true)
val k3sUninstallScript = "k3s-uninstall.sh"
if (chk("which $k3sUninstallScript")) {
cmd(k3sUninstallScript)
}
cmd("k3s-uninstall.sh")
}
@ -81,35 +78,25 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
if (k3sConfig.isDualStack()) {
k3sConfigResourceFileName += ".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)
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6)
k3sConfigMap = k3sConfigMap.plus("node_ipv6" to k3sConfig.node.ipv6!!)
.plus("loopback_ipv6" to k3sConfig.loopback.ipv6!!)
} else {
k3sConfigResourceFileName += ".ipv4.template.yaml"
metallbConfigResourceFileName += ".ipv4.template.yaml"
}
createK3sFileFromResourceTemplate(
k3sConfigFile,
k3sConfigMap,
alternativeResourceTemplate = File(k3sConfigResourceFileName)
)
createK3sFileFromResourceTemplate(k3sConfigFile, k3sConfigMap, alternativeResourceTemplate = File(k3sConfigResourceFileName))
createK3sFileFromResource(k3sInstallScript, posixFilePermission = "755")
cmd("INSTALL_K3S_VERSION=$K3S_VERSION k3s-install.sh")
// metallb
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.13.7-native-manifest.yaml"))
repeatTaskUntilSuccess(10, 10) {
applyK3sFileFromResourceTemplate(
File(k3sManualManifestsDir, "metallb-config.yaml"),
k3sConfigMap,
alternativeResourceName = File(metallbConfigResourceFileName)
)
}
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-l2advertisement.yaml"))
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-namespace.yaml"))
applyK3sFileFromResource(File(k3sManualManifestsDir, "metallb-0.10.2-manifest.yaml"))
applyK3sFileFromResourceTemplate(
File(k3sManualManifestsDir, "metallb-config.yaml"),
k3sConfigMap,
alternativeResourceName = File(metallbConfigResourceFileName)
)
// traefik
if (k3sConfig.isDualStack()) {
@ -123,9 +110,8 @@ fun Prov.installK3s(k3sConfig: K3sConfig): ProvResult {
applyK3sFileFromResource(k3sMiddleWareHttpsRedirect)
}
// other
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("ln -sf $k3sKubeConfig " + k8sCredentialsDir + "admin.conf", sudo = true)
@ -151,25 +137,22 @@ fun Prov.provisionK3sCertManager(certmanager: Certmanager) = task {
}
}
fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null, withTls: Boolean = false) = task {
if (withTls) {
val endpointName = endpoint?.name?.lowercase()
fun Prov.provisionK3sEcho(fqdn: String, endpoint: CertmanagerEndpoint? = null) = task {
val endpointName = endpoint?.name?.lowercase()
val issuer = if (endpointName == null) {
createK3sFileFromResourceTemplate(selfSignedCertificate, mapOf("host" to fqdn))
"selfsigned-issuer"
} else {
endpointName
}
applyK3sFileFromResourceTemplate(k3sEchoWithTls, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
val issuer = if (endpointName == null) {
createK3sFileFromResourceTemplate(selfSignedCertificate, mapOf("host" to fqdn))
"selfsigned-issuer"
} else {
applyK3sFileFromResource(k3sEchoNoTls)
endpointName
}
applyK3sFileFromResourceTemplate(k3sEcho, mapOf("fqdn" to fqdn, "issuer_name" to issuer))
}
fun Prov.provisionK3sApplication(applicationFile: ApplicationFile) = task {
fun Prov.provisionK3sApplication(applicationFileName: ApplicationFileName) = task {
copyFileFromLocal(
fullyQualifiedLocalFilename = applicationFile.id.fullyQualifiedName(),
fullyQualifiedLocalFilename = applicationFileName.fullqualified(),
fullyQualifiedFilename = k3sManualManifestsDir + "application.yaml",
posixFilePermission = "644",
sudo = true
@ -233,5 +216,5 @@ private fun File.templateName(): String {
}
internal fun Prov.configureShellAliases() = task {
addTextToFile("\nalias k=\"sudo kubectl\"\n", File(".bash_aliases"))
addTextToFile( "\nalias k=\"sudo kubectl\"\n", File(".bash_aliases",))
}

View file

@ -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 {
if(!testNetworkExists()) {
if(k3sConfig.isDualStack()) {
require(k3sConfig.loopback.ipv6 != null)
createFileFromResourceTemplate(
loopbackFile,
"99-loopback.dual.template.yaml",
resourcePathNetwork,
mapOf("loopback_ipv4" to k3sConfig.loopback.ipv4, "loopback_ipv6" to k3sConfig.loopback.ipv6),
mapOf("loopback_ipv4" to k3sConfig.loopback.ipv4, "loopback_ipv6" to k3sConfig.loopback.ipv6!!),
"644",
sudo = true
)

View file

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

Some files were not shown because too many files have changed in this diff Show more