add code from provs-ubuntu resp. provs-ubuntu-extensions
This commit is contained in:
parent
68de42c5ae
commit
f5bf7865d5
70 changed files with 3851 additions and 2 deletions
11
build.gradle
11
build.gradle
|
@ -2,10 +2,11 @@ buildscript {
|
||||||
ext.kotlin_version = '1.4.31'
|
ext.kotlin_version = '1.4.31'
|
||||||
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
|
ext.CI_PROJECT_ID = System.env.CI_PROJECT_ID
|
||||||
|
|
||||||
repositories { jcenter() }
|
repositories { mavenCentral() }
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,9 +14,12 @@ apply plugin: 'org.jetbrains.kotlin.jvm'
|
||||||
apply plugin: 'java-library'
|
apply plugin: 'java-library'
|
||||||
apply plugin: 'java-test-fixtures'
|
apply plugin: 'java-test-fixtures'
|
||||||
apply plugin: 'maven-publish'
|
apply plugin: 'maven-publish'
|
||||||
|
//apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'kotlinx-serialization'
|
||||||
|
|
||||||
|
|
||||||
group = 'io.provs'
|
group = 'io.provs'
|
||||||
version = '0.8.12-SNAPSHOT'
|
version = '0.8.13-SNAPSHOT'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -56,6 +60,9 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||||
|
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.1.0"
|
||||||
|
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.1.0"
|
||||||
|
|
||||||
implementation group: 'com.hierynomus', name: 'sshj', version: '0.31.0'
|
implementation group: 'com.hierynomus', name: 'sshj', version: '0.31.0'
|
||||||
|
|
||||||
api "org.slf4j:slf4j-api:1.7.30"
|
api "org.slf4j:slf4j-api:1.7.30"
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package io.provs.ubuntu.extensions.demos
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.local
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.helloWorld() = def {
|
||||||
|
cmd("echo Hello world!")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
local().helloWorld()
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package io.provs.ubuntu.extensions.demos
|
||||||
|
|
||||||
|
import io.provs.*
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints some information and settings of the operating system and environment.
|
||||||
|
*
|
||||||
|
* For running locally no arguments are required.
|
||||||
|
* For running remotely either 2 or 3 arguments must be provided:
|
||||||
|
* either host and user for connection by ssh key ()
|
||||||
|
* or host, user and password for password-authenticated connection.
|
||||||
|
* E.g. 172.0.0.123 username or 172.0.0.123 username password
|
||||||
|
*/
|
||||||
|
fun main(vararg args: String) {
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
local().printInfos()
|
||||||
|
} else {
|
||||||
|
if (args.size !in 2..3) {
|
||||||
|
println("Wrong number of arguments. Please specify either host and user if connection is done by ssh key or otherwise host, user and password. E.g. 172.0.0.123 username password")
|
||||||
|
} else {
|
||||||
|
val password = if (args.size == 2) null else Secret(args[3])
|
||||||
|
remote(args[0], args[1], password = password).printInfos()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.printInfos() = def {
|
||||||
|
println("\nUbuntu Version:\n${ubuntuVersion()}")
|
||||||
|
println("\nCurrent directory:\n${currentDir()}")
|
||||||
|
println("\nTime zone:\n${timeZone()}")
|
||||||
|
|
||||||
|
val dir = cmd("pwd").out
|
||||||
|
println("\nCurrent directory: $dir")
|
||||||
|
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.ubuntuVersion(): String? {
|
||||||
|
return cmd("lsb_release -a").out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.currentDir(): String? {
|
||||||
|
return cmd("pwd").out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.timeZone(): String? {
|
||||||
|
return cmd("cat /etc/timezone").out
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_compounds.monitoring
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxHttpConf
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
|
||||||
|
import io.provs.ubuntu.extensions.server_software.prometheus.base.configurePrometheusDocker
|
||||||
|
import io.provs.ubuntu.extensions.server_software.prometheus.base.runPrometheusDocker
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.provisionMonitoring() = requireAll {
|
||||||
|
configurePrometheusDocker()
|
||||||
|
runPrometheusDocker()
|
||||||
|
provisionNginxStandAlone(NginxConf.nginxHttpConf())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_compounds.monitoring
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxAddLocation
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxCreateSelfSignedCertificate
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxHttpsConfWithLocationFiles
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
|
||||||
|
import io.provs.ubuntu.extensions.server_software.prometheus.base.prometheusNginxConfig
|
||||||
|
import io.provs.ubuntu.extensions.server_software.prometheus.provisionPrometheusDocker
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.provisionNginxMonitoring(nginxHost: String = "localhost") = def {
|
||||||
|
provisionPrometheusDocker(nginxHost)
|
||||||
|
nginxCreateSelfSignedCertificate()
|
||||||
|
provisionNginxStandAlone(NginxConf.nginxHttpsConfWithLocationFiles())
|
||||||
|
nginxAddLocation("443", nginxHost, "/prometheus", prometheusNginxConfig)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.certbot
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileExists
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions a certbot for the specified serverName and email to obtain and renew letsencrypt certificates
|
||||||
|
* Parameter can be used to specify certbot options e.g. "--nginx" to configure nginx, see https://certbot.eff.org/docs/using.html#certbot-command-line-options
|
||||||
|
*/
|
||||||
|
fun Prov.provisionCertbot(serverName: String, email: String?, additionalOptions: String? = "") = requireAll {
|
||||||
|
aptInstall("snapd")
|
||||||
|
sh("""
|
||||||
|
sudo snap install core; sudo snap refresh core
|
||||||
|
sudo snap install --classic certbot
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
if (!fileExists("/usr/bin/certbot")) {
|
||||||
|
cmd("sudo ln -s /snap/bin/certbot /usr/bin/certbot")
|
||||||
|
val emailOption = email?.let { " -m $it" } ?: "--register-unsafely-without-email"
|
||||||
|
cmd("sudo certbot $additionalOptions -n --agree-tos $emailOption -d $serverName")
|
||||||
|
} else {
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.firewall
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.saveIpTables() = requireAll {
|
||||||
|
sh("""
|
||||||
|
iptables-save > /etc/iptables/rules.v4
|
||||||
|
ip6tables-save > /etc/iptables/rules.v6
|
||||||
|
|
||||||
|
netfilter-persistent save""",
|
||||||
|
sudo = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.makeIpTablesPersistent() = requireAll {
|
||||||
|
// inspired by https://gist.github.com/alonisser/a2c19f5362c2091ac1e7
|
||||||
|
// enables iptables-persistent to be installed without manual input
|
||||||
|
sh("""
|
||||||
|
echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
|
||||||
|
echo iptables-persistent iptables-persistent/autosave_v6 boolean true | sudo debconf-set-selections
|
||||||
|
""".trimIndent())
|
||||||
|
|
||||||
|
aptInstall("iptables-persistent netfilter-persistent")
|
||||||
|
saveIpTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.resetFirewall() = requireAll {
|
||||||
|
sh("""
|
||||||
|
#!/bin/bash
|
||||||
|
sudo iptables -F
|
||||||
|
sudo iptables -X
|
||||||
|
sudo iptables -t nat -F
|
||||||
|
sudo iptables -t nat -X
|
||||||
|
sudo iptables -t mangle -F
|
||||||
|
sudo iptables -t mangle -X
|
||||||
|
|
||||||
|
# the rules allow us to reconnect by opening up all traffic.
|
||||||
|
sudo iptables -P INPUT ACCEPT
|
||||||
|
sudo iptables -P FORWARD ACCEPT
|
||||||
|
sudo iptables -P OUTPUT ACCEPT
|
||||||
|
|
||||||
|
# print out all rules to the console after running this file.
|
||||||
|
sudo iptables -nL
|
||||||
|
""", sudo = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.provisionFirewall(addNetworkProtections: Boolean = false) = requireAll {
|
||||||
|
if (addNetworkProtections) {
|
||||||
|
networkProtections()
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspired by: https://github.com/ChrisTitusTech/firewallsetup/blob/master/firewall
|
||||||
|
sh("""
|
||||||
|
# Firewall
|
||||||
|
|
||||||
|
# Accept all traffic first to avoid ssh lockdown via iptables firewall rules #
|
||||||
|
iptables -P INPUT ACCEPT
|
||||||
|
iptables -P FORWARD ACCEPT
|
||||||
|
iptables -P OUTPUT ACCEPT
|
||||||
|
|
||||||
|
# Flush all chains
|
||||||
|
iptables --flush
|
||||||
|
|
||||||
|
# Allow unlimited traffic on the loopback interface
|
||||||
|
iptables -A INPUT -i lo -j ACCEPT
|
||||||
|
iptables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
|
||||||
|
# Previously initiated and accepted exchanges bypass rule checking
|
||||||
|
# Allow unlimited outbound traffic
|
||||||
|
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
iptables -A OUTPUT -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
|
#Ratelimit SSH for attack protection
|
||||||
|
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP
|
||||||
|
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
|
||||||
|
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -j ACCEPT
|
||||||
|
|
||||||
|
# Allow http/https ports to be accessible from the outside
|
||||||
|
iptables -A INPUT -p tcp --dport 80 -m state --state NEW -j ACCEPT # http
|
||||||
|
iptables -A INPUT -p tcp --dport 443 -m state --state NEW -j ACCEPT # https
|
||||||
|
|
||||||
|
# UDP packet rule. This is just a random udp packet rule as an example only
|
||||||
|
# iptables -A INPUT -p udp --dport 5021 -m state --state NEW -j ACCEPT
|
||||||
|
|
||||||
|
# Allow pinging of your server
|
||||||
|
iptables -A INPUT -p icmp --icmp-type 8 -m state --state NEW,ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
|
|
||||||
|
# Drop all other traffic
|
||||||
|
iptables -A INPUT -j DROP
|
||||||
|
|
||||||
|
# print the activated rules to the console when script is completed
|
||||||
|
iptables -nL
|
||||||
|
|
||||||
|
# Set default policies
|
||||||
|
iptables --policy INPUT DROP
|
||||||
|
iptables --policy OUTPUT DROP
|
||||||
|
iptables --policy FORWARD DROP
|
||||||
|
""", sudo = true)
|
||||||
|
if (chk("docker -v")) {
|
||||||
|
ipTablesRecreateDockerRules()
|
||||||
|
} else {
|
||||||
|
ProvResult(true, "No need to create iptables docker rules as no docker installed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.networkProtections() = def {
|
||||||
|
sh("""
|
||||||
|
# Drop ICMP echo-request messages sent to broadcast or multicast addresses
|
||||||
|
echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
|
||||||
|
|
||||||
|
# Drop source routed packets
|
||||||
|
echo 0 > /proc/sys/net/ipv4/conf/all/accept_source_route
|
||||||
|
|
||||||
|
# Enable TCP SYN cookie protection from SYN floods
|
||||||
|
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
|
||||||
|
|
||||||
|
# Don't accept ICMP redirect messages
|
||||||
|
echo 0 > /proc/sys/net/ipv4/conf/all/accept_redirects
|
||||||
|
|
||||||
|
# Don't send ICMP redirect messages
|
||||||
|
echo 0 > /proc/sys/net/ipv4/conf/all/send_redirects
|
||||||
|
|
||||||
|
# Enable source address spoofing protection
|
||||||
|
echo 1 > /proc/sys/net/ipv4/conf/all/rp_filter
|
||||||
|
|
||||||
|
# Log packets with impossible source addresses
|
||||||
|
echo 1 > /proc/sys/net/ipv4/conf/all/log_martians
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.ipTablesRecreateDockerRules() = requireAll {
|
||||||
|
// see https://stackoverflow.com/questions/25917941/docker-how-to-re-create-dockers-additional-iptables-rules
|
||||||
|
cmd("sudo service docker restart")
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.firewall.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.saveIpTablesToFile() = def {
|
||||||
|
val dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("-yyyy-MM-dd--HH:mm:ss"))
|
||||||
|
val file = "savedrules$dateTime.txt"
|
||||||
|
sh("""
|
||||||
|
sudo iptables-save > $file
|
||||||
|
cat $file""")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.restoreIpTablesFromFile(file: String? = null) = def {
|
||||||
|
val fileName = file ?: cmd("ls -r a* | head -1\n").out
|
||||||
|
fileName?.let { cmd("sudo iptables-restore < $file") }
|
||||||
|
?: ProvResult(false, err = "File to restore not found.")
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nexus
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.docker.containerRuns
|
||||||
|
import io.provs.remote
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileExists
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.user.base.createUser
|
||||||
|
import io.provs.ubuntu.extensions.server_software.certbot.provisionCertbot
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.nginxReverseProxyHttpConfig
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions sonatype nexus in a docker container.
|
||||||
|
* If you would want nexus to be accessible directly from the internet (e.g. for test or demo reasons)
|
||||||
|
* set parameter portAccessibleFromNetwork to true.
|
||||||
|
*/
|
||||||
|
fun Prov.provisionNexusWithDocker(portAccessibleFromNetwork: Boolean = false) = requireAll {
|
||||||
|
// https://blog.sonatype.com/sonatype-nexus-installation-using-docker
|
||||||
|
// https://medium.com/@AhGh/how-to-setup-sonatype-nexus-3-repository-manager-using-docker-7ff89bc311ce
|
||||||
|
aptInstall("docker.io")
|
||||||
|
|
||||||
|
if (!containerRuns("nexus")) {
|
||||||
|
val volume = "nexus-data"
|
||||||
|
if (!cmdNoEval("docker volume inspect $volume").success) {
|
||||||
|
cmd("docker volume create --name $volume")
|
||||||
|
}
|
||||||
|
cmd("sudo docker run -d --restart unless-stopped -p 8081:8081 --name nexus -v nexus-data:/nexus-data sonatype/nexus3")
|
||||||
|
|
||||||
|
for (n in 0..3) {
|
||||||
|
if (fileExists("/var/lib/docker/volumes/$volume/_data/admin.password", sudo = true)) {
|
||||||
|
val res = cmd("sudo cat /var/lib/docker/volumes/$volume/_data/admin.password")
|
||||||
|
println("Admin Password:" + res.out)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Thread.sleep(20000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!portAccessibleFromNetwork) {
|
||||||
|
val netIf = getDefaultNetworkingInterface()
|
||||||
|
netIf?.also {
|
||||||
|
val iptablesParameters = "DOCKER-USER -i $it ! -s 127.0.0.1 -j DROP"
|
||||||
|
if (!cmdNoEval("sudo iptables -C $iptablesParameters").success) {
|
||||||
|
cmd("sudo iptables -I $iptablesParameters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProvResult(true) // dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Prov.getDefaultNetworkingInterface(): String? {
|
||||||
|
return cmd("route | grep \"^default\" | grep -o \"[^ ]*\$\"\n").out?.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions sonatype nexus on the specified host.
|
||||||
|
* Creates user "nexus" on the remote system.
|
||||||
|
* Installs nexus in a docker container behind an nginx reverse proxy with ssl using letsencrypt certificates.
|
||||||
|
*
|
||||||
|
* To run this method it is required to have ssl root access to the host.
|
||||||
|
*/
|
||||||
|
@Suppress("unused") // to be used externally
|
||||||
|
fun provisionNexusServer(serverName: String, certbotEmail: String) {
|
||||||
|
val userName = "nexus" + 7
|
||||||
|
remote(serverName, "root").def {
|
||||||
|
createUser(userName, copyAuthorizedKeysFromCurrentUser = true, sudo = true)
|
||||||
|
}
|
||||||
|
remote(serverName, userName).requireAll {
|
||||||
|
provisionNexusWithDocker()
|
||||||
|
|
||||||
|
if (provisionNginxStandAlone(NginxConf.nginxReverseProxyHttpConfig(serverName)).success) {
|
||||||
|
|
||||||
|
cmd("sudo cat /etc/nginx/nginx.conf")
|
||||||
|
|
||||||
|
provisionCertbot(serverName, certbotEmail, "--nginx")
|
||||||
|
|
||||||
|
optional {
|
||||||
|
cmd("sudo cat /etc/nginx/nginx.conf")
|
||||||
|
cmd("sudo sed -i 's/X-Forwarded-Proto \"http\"/X-Forwarded-Proto \"https\"/g' /etc/nginx/nginx.conf")
|
||||||
|
cmd("sudo systemctl reload nginx")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nexus.base
|
||||||
|
|
||||||
|
fun reverseProxyConfigHttpPort80(serverName: String): String {
|
||||||
|
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
|
||||||
|
return """
|
||||||
|
events {} # event context have to be defined to consider config valid
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
proxy_send_timeout 120;
|
||||||
|
proxy_read_timeout 300;
|
||||||
|
proxy_buffering off;
|
||||||
|
keepalive_timeout 5 5;
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name $serverName;
|
||||||
|
|
||||||
|
# allow large uploads of files
|
||||||
|
client_max_body_size 1G;
|
||||||
|
|
||||||
|
# optimize downloading files larger than 1G
|
||||||
|
#proxy_max_temp_file_size 2G;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
|
||||||
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
|
proxy_set_header Host ${'$'}host;
|
||||||
|
proxy_set_header X-Real-IP ${'$'}remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto "http";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun reverseProxyConfigSsl(serverName: String, ssl_certificate: String? = null, ssl_certificate_key: String? = null): String {
|
||||||
|
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
|
||||||
|
|
||||||
|
val sslCertificateEntry = ssl_certificate?.let { "ssl_certificate $ssl_certificate;" } ?: "ssl_certificate /etc/letsencrypt/live/$serverName/fullchain.pem;"
|
||||||
|
val sslCertificateKeyEntry = ssl_certificate?.let { "ssl_certificate_key $ssl_certificate_key;" } ?: "ssl_certificate_key /etc/letsencrypt/live/$serverName/privkey.pem"
|
||||||
|
|
||||||
|
return """
|
||||||
|
events {} # event context have to be defined to consider config valid
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
proxy_send_timeout 120;
|
||||||
|
proxy_read_timeout 300;
|
||||||
|
proxy_buffering off;
|
||||||
|
keepalive_timeout 5 5;
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen *:443 ssl;
|
||||||
|
server_name $serverName;
|
||||||
|
|
||||||
|
# allow large uploads of files
|
||||||
|
client_max_body_size 1G;
|
||||||
|
|
||||||
|
# optimize downloading files larger than 1G
|
||||||
|
# proxy_max_temp_file_size 2G;
|
||||||
|
|
||||||
|
$sslCertificateEntry
|
||||||
|
$sslCertificateKeyEntry
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
|
||||||
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
|
proxy_set_header Host ${'$'}host;
|
||||||
|
proxy_set_header X-Real-IP ${'$'}remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto "https";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.remote
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileExists
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.NginxConf
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.createNginxLocationFolders
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
|
internal const val configFile = "/etc/nginx/nginx.conf"
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.provisionNginxStandAlone(config: NginxConf? = null) = requireAll {
|
||||||
|
|
||||||
|
aptInstall("nginx")
|
||||||
|
|
||||||
|
createNginxLocationFolders()
|
||||||
|
|
||||||
|
if (config != null) {
|
||||||
|
if (fileExists(configFile)) {
|
||||||
|
cmd("sudo mv $configFile $configFile-orig")
|
||||||
|
}
|
||||||
|
createFile(configFile, config.conf, sudo = true)
|
||||||
|
val configCheck = cmd("sudo nginx -t")
|
||||||
|
if (configCheck.success) {
|
||||||
|
cmd("sudo service nginx restart")
|
||||||
|
} else {
|
||||||
|
ProvResult(false, err = "Nginx config is incorrect:\n" + configCheck.err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProvResult(true) // dummy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun provisionRemote(vararg args: String) {
|
||||||
|
if (args.size != 2) {
|
||||||
|
println("Pls specify host and user for remote installation of nginx.")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
remote(args[0], args[1]).provisionNginxStandAlone()
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
fun Prov.nginxAddBasicAuth(user: String, password: Secret) = requireAll {
|
||||||
|
aptInstall("apache2-utils")
|
||||||
|
val passwordFile = "/etc/nginx/.htpasswd"
|
||||||
|
cmdNoLog("sudo htpasswd -b -c $passwordFile $user ${password.plain()}")
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,162 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx.base
|
||||||
|
|
||||||
|
class NginxConf(val conf: String = NGINX_MINIMAL_CONF) {
|
||||||
|
companion object {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const val NGINX_MINIMAL_CONF = """
|
||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 200 'Hi from nginx!';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused") // use later
|
||||||
|
fun NginxConf.Companion.nginxHttpConf(
|
||||||
|
serverName: String = "localhost"
|
||||||
|
): NginxConf {
|
||||||
|
return NginxConf(
|
||||||
|
"""
|
||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name $serverName;
|
||||||
|
|
||||||
|
include /etc/nginx/locations-enabled/port80*$locationsFileExtension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun NginxConf.Companion.nginxHttpsConfWithLocationFiles(
|
||||||
|
sslCertificate: String = "/etc/nginx/ssl/cert/selfsigned.crt",
|
||||||
|
sslCertificateKey: String = "/etc/nginx/ssl/private/selfsigned.key"
|
||||||
|
): NginxConf {
|
||||||
|
return NginxConf(
|
||||||
|
"""
|
||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
ssl_certificate $sslCertificate;
|
||||||
|
ssl_certificate_key $sslCertificateKey;
|
||||||
|
|
||||||
|
include /etc/nginx/locations-enabled/port443*$locationsFileExtension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused") // use later
|
||||||
|
fun NginxConf.Companion.nginxReverseProxySslConfig(
|
||||||
|
serverName: String,
|
||||||
|
ssl_certificate: String? = null,
|
||||||
|
ssl_certificate_key: String? = null
|
||||||
|
): NginxConf {
|
||||||
|
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
|
||||||
|
|
||||||
|
val sslCertificateEntry = ssl_certificate?.let { "ssl_certificate $ssl_certificate;" }
|
||||||
|
?: "ssl_certificate /etc/letsencrypt/live/$serverName/fullchain.pem;"
|
||||||
|
val sslCertificateKeyEntry = ssl_certificate?.let { "ssl_certificate_key $ssl_certificate_key;" }
|
||||||
|
?: "ssl_certificate_key /etc/letsencrypt/live/$serverName/privkey.pem"
|
||||||
|
|
||||||
|
return NginxConf(
|
||||||
|
"""
|
||||||
|
events {} # event context have to be defined to consider config valid
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
proxy_send_timeout 120;
|
||||||
|
proxy_read_timeout 300;
|
||||||
|
proxy_buffering off;
|
||||||
|
keepalive_timeout 5 5;
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen *:443 ssl;
|
||||||
|
server_name $serverName;
|
||||||
|
|
||||||
|
# allow large uploads of files
|
||||||
|
client_max_body_size 1G;
|
||||||
|
|
||||||
|
# optimize downloading files larger than 1G
|
||||||
|
#proxy_max_temp_file_size 2G;
|
||||||
|
|
||||||
|
$sslCertificateEntry
|
||||||
|
$sslCertificateKeyEntry
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
|
||||||
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
|
proxy_set_header Host ${'$'}host;
|
||||||
|
proxy_set_header X-Real-IP ${'$'}remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto "https";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused") // use later
|
||||||
|
fun NginxConf.Companion.nginxReverseProxyHttpConfig(
|
||||||
|
serverName: String
|
||||||
|
): NginxConf {
|
||||||
|
// see https://help.sonatype.com/repomanager3/installation/run-behind-a-reverse-proxy
|
||||||
|
|
||||||
|
return NginxConf(
|
||||||
|
"""
|
||||||
|
events {} # event context have to be defined to consider config valid
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
proxy_send_timeout 120;
|
||||||
|
proxy_read_timeout 300;
|
||||||
|
proxy_buffering off;
|
||||||
|
keepalive_timeout 5 5;
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen *:80;
|
||||||
|
server_name $serverName;
|
||||||
|
|
||||||
|
# allow large uploads of files
|
||||||
|
client_max_body_size 1G;
|
||||||
|
|
||||||
|
# optimize downloading files larger than 1G
|
||||||
|
#proxy_max_temp_file_size 2G;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
|
||||||
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
|
proxy_set_header Host ${'$'}host;
|
||||||
|
proxy_set_header X-Real-IP ${'$'}remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For ${'$'}proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto "https";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.*
|
||||||
|
|
||||||
|
|
||||||
|
internal const val locationsAvailableDir = "/etc/nginx/locations-available/"
|
||||||
|
internal const val locationsEnabledDir = "/etc/nginx/locations-enabled/"
|
||||||
|
internal const val locationsFileExtension = ".locations"
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.createNginxLocationFolders() = requireAll {
|
||||||
|
createDirs(locationsEnabledDir, sudo = true)
|
||||||
|
createDirs(locationsAvailableDir, sudo = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.nginxIncludeLocationFolders() = requireAll {
|
||||||
|
replaceTextInFile("/etc/nginx/nginx.conf", "listen 80;\n",
|
||||||
|
"""listen 80;
|
||||||
|
include ${locationsAvailableDir}port80*$locationsFileExtension;
|
||||||
|
include ${locationsEnabledDir}port443*$locationsFileExtension;
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.nginxAddLocation(port: String, locationFileName: String, urlPath: String, content: String) = requireAll {
|
||||||
|
|
||||||
|
val locationConf = """location $urlPath {""" +
|
||||||
|
content +
|
||||||
|
"\n}"
|
||||||
|
|
||||||
|
if (!dirExists(locationsAvailableDir, sudo = true)) {
|
||||||
|
createNginxLocationFolders()
|
||||||
|
}
|
||||||
|
|
||||||
|
createFile("${locationsAvailableDir}port${port}_$locationFileName$locationsFileExtension", locationConf, sudo = true)
|
||||||
|
if (!fileExists("${locationsEnabledDir}port${port}_$locationFileName$locationsFileExtension", sudo = true)) {
|
||||||
|
cmd("sudo ln -s ${locationsAvailableDir}port${port}_$locationFileName$locationsFileExtension ${locationsEnabledDir}port${port}_$locationFileName$locationsFileExtension ")
|
||||||
|
} else {
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDirs
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
|
||||||
|
|
||||||
|
|
||||||
|
internal val certificateName = "selfsigned"
|
||||||
|
internal val sslDays = 365
|
||||||
|
val dirSslCert="/etc/nginx/ssl/cert"
|
||||||
|
val dirSslKey="/etc/nginx/ssl/private"
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.nginxCreateSelfSignedCertificate(
|
||||||
|
country: String = "DE",
|
||||||
|
state: String = "test",
|
||||||
|
locality: String = "test",
|
||||||
|
organization: String = "test",
|
||||||
|
organizationalUnit: String = "test",
|
||||||
|
commonName: String = "test",
|
||||||
|
email : String = "test@test.net"
|
||||||
|
) = def {
|
||||||
|
// inspired by https://gist.github.com/adrianorsouza/2bbfe5e197ce1c0b97c8
|
||||||
|
createDirs(dirSslCert, sudo = true)
|
||||||
|
createDirs(dirSslKey, sudo = true)
|
||||||
|
cmd("cd $dirSslKey && sudo openssl req -x509 -nodes -newkey rsa:2048 -keyout $certificateName.key -out $certificateName.crt -days $sslDays -subj \"/C=$country/ST=$state/L=$locality/O=$organization/OU=$organizationalUnit/CN=$commonName/emailAddress=$email\"")
|
||||||
|
cmd("sudo mv $dirSslKey/$certificateName.crt $dirSslCert/")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.configureNginxWithSelfSignedCertificate() = def {
|
||||||
|
// todo: should not call provisionNginxStandAlone, which is defined in the package above
|
||||||
|
provisionNginxStandAlone(NginxConf.nginxReverseProxySslConfig("localhost",
|
||||||
|
dirSslCert+"/"+ certificateName + ".crt",
|
||||||
|
dirSslKey + "/" + certificateName + ".key"))
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.prometheus
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.extensions.server_software.prometheus.base.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions prometheus monitoring.
|
||||||
|
* If running behind an nginx, pls specify the hostname in parameter nginxHost (e.g. mydomain.com).
|
||||||
|
* To run it without nodeExporter (which provides system data to prometheus), set withNodeExporter to false.
|
||||||
|
*/
|
||||||
|
fun Prov.provisionPrometheusDocker(nginxHost: String? = null, withNodeExporter: Boolean = true) = def {
|
||||||
|
configurePrometheusDocker()
|
||||||
|
if (withNodeExporter) {
|
||||||
|
installNodeExporter()
|
||||||
|
runNodeExporter()
|
||||||
|
addNodeExporterToPrometheusConf()
|
||||||
|
}
|
||||||
|
runPrometheusDocker(nginxHost)
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.prometheus.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.local
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileContainsText
|
||||||
|
import io.provs.ubuntu.filesystem.base.replaceTextInFile
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.user.base.whoami
|
||||||
|
|
||||||
|
|
||||||
|
internal val defaultInstallationDir = "/usr/local/bin/"
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.installNodeExporter() = requireAll {
|
||||||
|
// inspired by https://devopscube.com/monitor-linux-servers-prometheus-node-exporter/ and
|
||||||
|
// https://www.howtoforge.com/tutorial/how-to-install-prometheus-and-node-exporter-on-centos-8/#step-install-and-configure-nodeexporter
|
||||||
|
val downloadFileBasename = "node_exporter-1.0.1.linux-amd64"
|
||||||
|
val downloadFile = "$downloadFileBasename.tar.gz"
|
||||||
|
val downloadPath = "~/tmp/"
|
||||||
|
val fqFile = downloadPath + downloadFile
|
||||||
|
|
||||||
|
aptInstall("curl")
|
||||||
|
createDir("tmp")
|
||||||
|
sh(
|
||||||
|
"""
|
||||||
|
cd tmp && curl -LO https://github.com/prometheus/node_exporter/releases/download/v1.0.1/$downloadFile --output $downloadFile
|
||||||
|
cd tmp && tar -xvf $fqFile -C $downloadPath
|
||||||
|
|
||||||
|
sudo mv $downloadPath$downloadFileBasename/node_exporter $defaultInstallationDir"""
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.runNodeExporter() = def {
|
||||||
|
createFile("/etc/systemd/system/node_exporter.service", nodeExporterServiceConf(whoami()?:"nouserfound"), sudo = true)
|
||||||
|
|
||||||
|
sh("""
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
|
||||||
|
# start the node_exporter service and enable it to launch everytime at system startup.
|
||||||
|
sudo systemctl start node_exporter
|
||||||
|
sudo systemctl enable node_exporter
|
||||||
|
|
||||||
|
# check if running
|
||||||
|
sudo systemctl status node_exporter --no-pager -l
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.addNodeExporterToPrometheusConf (
|
||||||
|
prometheusConf: String = "/etc/prometheus/prometheus.yml",
|
||||||
|
sudo: Boolean = true
|
||||||
|
) = requireAll {
|
||||||
|
val prometheusConfNodeExporter = """
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: 'node_exporter'
|
||||||
|
static_configs:
|
||||||
|
- targets: ['172.17.0.1:9100']
|
||||||
|
"""
|
||||||
|
if (!fileContainsText(prometheusConf, "- job_name: 'node_exporter'", sudo)) {
|
||||||
|
replaceTextInFile(prometheusConf, "\nscrape_configs:\n", prometheusConfNodeExporter)
|
||||||
|
}
|
||||||
|
// cmd("sudo systemctl restart prometheus") for standalone
|
||||||
|
cmd("sudo docker restart prometheus")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun nodeExporterServiceConf(user: String, installationDir: String = defaultInstallationDir): String {
|
||||||
|
return """
|
||||||
|
[Unit]
|
||||||
|
Description=Node Exporter
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=$user
|
||||||
|
ExecStart=${installationDir}node_exporter
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
|
"""
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.prometheus.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.docker.containerRuns
|
||||||
|
import io.provs.local
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDirs
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
|
||||||
|
internal val configDir = "/etc/prometheus/"
|
||||||
|
internal val configFile = "prometheus.yml"
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.configurePrometheusDocker(config: String = prometheusDefaultConfig) = requireAll {
|
||||||
|
createDirs(configDir, sudo = true)
|
||||||
|
createFile(configDir + configFile, config, sudo = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.runPrometheusDocker(nginxHost: String? = null) = requireAll {
|
||||||
|
aptInstall("docker.io")
|
||||||
|
|
||||||
|
val containerName = "prometheus"
|
||||||
|
|
||||||
|
if (containerRuns(containerName)) {
|
||||||
|
cmd("sudo docker restart $containerName")
|
||||||
|
} else {
|
||||||
|
if (nginxHost == null) {
|
||||||
|
cmd(
|
||||||
|
"sudo docker run -d -p 9090:9090 " +
|
||||||
|
" --name $containerName " +
|
||||||
|
" --restart on-failure:1" +
|
||||||
|
" -v prometheus-data:/prometheus" +
|
||||||
|
" -v $configDir$configFile:/etc/prometheus/prometheus.yml " +
|
||||||
|
" prom/prometheus"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cmd(
|
||||||
|
"sudo docker run -d -p 9090:9090 " +
|
||||||
|
" --name $containerName " +
|
||||||
|
" --restart on-failure:1" +
|
||||||
|
" -v prometheus-data:/prometheus" +
|
||||||
|
" -v $configDir$configFile:/etc/prometheus/prometheus.yml " +
|
||||||
|
" prom/prometheus --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus " +
|
||||||
|
"--web.console.libraries=/usr/share/prometheus/console_libraries " +
|
||||||
|
"--web.console.templates=/usr/share/prometheus/consoles " +
|
||||||
|
"--web.external-url=http://$nginxHost/prometheus"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private const val prometheusDefaultConfig =
|
||||||
|
"""
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s # By default, scrape targets every 15 seconds.
|
||||||
|
|
||||||
|
# Attach these labels to any time series or alerts when communicating with
|
||||||
|
# external systems (federation, remote storage, Alertmanager).
|
||||||
|
external_labels:
|
||||||
|
monitor: 'codelab-monitor'
|
||||||
|
|
||||||
|
# A scrape configuration containing exactly one endpoint to scrape:
|
||||||
|
# Here it's Prometheus itself.
|
||||||
|
scrape_configs:
|
||||||
|
# The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
|
||||||
|
- job_name: 'prometheus'
|
||||||
|
|
||||||
|
# Override the global default and scrape targets from this job every 5 seconds.
|
||||||
|
scrape_interval: 5s
|
||||||
|
|
||||||
|
static_configs:
|
||||||
|
- targets: ['localhost:9090']
|
||||||
|
"""
|
|
@ -0,0 +1,5 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.prometheus.base
|
||||||
|
|
||||||
|
val prometheusNginxConfig = """
|
||||||
|
proxy_pass http://localhost:9090/prometheus;
|
||||||
|
"""
|
|
@ -0,0 +1,155 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace
|
||||||
|
|
||||||
|
import io.provs.*
|
||||||
|
import io.provs.processors.RemoteProcessor
|
||||||
|
import io.provs.ubuntu.extensions.workplace.base.*
|
||||||
|
import io.provs.ubuntu.git.provisionGit
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.install.base.aptInstallFromPpa
|
||||||
|
import io.provs.ubuntu.install.base.aptPurge
|
||||||
|
import io.provs.ubuntu.keys.KeyPair
|
||||||
|
import io.provs.ubuntu.keys.base.gpgFingerprint
|
||||||
|
import io.provs.ubuntu.keys.provisionKeysCurrentUser
|
||||||
|
import io.provs.ubuntu.secret.secretSources.PromptSecretSource
|
||||||
|
import io.provs.ubuntu.user.base.currentUserCanSudo
|
||||||
|
import io.provs.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired
|
||||||
|
import io.provs.ubuntu.user.base.whoami
|
||||||
|
import java.net.InetAddress
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
|
||||||
|
enum class WorkplaceType {
|
||||||
|
MINIMAL, OFFICE, IDE
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions software and configurations for a personal workplace.
|
||||||
|
* Offers the possibility to choose between different types.
|
||||||
|
* Type OFFICE installs office-related software like Thunderbird, LibreOffice, and much more.
|
||||||
|
* Type IDE provides additional software for a development environment, such as Visual Studio Code, IntelliJ, etc.
|
||||||
|
*
|
||||||
|
* Prerequisites: user must be sudoer. If password is required for user to execute sudo, then also parameter userPassword must be provided
|
||||||
|
*
|
||||||
|
* @param workplaceType
|
||||||
|
* @param userPassword only needs to be provided if user cannot sudo without password
|
||||||
|
*/
|
||||||
|
fun Prov.provisionWorkplace(
|
||||||
|
workplaceType: WorkplaceType = WorkplaceType.MINIMAL,
|
||||||
|
ssh: KeyPair? = null,
|
||||||
|
gpg: KeyPair? = null,
|
||||||
|
gitUserName: String? = null,
|
||||||
|
gitEmail: String? = null,
|
||||||
|
userPassword: Secret? = null
|
||||||
|
) = requireAll {
|
||||||
|
|
||||||
|
userPassword?.also { makeUserSudoerWithNoSudoPasswordRequired(it) }
|
||||||
|
|
||||||
|
if (!currentUserCanSudo()) {
|
||||||
|
throw Exception("Current user ${whoami()} cannot execute sudo without a password, but he must be able to in order to provisionWorkplace")
|
||||||
|
}
|
||||||
|
|
||||||
|
aptInstall("ssh gnupg curl git")
|
||||||
|
|
||||||
|
provisionKeysCurrentUser(gpg, ssh)
|
||||||
|
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
|
||||||
|
|
||||||
|
installVirtualBoxGuestAdditions()
|
||||||
|
|
||||||
|
aptPurge("remove-power-management xfce4-power-manager " +
|
||||||
|
"xfce4-power-manager-plugins xfce4-power-manager-data")
|
||||||
|
aptPurge("abiword gnumeric")
|
||||||
|
aptPurge("popularity-contest")
|
||||||
|
|
||||||
|
configureNoSwappiness()
|
||||||
|
|
||||||
|
if (workplaceType == WorkplaceType.OFFICE || workplaceType == WorkplaceType.IDE) {
|
||||||
|
aptInstall("seahorse")
|
||||||
|
aptInstall(BASH_UTILS)
|
||||||
|
aptInstall(OS_ANALYSIS)
|
||||||
|
aptInstall(ZIP_UTILS)
|
||||||
|
|
||||||
|
aptInstall("firefox chromium-browser")
|
||||||
|
aptInstall("thunderbird libreoffice")
|
||||||
|
aptInstall("xclip")
|
||||||
|
|
||||||
|
installZimWiki()
|
||||||
|
installGopass()
|
||||||
|
aptInstallFromPpa("nextcloud-devs", "client", "nextcloud-client")
|
||||||
|
|
||||||
|
aptInstall("inkscape")
|
||||||
|
aptInstall("dia")
|
||||||
|
|
||||||
|
aptInstall(SPELLCHECKING_DE)
|
||||||
|
|
||||||
|
installRedshift()
|
||||||
|
configureRedshift()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workplaceType == WorkplaceType.IDE) {
|
||||||
|
|
||||||
|
aptInstall(JAVA_JDK)
|
||||||
|
|
||||||
|
aptInstall(OPEN_VPM)
|
||||||
|
aptInstall(OPENCONNECT)
|
||||||
|
aptInstall(VPNC)
|
||||||
|
|
||||||
|
installDocker()
|
||||||
|
|
||||||
|
// IDEs
|
||||||
|
cmd("sudo snap install intellij-idea-community --classic")
|
||||||
|
installVSC("python", "clojure")
|
||||||
|
}
|
||||||
|
|
||||||
|
ProvResult(true) // dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions a workplace on a remote machine.
|
||||||
|
* Prerequisite: you have built the uberjar by ./gradlew uberJarLatest
|
||||||
|
* The remote host and remote user are specified by args parameters.
|
||||||
|
* The first argument specifies hostName or IP-Address of the remote machine,
|
||||||
|
* the second argument defines the user on the remote machine for whom the workplace is provisioned;
|
||||||
|
* You can invoke this method e.g. using the jar-file from the project root by:
|
||||||
|
* java -jar build/libs/provs-extensions-uber.jar io.provs.ubuntu.extensions.workplace.ProvisionWorkplaceKt provisionRemote <ip> <user>
|
||||||
|
* You will be prompted for the password of the remote user.
|
||||||
|
*
|
||||||
|
* @param args host and userName of the remote machine as the first resp. second argument
|
||||||
|
*/
|
||||||
|
fun provisionRemote(args: Array<String>) {
|
||||||
|
if (args.size != 2) {
|
||||||
|
println("Please specify host and user.")
|
||||||
|
exitProcess(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val host = InetAddress.getByName(args[0])
|
||||||
|
val userName = args[1]
|
||||||
|
val pwSecret = PromptSecretSource("Password for user $userName on $host").secret()
|
||||||
|
val pwFromSecret = Password(pwSecret.plain())
|
||||||
|
|
||||||
|
val config = readWorkplaceConfigFromFile() ?: WorkplaceConfig()
|
||||||
|
Prov.newInstance(RemoteProcessor(host, userName, pwFromSecret), OS.LINUX.name).provisionWorkplace(
|
||||||
|
config.type,
|
||||||
|
config.ssh?.keyPair(),
|
||||||
|
config.gpg?.keyPair(),
|
||||||
|
config.gitUserName,
|
||||||
|
config.gitEmail,
|
||||||
|
pwFromSecret
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provisions a workplace on a remote machine by calling method provisionRemote.
|
||||||
|
*
|
||||||
|
* @ see #provisionRemote(args: Array<String>)
|
||||||
|
*
|
||||||
|
* You can invoke this method e.g. using the jar-file from the project root by:
|
||||||
|
* java -jar build/libs/provs-ext-latest.jar workplace.WorkplaceKt main
|
||||||
|
*
|
||||||
|
* @param args host and userName of the remote machine as first resp. second argument
|
||||||
|
*/
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
provisionRemote(args = args)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace
|
||||||
|
|
||||||
|
import io.provs.ubuntu.keys.KeyPairSource
|
||||||
|
import io.provs.ubuntu.secret.SecretSource
|
||||||
|
import io.provs.ubuntu.secret.SecretSourceType
|
||||||
|
import io.provs.ubuntu.secret.SecretSupplier
|
||||||
|
import io.provs.ubuntu.secret.secretSources.PlainSecretSource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.*
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class WorkplaceConfig(
|
||||||
|
val type: WorkplaceType = WorkplaceType.MINIMAL,
|
||||||
|
val ssh: KeyPairSource? = null,
|
||||||
|
val gpg: KeyPairSource? = null,
|
||||||
|
val gitUserName: String? = null,
|
||||||
|
val gitEmail: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------- file methods ------------------------------------
|
||||||
|
fun readWorkplaceConfigFromFile(filename: String = "WorkplaceConfig.json"): WorkplaceConfig? {
|
||||||
|
val file = File(filename)
|
||||||
|
return if (file.exists())
|
||||||
|
try {
|
||||||
|
// read from file
|
||||||
|
val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
|
||||||
|
|
||||||
|
return Json.decodeFromString(WorkplaceConfig.serializer(), inputAsString)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
null
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun writeWorkplaceConfigToFile(config: WorkplaceConfig) {
|
||||||
|
val fileName = "WorkplaceConfig.json"
|
||||||
|
|
||||||
|
FileWriter(fileName).use { it.write(Json.encodeToString(WorkplaceConfig.serializer(), config)) }
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
fun Prov.installDocker() = def {
|
||||||
|
aptInstall("containerd docker.io")
|
||||||
|
if (!chk("getent group docker")) {
|
||||||
|
cmd("sudo groupadd docker")
|
||||||
|
}
|
||||||
|
cmd("sudo gpasswd -a \$USER docker")
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.web.base.downloadFromURL
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.installFakturama() = def {
|
||||||
|
createDir("/tmp", sudo = true)
|
||||||
|
downloadFromURL( "https://files.fakturama.info/release/v2.1.1/Installer_Fakturama_linux_x64_2.1.1b.deb", "fakturama.deb", "/tmp")
|
||||||
|
cmd("sudo dpkg -i fakturama.deb", "/tmp")
|
||||||
|
|
||||||
|
createDir("/opt/fakturama", sudo = true)
|
||||||
|
val filename = "Handbuch-Fakturama_2.1.1.pdf"
|
||||||
|
downloadFromURL( "https://files.fakturama.info/release/v2.1.1/Handbuch-Fakturama_2.1.1.pdf", filename, "/tmp")
|
||||||
|
cmd("sudo mv /tmp/$filename /opt/fakturama")
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDirs
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.userHome
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.install.base.isPackageInstalled
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.installGopass(version: String = "1.12.7", enforceVersion: Boolean = false) = def {
|
||||||
|
if (isPackageInstalled("gopass") && !enforceVersion) {
|
||||||
|
ProvResult(true)
|
||||||
|
} else {
|
||||||
|
// install required dependencies
|
||||||
|
aptInstall("rng-tools gnupg2 git")
|
||||||
|
aptInstall("curl")
|
||||||
|
|
||||||
|
sh(
|
||||||
|
"""
|
||||||
|
curl -L https://github.com/gopasspw/gopass/releases/download/v${version}/gopass_${version}_linux_amd64.deb -o gopass_${version}_linux_amd64.deb
|
||||||
|
sudo dpkg -i gopass_${version}_linux_amd64.deb
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
gopassEnsureVersion(version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.configureGopass(gopassRootFolder: String? = null) = def {
|
||||||
|
val gopassRootFolderNonNull = (gopassRootFolder ?: userHome()) + ".password-store"
|
||||||
|
// use default
|
||||||
|
createDir(gopassRootFolderNonNull)
|
||||||
|
createDirs(".config/gopass", "~/")
|
||||||
|
createFile("~/.config/gopass/config.yml", gopassConfig(gopassRootFolderNonNull))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.gopassMountStore(storeName: String, path: String, indexOfRecepientKey: Int = 0) = def {
|
||||||
|
cmd("printf \"$indexOfRecepientKey\\n\" | gopass mounts add $storeName $path")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
internal fun gopassConfig(gopassRoot: String): String {
|
||||||
|
return """
|
||||||
|
root:
|
||||||
|
askformore: false
|
||||||
|
autoclip: true
|
||||||
|
autoprint: false
|
||||||
|
autoimport: true
|
||||||
|
autosync: false
|
||||||
|
check_recipient_hash: false
|
||||||
|
cliptimeout: 45
|
||||||
|
concurrency: 1
|
||||||
|
editrecipients: false
|
||||||
|
exportkeys: true
|
||||||
|
nocolor: false
|
||||||
|
noconfirm: true
|
||||||
|
nopager: false
|
||||||
|
notifications: true
|
||||||
|
path: gpgcli-gitcli-fs+file://$gopassRoot
|
||||||
|
recipient_hash:
|
||||||
|
.gpg-id: 3078303637343130344341383141343930350aa69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26
|
||||||
|
safecontent: false
|
||||||
|
usesymbols: false
|
||||||
|
mounts: {}
|
||||||
|
""".trim() + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if gopass is installed and has the given version.
|
||||||
|
*
|
||||||
|
* @param version that is checked; specifies left part of text of installed version, e.g. both "1" and "1.12" will return true if installed version is "1.12.6+8d7a311b9273846bbb618e4bd9ddbae51b1db7b8"
|
||||||
|
*/
|
||||||
|
internal fun Prov.gopassEnsureVersion(version: String) = def {
|
||||||
|
val installedGopassVersion = gopassVersion()
|
||||||
|
if (installedGopassVersion != null && installedGopassVersion.startsWith("gopass " + version)) {
|
||||||
|
ProvResult(true, out = "Required gopass version ($version) matches installed version ($installedGopassVersion)")
|
||||||
|
} else {
|
||||||
|
ProvResult(false, err = "Wrong gopass version. Expected $version but found $installedGopassVersion")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Prov.gopassVersion(): String? {
|
||||||
|
val result = cmdNoEval("gopass -v")
|
||||||
|
return if (!result.success) null else result.out
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDirs
|
||||||
|
import io.provs.ubuntu.filesystem.base.userHome
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.install.base.isPackageInstalled
|
||||||
|
import io.provs.ubuntu.web.base.downloadFromURL
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.downloadGopassBridge() = def {
|
||||||
|
val version = "0.8.0"
|
||||||
|
val filename = "gopass_bridge-${version}-fx.xpi"
|
||||||
|
val downloadDir = "${userHome()}Downloads/"
|
||||||
|
|
||||||
|
createDirs(downloadDir)
|
||||||
|
downloadFromURL(
|
||||||
|
"-L https://addons.mozilla.org/firefox/downloads/file/3630534/" + filename,
|
||||||
|
downloadDir + filename
|
||||||
|
)
|
||||||
|
// needs manual install with: firefox Downloads/gopass_bridge-0.8.0-fx.xpi
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.installGopassBridgeJsonApi() = def {
|
||||||
|
// see https://github.com/gopasspw/gopass-jsonapi
|
||||||
|
val gopassBridgeVersion = "1.11.1"
|
||||||
|
val requiredGopassVersion = "1.12"
|
||||||
|
val filename = "gopass-jsonapi_${gopassBridgeVersion}_linux_amd64.deb"
|
||||||
|
val downloadUrl = "-L https://github.com/gopasspw/gopass-jsonapi/releases/download/v$gopassBridgeVersion/$filename"
|
||||||
|
val downloadDir = "${userHome()}Downloads"
|
||||||
|
val installedJsonApiVersion = gopassJsonApiVersion()?.trim()
|
||||||
|
|
||||||
|
if (installedJsonApiVersion == null) {
|
||||||
|
if (chk("gopass ls")) {
|
||||||
|
if (gopassEnsureVersion(requiredGopassVersion).success) {
|
||||||
|
aptInstall("git gnupg2") // required dependencies
|
||||||
|
createDir(downloadDir)
|
||||||
|
downloadFromURL(downloadUrl, filename, downloadDir)
|
||||||
|
cmd("dpkg -i " + downloadDir + "/" + filename, sudo = true)
|
||||||
|
} else {
|
||||||
|
ProvResult(
|
||||||
|
false,
|
||||||
|
"Version of currently installed gopass (" + gopassVersion() + ") is incompatible with gopass-jsonapi version to be installed. " +
|
||||||
|
"Please upgrade gopass to version: " + requiredGopassVersion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addResultToEval(
|
||||||
|
ProvResult(
|
||||||
|
false,
|
||||||
|
"gopass not initialized correctly. You can initialize gopass with: \"gopass init\""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (installedJsonApiVersion.startsWith("gopass-jsonapi version " + gopassBridgeVersion)) {
|
||||||
|
addResultToEval(ProvResult(true, out = "Version $gopassBridgeVersion of gopass-jsonapi is already installed"))
|
||||||
|
} else {
|
||||||
|
addResultToEval(
|
||||||
|
ProvResult(
|
||||||
|
false,
|
||||||
|
err = "gopass-jsonapi (version $gopassBridgeVersion) cannot be installed as version $installedJsonApiVersion is already installed." +
|
||||||
|
" Upgrading gopass-jsonapi is currently not supported by provs."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.configureGopassBridgeJsonApi() = def {
|
||||||
|
if (isPackageInstalled("gopass-jsonapi")) {
|
||||||
|
// 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 ..."
|
||||||
|
cmd("printf \"\\n\\n\\n\" | gopass-jsonapi configure --browser firefox")
|
||||||
|
} else {
|
||||||
|
ProvResult(
|
||||||
|
false,
|
||||||
|
err = "gopass-jsonapi is missing. Gopass-jsonapi must be installed to be able to configure it."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Prov.gopassJsonApiVersion(): String? {
|
||||||
|
val result = cmdNoEval("gopass-jsonapi -v")
|
||||||
|
return if (!result.success) null else result.out
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.filesystem.base.addTextToFile
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun Prov.configureNoSwappiness() = def {
|
||||||
|
// set swappiness to 0
|
||||||
|
addTextToFile("vm.swappiness=0", File("/etc/sysctl.conf"), sudo = true)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
val OS_ANALYSIS = "lsof strace ncdu iptraf htop iotop iftop"
|
||||||
|
|
||||||
|
val ZIP_UTILS = "p7zip-rar p7zip-full rar unrar zip unzip"
|
||||||
|
|
||||||
|
val BASH_UTILS = "bash-completion"
|
||||||
|
|
||||||
|
val SPELLCHECKING_DE = "hyphen-de hunspell hunspell-de-de"
|
||||||
|
|
||||||
|
val OPEN_VPM = "openvpn network-manager-openvpn network-manager-openvpn-gnome"
|
||||||
|
|
||||||
|
val OPENCONNECT = "openconnect network-manager-openconnect network-manager-openconnect-gnome"
|
||||||
|
|
||||||
|
val VPNC = "vpnc network-manager-vpnc network-manager-vpnc-gnome vpnc-scripts"
|
||||||
|
|
||||||
|
val JAVA_JDK = "openjdk-8-jdk openjdk-11-jdk openjdk-14-jdk"
|
|
@ -0,0 +1,35 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.installRedshift() = def {
|
||||||
|
aptInstall("redshift redshift-gtk")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.configureRedshift() = def {
|
||||||
|
aptInstall("redshift redshift-gtk")
|
||||||
|
|
||||||
|
createDir(".config")
|
||||||
|
createFile("~/.config/redshift.conf", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val config = """
|
||||||
|
[redshift]
|
||||||
|
temp-day=5500
|
||||||
|
temp-night=2700
|
||||||
|
brightness-day=1
|
||||||
|
brightness-night=0.6
|
||||||
|
fade=1
|
||||||
|
|
||||||
|
location-provider=manual
|
||||||
|
|
||||||
|
[manual]
|
||||||
|
lat=48.783333
|
||||||
|
lon=9.1833334
|
||||||
|
""".trimIndent()
|
|
@ -0,0 +1,75 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.installVSC(vararg options: String) = requireAll {
|
||||||
|
val clojureExtensions =
|
||||||
|
arrayListOf("betterthantomorrow.calva", "martinklepsch.clojure-joker-linter", "DavidAnson.vscode-markdownlint")
|
||||||
|
val pythonExtensions = arrayListOf("ms-python.python")
|
||||||
|
|
||||||
|
prerequisitesVSCinstall()
|
||||||
|
|
||||||
|
installVSCPackage()
|
||||||
|
|
||||||
|
if (options.contains("clojure")) {
|
||||||
|
installExtensions(clojureExtensions)
|
||||||
|
}
|
||||||
|
if (options.contains("python")) {
|
||||||
|
installExtensions(pythonExtensions)
|
||||||
|
}
|
||||||
|
|
||||||
|
provisionAdditionalTools()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Prov.prerequisitesVSCinstall() = def {
|
||||||
|
aptInstall("curl gpg unzip apt-transport-https")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused") // only required for installation of vscode via apt
|
||||||
|
private fun Prov.configurePackageManagerForVsc() = requireAll {
|
||||||
|
// see https://code.visualstudio.com/docs/setup/linux
|
||||||
|
// alternatively install with snapd (but this cannot be tested within docker as snapd within docker is not working/supported)
|
||||||
|
|
||||||
|
sh(
|
||||||
|
"""
|
||||||
|
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
|
||||||
|
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
|
||||||
|
sudo sh -c 'echo \"deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/vscode stable main\" > /etc/apt/sources.list.d/vscode.list'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
aptInstall("apt-transport-https")
|
||||||
|
aptInstall("code")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Prov.installVSCPackage() = def {
|
||||||
|
cmd("sudo snap install code --classic")
|
||||||
|
|
||||||
|
// to install via apt use:
|
||||||
|
// configurePackageManagerForVsc()
|
||||||
|
// aptInstall("code")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Prov.installExtensions(extensions: List<String>) = optional {
|
||||||
|
var res = ProvResult(true)
|
||||||
|
for (ext in extensions) {
|
||||||
|
res = cmd("code --install-extension $ext")
|
||||||
|
}
|
||||||
|
res
|
||||||
|
// Settings can be found at $HOME/.config/Code/User/settings.json
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Prov.provisionAdditionalTools() = requireAll {
|
||||||
|
// Joker
|
||||||
|
cmd("curl -Lo joker-0.12.2-linux-amd64.zip https://github.com/candid82/joker/releases/download/v0.12.2/joker-0.12.2-linux-amd64.zip")
|
||||||
|
cmd("unzip joker-0.12.2-linux-amd64.zip")
|
||||||
|
cmd("sudo mv joker /usr/local/bin/")
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.remote
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.secret.secretSources.GopassSecretSource
|
||||||
|
import io.provs.ubuntu.secret.secretSources.PlainSecretSource
|
||||||
|
import io.provs.ubuntu.secret.secretSources.PromptSecretSource
|
||||||
|
import io.provs.ubuntu.user.base.whoami
|
||||||
|
|
||||||
|
fun Prov.installVirtualBoxGuestAdditions() = def {
|
||||||
|
// if running in a VirtualBox vm
|
||||||
|
if (!chk("lspci | grep VirtualBox")) {
|
||||||
|
return@def ProvResult(true, "Not running in a VirtualBox")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chk("VBoxService --version")) {
|
||||||
|
return@def ProvResult(true, "VBoxService already installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// install guest additions
|
||||||
|
cmd("sudo add-apt-repository multiverse")
|
||||||
|
aptInstall("virtualbox-guest-x11") // virtualbox-guest-dkms")
|
||||||
|
// and add user to group vboxsf e.g. to be able to use shared folders
|
||||||
|
whoami()?.let { cmd("sudo usermod -G vboxsf -a " + it) }
|
||||||
|
?: ProvResult(true)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.install.base.aptInstallFromPpa
|
||||||
|
import io.provs.ubuntu.install.base.isPackageInstalled
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.installZimWiki() = def {
|
||||||
|
if (isPackageInstalled("zim")) {
|
||||||
|
ProvResult(true, out = "zim already installed.")
|
||||||
|
} else {
|
||||||
|
aptInstallFromPpa("jaap.karssenberg", "zim", "zim")
|
||||||
|
aptInstall("python3-gtkspellcheck")
|
||||||
|
}
|
||||||
|
}
|
197
src/main/kotlin/io/provs/ubuntu/filesystem/base/Filesystem.kt
Normal file
197
src/main/kotlin/io/provs/ubuntu/filesystem/base/Filesystem.kt
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
package io.provs.ubuntu.filesystem.base
|
||||||
|
|
||||||
|
import io.provs.*
|
||||||
|
import io.provs.platforms.SHELL
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean {
|
||||||
|
return cmdNoEval((if (sudo) "sudo " else "") + "test -e " + file).success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.createFile(
|
||||||
|
fullyQualifiedFilename: String,
|
||||||
|
text: String?,
|
||||||
|
posixFilePermission: String? = null,
|
||||||
|
sudo: Boolean = false
|
||||||
|
): ProvResult =
|
||||||
|
def {
|
||||||
|
val withSudo = if (sudo) "sudo " else ""
|
||||||
|
posixFilePermission?.let {
|
||||||
|
ensureValidPosixFilePermission(posixFilePermission)
|
||||||
|
cmd(withSudo + "install -m $posixFilePermission /dev/null $fullyQualifiedFilename")
|
||||||
|
}
|
||||||
|
if (text != null) {
|
||||||
|
if (sudo) {
|
||||||
|
cmd(
|
||||||
|
"printf " + text.escapeProcentForPrintf()
|
||||||
|
.escapeAndEncloseByDoubleQuoteForShell() + " | sudo tee $fullyQualifiedFilename > /dev/null"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
cmd(
|
||||||
|
"printf " + text.escapeProcentForPrintf()
|
||||||
|
.escapeAndEncloseByDoubleQuoteForShell() + " > $fullyQualifiedFilename"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cmd(withSudo + "touch $fullyQualifiedFilename")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.createSecretFile(
|
||||||
|
fullyQualifiedFilename: String,
|
||||||
|
secret: Secret,
|
||||||
|
posixFilePermission: String? = null
|
||||||
|
): ProvResult =
|
||||||
|
def {
|
||||||
|
posixFilePermission?.let {
|
||||||
|
ensureValidPosixFilePermission(posixFilePermission)
|
||||||
|
cmd("install -m $posixFilePermission /dev/null $fullyQualifiedFilename")
|
||||||
|
}
|
||||||
|
cmdNoLog("echo '" + secret.plain().escapeSingleQuote() + "' > $fullyQualifiedFilename")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.deleteFile(file: String, sudo: Boolean = false): ProvResult = def {
|
||||||
|
cmd((if (sudo) "sudo " else "") + "rm $file")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.fileContainsText(file: String, content: String, sudo: Boolean = false): Boolean {
|
||||||
|
return cmdNoEval((if (sudo) "sudo " else "") + "grep '${content.escapeSingleQuote()}' $file").success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.fileContent(file: String, sudo: Boolean = false): String? {
|
||||||
|
return cmd((if (sudo) "sudo " else "") + "cat $file").out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.addTextToFile(
|
||||||
|
text: String,
|
||||||
|
file: File,
|
||||||
|
doNotAddIfExisting: Boolean = true,
|
||||||
|
sudo: Boolean = false
|
||||||
|
): ProvResult =
|
||||||
|
def {
|
||||||
|
// TODO find solution without trim handling spaces, newlines, etc correctly
|
||||||
|
val findCmd = "grep '${text.trim().escapeSingleQuote()}' ${file}"
|
||||||
|
val findResult = cmdNoEval(if (sudo) findCmd.sudoizeCommand() else findCmd)
|
||||||
|
if (!findResult.success || !doNotAddIfExisting) {
|
||||||
|
val addCmd = "printf \"" + text.escapeDoubleQuote() + "\" >> " + file
|
||||||
|
cmd(if (sudo) addCmd.sudoizeCommand() else addCmd)
|
||||||
|
} else {
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.replaceTextInFile(file: String, oldText: String, replacement: String) = def {
|
||||||
|
replaceTextInFile(file, Regex.fromLiteral(oldText), Regex.escapeReplacement(replacement))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.replaceTextInFile(file: String, oldText: Regex, replacement: String) = def {
|
||||||
|
// todo: only use sudo for root or if owner different from current
|
||||||
|
val content = fileContent(file, true)
|
||||||
|
if (content != null) {
|
||||||
|
cmd("sudo truncate -s 0 $file")
|
||||||
|
addTextToFile(content.replace(oldText, Regex.escapeReplacement(replacement)), File(file), sudo = true)
|
||||||
|
} else {
|
||||||
|
ProvResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.insertTextInFile(file: String, textBehindWhichToInsert: Regex, textToInsert: String) = def {
|
||||||
|
// todo: only use sudo for root or if owner different from current
|
||||||
|
val content = fileContent(file, true)
|
||||||
|
if (content != null) {
|
||||||
|
val match = textBehindWhichToInsert.find(content)
|
||||||
|
if (match != null) {
|
||||||
|
cmd("sudo truncate -s 0 $file")
|
||||||
|
addTextToFile(
|
||||||
|
content.replace(textBehindWhichToInsert, match.value + Regex.escapeReplacement(textToInsert)),
|
||||||
|
File(file),
|
||||||
|
sudo = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ProvResult(false, err = "Text not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProvResult(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.dirExists(dir: String, path: String? = null, sudo: Boolean = false): Boolean {
|
||||||
|
val effectivePath = if (path != null) path else
|
||||||
|
(if (dir.startsWith(File.separator)) File.separator else "~" + File.separator)
|
||||||
|
val cmd = "cd $effectivePath && test -d $dir"
|
||||||
|
return cmdNoEval(if (sudo) cmd.sudoizeCommand() else cmd).success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.createDir(
|
||||||
|
dir: String,
|
||||||
|
path: String = "~/",
|
||||||
|
failIfExisting: Boolean = false,
|
||||||
|
sudo: Boolean = false
|
||||||
|
): ProvResult = def {
|
||||||
|
if (!failIfExisting && dirExists(dir, path, sudo)) {
|
||||||
|
ProvResult(true)
|
||||||
|
} else {
|
||||||
|
val cmd = "cd $path && mkdir $dir"
|
||||||
|
cmd(if (sudo) cmd.sudoizeCommand() else cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.createDirs(
|
||||||
|
dirs: String,
|
||||||
|
path: String = "~/",
|
||||||
|
failIfExisting: Boolean = false,
|
||||||
|
sudo: Boolean = false
|
||||||
|
): ProvResult = def {
|
||||||
|
if (!failIfExisting && dirExists(dirs, path, sudo)) {
|
||||||
|
ProvResult(true)
|
||||||
|
} else {
|
||||||
|
val cmd = "cd $path && mkdir -p $dirs"
|
||||||
|
cmd(if (sudo) cmd.sudoizeCommand() else cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.deleteDir(dir: String, path: String, sudo: Boolean = false): ProvResult {
|
||||||
|
if ("" == path)
|
||||||
|
throw RuntimeException("In deleteDir: path must not be empty.")
|
||||||
|
val cmd = "cd $path && rmdir $dir"
|
||||||
|
return if (!sudo) {
|
||||||
|
cmd(cmd)
|
||||||
|
} else {
|
||||||
|
cmd(cmd.sudoizeCommand())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.userHome(): String {
|
||||||
|
val user = cmd("whoami").out
|
||||||
|
if (user == null) {
|
||||||
|
throw RuntimeException("Could not determine user with whoami")
|
||||||
|
} else {
|
||||||
|
// assume default home folder
|
||||||
|
return "/home/" + user.trim() + "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun ensureValidPosixFilePermission(posixFilePermission: String) {
|
||||||
|
if (!Regex("^[0-7]{3}$").matches(posixFilePermission)) throw RuntimeException("Wrong file permission ($posixFilePermission), permission must consist of 3 digits as e.g. 664 ")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun String.sudoizeCommand(): String {
|
||||||
|
return "sudo " + SHELL + " -c " + this.escapeAndEncloseByDoubleQuoteForShell()
|
||||||
|
}
|
22
src/main/kotlin/io/provs/ubuntu/git/ProvisionGit.kt
Normal file
22
src/main/kotlin/io/provs/ubuntu/git/ProvisionGit.kt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package io.provs.ubuntu.git
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.provisionGit(
|
||||||
|
userName: String? = null,
|
||||||
|
email: String? = null,
|
||||||
|
signingKey: String? = null,
|
||||||
|
diffTool: String? = null
|
||||||
|
): ProvResult = def {
|
||||||
|
|
||||||
|
aptInstall("git")
|
||||||
|
|
||||||
|
cmd("git config --global push.default simple")
|
||||||
|
userName?.let { cmd("git config --global user.name $it") }
|
||||||
|
email?.let { cmd("git config --global user.email $it") }
|
||||||
|
signingKey?.let { cmd("git config --global user.signingkey $it") }
|
||||||
|
diffTool?.let { cmd("git config --global --add diff.tool $it") } ?: ProvResult(true)
|
||||||
|
}
|
80
src/main/kotlin/io/provs/ubuntu/git/base/Git.kt
Normal file
80
src/main/kotlin/io/provs/ubuntu/git/base/Git.kt
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package io.provs.ubuntu.git.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.addTextToFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.dirExists
|
||||||
|
import io.provs.ubuntu.keys.base.isHostKnown
|
||||||
|
import io.provs.ubuntu.utils.printToShell
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param host name or ip
|
||||||
|
* @param rsaFingerprints
|
||||||
|
*/
|
||||||
|
private fun Prov.trustHost(host: String, rsaFingerprints: Set<String>) = def {
|
||||||
|
if (!isHostKnown(host)) {
|
||||||
|
// logic based on https://serverfault.com/questions/447028/non-interactive-git-clone-ssh-fingerprint-prompt
|
||||||
|
val key = cmd("ssh-keyscan $host").out
|
||||||
|
if (key == null) {
|
||||||
|
ProvResult(false, "No key retrieved for $host")
|
||||||
|
} else {
|
||||||
|
val c = printToShell(key).trim()
|
||||||
|
val fpr = cmd(c + " | ssh-keygen -lf -").out
|
||||||
|
if (rsaFingerprints.contains(fpr)
|
||||||
|
) {
|
||||||
|
createDir(".ssh", "~/")
|
||||||
|
cmd(printToShell(key) + " >> ~/.ssh/known_hosts")
|
||||||
|
} else {
|
||||||
|
ProvResult(false, "Fingerprint $fpr not valid for $host")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.gitClone(repo: String, path: String, pullIfExisting: Boolean = true): ProvResult = def {
|
||||||
|
val dir = cmdNoEval("basename $repo .git").out?.trim()
|
||||||
|
if (dir == null) {
|
||||||
|
ProvResult(false, err = "$repo is not a valid git repository")
|
||||||
|
} else {
|
||||||
|
val pathToDir = if (path.endsWith("/")) path + dir else path + "/" + dir
|
||||||
|
if (dirExists(pathToDir + "/.git/")) {
|
||||||
|
if (pullIfExisting) {
|
||||||
|
cmd("cd $pathToDir && git pull")
|
||||||
|
} else {
|
||||||
|
ProvResult(true, out = "Repo $repo is already existing")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cmd("cd $path && git clone $repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.trustGithub() = def {
|
||||||
|
// current see https://docs.github.com/en/github/authenticating-to-github/githubs-ssh-key-fingerprints
|
||||||
|
val fingerprints = setOf(
|
||||||
|
"2048 SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 github.com (RSA)\n",
|
||||||
|
"2048 SHA256:br9IjFspm1vxR3iA35FWE+4VTyz1hYVLIE2t1/CeyWQ github.com (RSA)\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
trustHost("github.com", fingerprints)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.trustGitlab() = def {
|
||||||
|
// 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("~/.ssh/known_hosts"))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
60
src/main/kotlin/io/provs/ubuntu/install/base/Install.kt
Normal file
60
src/main/kotlin/io/provs/ubuntu/install/base/Install.kt
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package io.provs.ubuntu.install.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
|
||||||
|
|
||||||
|
private var aptInit = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs package(s) by using package manager "apt".
|
||||||
|
*
|
||||||
|
* @param packages the packages to be installed, packages separated by space if there are more than one
|
||||||
|
*/
|
||||||
|
fun Prov.aptInstall(packages: String): ProvResult = def {
|
||||||
|
if (!aptInit) {
|
||||||
|
cmd("sudo apt-get update")
|
||||||
|
cmd("sudo apt-get install -qy apt-utils")
|
||||||
|
aptInit = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val packageList = packages.split(" ")
|
||||||
|
for (packg in packageList) {
|
||||||
|
// see https://superuser.com/questions/164553/automatically-answer-yes-when-using-apt-get-install
|
||||||
|
cmd("sudo DEBIAN_FRONTEND=noninteractive apt-get install -qy $packg")
|
||||||
|
}
|
||||||
|
ProvResult(true) // dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a package from a ppa (personal package archive) by using package manager "apt".
|
||||||
|
*
|
||||||
|
* @param packageName the package to install
|
||||||
|
*/
|
||||||
|
fun Prov.aptInstallFromPpa(launchPadUser: String, ppaName: String, packageName: String): ProvResult = def {
|
||||||
|
aptInstall("software-properties-common") // for being able to use add-apt-repository
|
||||||
|
cmd("sudo add-apt-repository -y ppa:$launchPadUser/$ppaName")
|
||||||
|
aptInstall(packageName)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a program is installed
|
||||||
|
*
|
||||||
|
* @param packageName to check
|
||||||
|
* @return true if program is installed
|
||||||
|
*/
|
||||||
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.isPackageInstalled(packageName: String): Boolean {
|
||||||
|
return chk("timeout 2 dpkg -l $packageName")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a package including its configuration and data files
|
||||||
|
*/
|
||||||
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.aptPurge(packageName: String): Boolean {
|
||||||
|
return chk("sudo apt-get purge -qy $packageName")
|
||||||
|
}
|
33
src/main/kotlin/io/provs/ubuntu/keys/ProvisionKeys.kt
Normal file
33
src/main/kotlin/io/provs/ubuntu/keys/ProvisionKeys.kt
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package io.provs.ubuntu.keys
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.keys.base.configureGpgKeys
|
||||||
|
import io.provs.ubuntu.keys.base.configureSshKeys
|
||||||
|
import io.provs.ubuntu.secret.SecretSourceType
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
||||||
|
open class KeyPair(val publicKey: Secret, val privateKey: Secret)
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class KeyPairSource(val sourceType: SecretSourceType, val publicKey: String, val privateKey: String) {
|
||||||
|
fun keyPair() : KeyPair {
|
||||||
|
val pub = sourceType.secret(publicKey)
|
||||||
|
val priv = sourceType.secret(privateKey)
|
||||||
|
return KeyPair(pub, priv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* provisions gpg and/or ssh keys for the current user
|
||||||
|
*/
|
||||||
|
fun Prov.provisionKeysCurrentUser(gpgKeys: KeyPair? = null, sshKeys: KeyPair? = null) = requireAll {
|
||||||
|
gpgKeys?.let { configureGpgKeys(it, true) }
|
||||||
|
sshKeys?.let { configureSshKeys(it) }
|
||||||
|
ProvResult(true) // dummy
|
||||||
|
}
|
||||||
|
|
74
src/main/kotlin/io/provs/ubuntu/keys/base/Gpg.kt
Normal file
74
src/main/kotlin/io/provs/ubuntu/keys/base/Gpg.kt
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
package io.provs.ubuntu.keys.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.createSecretFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.dirExists
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.keys.KeyPair
|
||||||
|
import io.provs.ubuntu.utils.printToShell
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a gpg key pair for the current user.
|
||||||
|
*
|
||||||
|
* @param gpgKeys
|
||||||
|
* @param trust whether to trust keys with trust-level 5 (ultimate)
|
||||||
|
*/
|
||||||
|
fun Prov.configureGpgKeys(gpgKeys: KeyPair, trust: Boolean = false, skipIfExistin: Boolean = true) = requireAll {
|
||||||
|
aptInstall("gnupg")
|
||||||
|
val fingerprint = gpgFingerprint(gpgKeys.publicKey.plain())
|
||||||
|
if (fingerprint == null) {
|
||||||
|
ProvResult(false, err = "Fingerprint of key could not be determined")
|
||||||
|
} else {
|
||||||
|
if (gpgKeysInstalled(fingerprint) && skipIfExistin) {
|
||||||
|
ProvResult(true, out = "Keys were already installed")
|
||||||
|
} else {
|
||||||
|
val pubkeyFile = "~/pub-key.asc"
|
||||||
|
val privkeyFile = "~/priv-key.asc"
|
||||||
|
|
||||||
|
createSecretFile(pubkeyFile, gpgKeys.publicKey)
|
||||||
|
createSecretFile(privkeyFile, gpgKeys.privateKey)
|
||||||
|
|
||||||
|
cmd("gpg --import $pubkeyFile")
|
||||||
|
|
||||||
|
// using option --batch for older keys; see https://superuser.com/questions/1135812/gpg2-asking-for-passphrase-when-importing-secret-keys
|
||||||
|
cmd("gpg --batch --import $privkeyFile")
|
||||||
|
|
||||||
|
if (trust) {
|
||||||
|
cmd("printf \"5\\ny\\n\" | gpg --no-tty --command-fd 0 --expert --edit-key $fingerprint trust")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd("shred $pubkeyFile")
|
||||||
|
cmd("shred $privkeyFile")
|
||||||
|
|
||||||
|
configureGPGAgent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Prov.configureGPGAgent() = def {
|
||||||
|
if (dirExists(".gnupg")) {
|
||||||
|
createDir(".gnupg", "~/")
|
||||||
|
}
|
||||||
|
val content = """
|
||||||
|
allow-preset-passphrase
|
||||||
|
allow-loopback-pinentry
|
||||||
|
""".trimIndent()
|
||||||
|
createFile("~/.gnupg/gpg-agent.conf", content)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Prov.gpgKeysInstalled(fingerprint: String): Boolean {
|
||||||
|
return cmdNoLog("gpg --list-keys $fingerprint").success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.gpgFingerprint(pubKey: String): String? {
|
||||||
|
val result =
|
||||||
|
cmdNoLog(" " + printToShell(pubKey) + " | gpg --with-colons --import-options show-only --import --fingerprint")
|
||||||
|
return result.out?.let { """^fpr:*([A-Z0-9]*):$""".toRegex(RegexOption.MULTILINE).find(it)?.groupValues?.get(1) }
|
||||||
|
}
|
46
src/main/kotlin/io/provs/ubuntu/keys/base/Ssh.kt
Normal file
46
src/main/kotlin/io/provs/ubuntu/keys/base/Ssh.kt
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package io.provs.ubuntu.keys.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDir
|
||||||
|
import io.provs.ubuntu.filesystem.base.createSecretFile
|
||||||
|
import io.provs.ubuntu.keys.KeyPair
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* installs ssh keys for active user
|
||||||
|
*/
|
||||||
|
fun Prov.configureSshKeys(sshKeys: KeyPair) = def {
|
||||||
|
createDir(".ssh", "~/")
|
||||||
|
createSecretFile("~/.ssh/id_rsa.pub", sshKeys.publicKey, "644")
|
||||||
|
createSecretFile("~/.ssh/id_rsa", sshKeys.privateKey, "600")
|
||||||
|
configureSSHClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.configureSSHClient() = def {
|
||||||
|
// TODO("Not yet implemented")
|
||||||
|
ProvResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies a host or Ip to be trusted
|
||||||
|
*
|
||||||
|
* ATTENTION:
|
||||||
|
* This method is NOT secure as a man-in-the-middle could compromise the connection.
|
||||||
|
* Don't use this for critical systems resp. environments
|
||||||
|
*/
|
||||||
|
fun Prov.trustServer(hostOrIp: String) = def {
|
||||||
|
cmd("ssh-keyscan $hostOrIp >> ~/.ssh/known_hosts")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the specified hostname or Ip is in a known_hosts file
|
||||||
|
*
|
||||||
|
* @return whether if was found
|
||||||
|
*/
|
||||||
|
fun Prov.isHostKnown(hostOrIp: String) : Boolean {
|
||||||
|
return cmdNoEval("ssh-keygen -F $hostOrIp").out?.isNotEmpty() ?: false
|
||||||
|
}
|
||||||
|
|
39
src/main/kotlin/io/provs/ubuntu/secret/SecretSource.kt
Normal file
39
src/main/kotlin/io/provs/ubuntu/secret/SecretSource.kt
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package io.provs.ubuntu.secret
|
||||||
|
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.secret.secretSources.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
abstract class SecretSource(protected val input: String) {
|
||||||
|
abstract fun secret() : Secret
|
||||||
|
abstract fun secretNullable() : Secret?
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class SecretSourceType() {
|
||||||
|
|
||||||
|
PLAIN, FILE, PROMPT, PASS, GOPASS;
|
||||||
|
|
||||||
|
fun secret(input: String) : Secret {
|
||||||
|
return when (this) {
|
||||||
|
PLAIN -> PlainSecretSource(input).secret()
|
||||||
|
FILE -> FileSecretSource(input).secret()
|
||||||
|
PROMPT -> PromptSecretSource().secret()
|
||||||
|
PASS -> PassSecretSource(input).secret()
|
||||||
|
GOPASS -> GopassSecretSource(input).secret()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Suppress("unused") // for use in other projects
|
||||||
|
class SecretSupplier(private val source: SecretSourceType, val parameter: String) {
|
||||||
|
fun secret(): Secret {
|
||||||
|
return source.secret(parameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package io.provs.ubuntu.secret.secretSources
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.secret.SecretSource
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve secret from a file
|
||||||
|
*/
|
||||||
|
class FileSecretSource(fqFileName: String) : SecretSource(fqFileName) {
|
||||||
|
|
||||||
|
override fun secret(): 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")
|
||||||
|
return p.getSecret("cat " + input)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package io.provs.ubuntu.secret.secretSources
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.secret.SecretSource
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve secret from gopass
|
||||||
|
*/
|
||||||
|
class GopassSecretSource(path: String) : SecretSource(path) {
|
||||||
|
override fun secret(): Secret {
|
||||||
|
return secretNullable() ?: throw Exception("Failed to get \"$input\" secret from gopass.")
|
||||||
|
}
|
||||||
|
override fun secretNullable(): Secret? {
|
||||||
|
val p = Prov.newInstance(name = "GopassSecretSource for $input")
|
||||||
|
return p.getSecret("gopass show -f " + input)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package io.provs.ubuntu.secret.secretSources
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.secret.SecretSource
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve secret from passwordstore on Unix
|
||||||
|
*/
|
||||||
|
class PassSecretSource(path: String) : SecretSource(path) {
|
||||||
|
override fun secret(): 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")
|
||||||
|
return p.getSecret("pass " + input)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package io.provs.ubuntu.secret.secretSources
|
||||||
|
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.secret.SecretSource
|
||||||
|
|
||||||
|
|
||||||
|
class PlainSecretSource(plainSecret: String) : SecretSource(plainSecret) {
|
||||||
|
override fun secret(): Secret {
|
||||||
|
return Secret(input)
|
||||||
|
}
|
||||||
|
override fun secretNullable(): Secret {
|
||||||
|
return Secret(input)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package io.provs.ubuntu.secret.secretSources
|
||||||
|
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.ubuntu.secret.SecretSource
|
||||||
|
import java.awt.FlowLayout
|
||||||
|
import javax.swing.*
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordPanel : JPanel(FlowLayout()) {
|
||||||
|
|
||||||
|
private val passwordField = JPasswordField(20)
|
||||||
|
private var entered = false
|
||||||
|
|
||||||
|
val enteredPassword
|
||||||
|
get() = if (entered) String(passwordField.password) else null
|
||||||
|
|
||||||
|
init {
|
||||||
|
add(JLabel("Password: "))
|
||||||
|
add(passwordField)
|
||||||
|
passwordField.setActionCommand("OK")
|
||||||
|
passwordField.addActionListener {
|
||||||
|
if (it.actionCommand == "OK") {
|
||||||
|
entered = true
|
||||||
|
|
||||||
|
SwingUtilities.getWindowAncestor(it.source as JComponent)
|
||||||
|
.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun request(passwordIdentifier: String) = apply {
|
||||||
|
JOptionPane.showOptionDialog(null, this@PasswordPanel,
|
||||||
|
"Enter $passwordIdentifier",
|
||||||
|
JOptionPane.DEFAULT_OPTION,
|
||||||
|
JOptionPane.INFORMATION_MESSAGE,
|
||||||
|
null, emptyArray(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
fun requestPassword(passwordIdentifier: String) = PasswordPanel()
|
||||||
|
.request(passwordIdentifier)
|
||||||
|
.enteredPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PromptSecretSource(text: String = "Secret/Password") : SecretSource(text) {
|
||||||
|
|
||||||
|
override fun secret(): Secret {
|
||||||
|
val password = PasswordPanel.requestPassword(input)
|
||||||
|
if (password == null) {
|
||||||
|
throw IllegalArgumentException("Failed to retrieve secret from prompting.")
|
||||||
|
} else {
|
||||||
|
return Secret(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun secretNullable(): Secret? {
|
||||||
|
val password = PasswordPanel.requestPassword(input)
|
||||||
|
|
||||||
|
return if(password == null) {
|
||||||
|
null
|
||||||
|
}else {
|
||||||
|
Secret(password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
src/main/kotlin/io/provs/ubuntu/user/UserConfig.kt
Normal file
29
src/main/kotlin/io/provs/ubuntu/user/UserConfig.kt
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package io.provs.ubuntu.user
|
||||||
|
|
||||||
|
import io.provs.ubuntu.keys.KeyPairSource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.FileReader
|
||||||
|
import java.io.FileWriter
|
||||||
|
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class UserConfig(val userName: String, val gitEmail: String? = null, val gpg: KeyPairSource? = null, val ssh: KeyPairSource? = null)
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------------------------- file methods ------------------------------------
|
||||||
|
@Suppress("unused")
|
||||||
|
fun readUserConfigFromFile(filename: String = "UserConfig.json") : UserConfig {
|
||||||
|
// read from file
|
||||||
|
val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
|
||||||
|
|
||||||
|
// serializing objects
|
||||||
|
return Json.decodeFromString(UserConfig.serializer(), inputAsString)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun writeUserConfigToFile(config: UserConfig) {
|
||||||
|
val fileName = "UserConfig.json"
|
||||||
|
|
||||||
|
FileWriter(fileName).use { it.write(Json.encodeToString(UserConfig.serializer(), config)) }
|
||||||
|
}
|
166
src/main/kotlin/io/provs/ubuntu/user/base/User.kt
Normal file
166
src/main/kotlin/io/provs/ubuntu/user/base/User.kt
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package io.provs.ubuntu.user.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.processors.RemoteProcessor
|
||||||
|
import io.provs.ubuntu.filesystem.base.createDirs
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileExists
|
||||||
|
import io.provs.ubuntu.git.provisionGit
|
||||||
|
import io.provs.ubuntu.keys.base.gpgFingerprint
|
||||||
|
import io.provs.ubuntu.keys.provisionKeysCurrentUser
|
||||||
|
import io.provs.ubuntu.user.UserConfig
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.userExists(userName: String): Boolean {
|
||||||
|
return cmdNoEval("grep -c '^$userName:' /etc/passwd").success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user.
|
||||||
|
*/
|
||||||
|
fun Prov.createUser(
|
||||||
|
userName: String,
|
||||||
|
password: Secret? = null,
|
||||||
|
sudo: Boolean = false,
|
||||||
|
copyAuthorizedKeysFromCurrentUser: Boolean = false
|
||||||
|
): ProvResult = requireAll {
|
||||||
|
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 (sudo) {
|
||||||
|
makeUserSudoerWithNoSudoPasswordRequired(userName)
|
||||||
|
}
|
||||||
|
val authorizedKeysFile = "~/.ssh/authorized_keys"
|
||||||
|
if (copyAuthorizedKeysFromCurrentUser && fileExists(authorizedKeysFile)) {
|
||||||
|
createDirs("/home/$userName/.ssh")
|
||||||
|
val newAuthorizedKeysFile = "/home/$userName/.ssh/authorized_keys"
|
||||||
|
cmd("sudo cp $authorizedKeysFile $newAuthorizedKeysFile")
|
||||||
|
cmd("chown $userName $newAuthorizedKeysFile")
|
||||||
|
|
||||||
|
}
|
||||||
|
ProvResult(true) // dummy
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures gpg and ssh keys for the current if keys are provided in the config.
|
||||||
|
* Installs and configures git for the user if gitEmail is provided in the config.
|
||||||
|
* Does NOT CREATE the user.
|
||||||
|
*/
|
||||||
|
fun Prov.configureUser(config: UserConfig) = requireAll {
|
||||||
|
provisionKeysCurrentUser(
|
||||||
|
config.gpg?.keyPair(),
|
||||||
|
config.ssh?.keyPair()
|
||||||
|
)
|
||||||
|
|
||||||
|
config.gitEmail?.run {
|
||||||
|
provisionGit(
|
||||||
|
config.userName,
|
||||||
|
config.gitEmail,
|
||||||
|
config.gpg?.keyPair()?.let { gpgFingerprint(it.publicKey.plain()) })
|
||||||
|
} ?: ProvResult(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
// todo create test
|
||||||
|
fun Prov.deleteUser(userName: String, deleteHomeDir: Boolean = false): ProvResult = requireAll {
|
||||||
|
val flagToDeleteHomeDir = if (deleteHomeDir) " -r " else ""
|
||||||
|
if (userExists(userName)) {
|
||||||
|
cmd("sudo userdel $flagToDeleteHomeDir $userName")
|
||||||
|
} else {
|
||||||
|
ProvResult(false, err = "User $userName cannot be deleted as it does not exist.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes userName a sudoer who does not need a password to sudo.
|
||||||
|
* 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.makeUserSudoerWithNoSudoPasswordRequired(
|
||||||
|
userName: String,
|
||||||
|
password: Secret? = null,
|
||||||
|
overwriteFile: Boolean = false
|
||||||
|
): ProvResult = def {
|
||||||
|
val userSudoFile = "/etc/sudoers.d/$userName"
|
||||||
|
if (!fileExists(userSudoFile) || overwriteFile) {
|
||||||
|
val sudoPrefix = if (password == null) "sudo" else "echo ${password.plain()} | sudo -S"
|
||||||
|
// see https://stackoverflow.com/questions/323957/how-do-i-edit-etc-sudoers-from-a-script
|
||||||
|
val result = cmdNoLog(sudoPrefix + " sh -c \"echo '$userName ALL=(ALL) NOPASSWD:ALL' | (sudo su -c 'EDITOR=\"tee\" visudo -f " + userSudoFile + "')\"")
|
||||||
|
// don't log the command (containing the password) resp. don't include it in the ProvResult, just include success and err
|
||||||
|
ProvResult(result.success, err = result.err)
|
||||||
|
} else {
|
||||||
|
ProvResult(true, out = "File already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the current (executing) user be able to sudo without password.
|
||||||
|
* IMPORTANT: Current user must already by sudoer when calling this function.
|
||||||
|
*/
|
||||||
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.makeUserSudoerWithNoSudoPasswordRequired(password: Secret) = def {
|
||||||
|
val currentUser = whoami()
|
||||||
|
if (currentUser != null) {
|
||||||
|
makeUserSudoerWithNoSudoPasswordRequired(currentUser, password, overwriteFile = true)
|
||||||
|
} else {
|
||||||
|
ProvResult(false, "Current user could not be determined.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if user is in group sudo.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
fun Prov.userIsInGroupSudo(userName: String): Boolean {
|
||||||
|
return cmd("getent group sudo | grep -c '$userName'").success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if current user can execute sudo commands.
|
||||||
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
|
fun Prov.currentUserCanSudo(): Boolean {
|
||||||
|
return cmd("timeout 1 sudo id").success
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns username of current user if it can be determined
|
||||||
|
*/
|
||||||
|
fun Prov.whoami(): String? {
|
||||||
|
return cmd("whoami").run { if (success) out?.trim() else null }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user on the specified host.
|
||||||
|
*
|
||||||
|
* @host hostname or ip-address
|
||||||
|
* @hostUser user on the remote system, which is used to create the new user,
|
||||||
|
* hostUser must be sudoer
|
||||||
|
* @hostPassword pw of hostUser on the remote system;
|
||||||
|
* ssh-key authentication will be used if hostPassword is null
|
||||||
|
*/
|
||||||
|
@Suppress("api") // use externally
|
||||||
|
fun createRemoteUser(
|
||||||
|
host: InetAddress,
|
||||||
|
hostUser: String,
|
||||||
|
hostPassword: Secret?,
|
||||||
|
newUserName: String,
|
||||||
|
newUserPW: Secret,
|
||||||
|
makeNewUserSudoer: Boolean = false
|
||||||
|
) {
|
||||||
|
Prov.newInstance(RemoteProcessor(host, hostUser, hostPassword), name = "createRemoteUser")
|
||||||
|
.createUser(newUserName, newUserPW, makeNewUserSudoer)
|
||||||
|
}
|
||||||
|
|
11
src/main/kotlin/io/provs/ubuntu/utils/Utils.kt
Normal file
11
src/main/kotlin/io/provs/ubuntu/utils/Utils.kt
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package io.provs.ubuntu.utils
|
||||||
|
|
||||||
|
import io.provs.escapeBackslash
|
||||||
|
import io.provs.escapeDoubleQuote
|
||||||
|
|
||||||
|
|
||||||
|
// todo: investigate to use .escapeAndEncloseByDoubleQuoteForShell() or similar instead (?)
|
||||||
|
internal fun printToShell(text: String): String {
|
||||||
|
return "echo -n \"${text.escapeBackslash().escapeDoubleQuote()}\""
|
||||||
|
}
|
||||||
|
|
26
src/main/kotlin/io/provs/ubuntu/web/base/Web.kt
Normal file
26
src/main/kotlin/io/provs/ubuntu/web/base/Web.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package io.provs.ubuntu.web.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.install.base.isPackageInstalled
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a file from the given URL using curl
|
||||||
|
*
|
||||||
|
* @param path where to download to
|
||||||
|
* @param url file to download
|
||||||
|
* @param filename filename after download
|
||||||
|
*/
|
||||||
|
@Suppress("unused") // used externally
|
||||||
|
fun Prov.downloadFromURL(url: String, filename: String? = null, path: String? = null, sudo: Boolean = false) : ProvResult = def {
|
||||||
|
|
||||||
|
if (!isPackageInstalled("curl")) aptInstall("curl")
|
||||||
|
|
||||||
|
if (filename == null) {
|
||||||
|
cmd("curl $url", path, sudo)
|
||||||
|
} else {
|
||||||
|
cmd("curl $url -o $filename", path, sudo)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.firewall
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
|
||||||
|
import io.provs.docker.dockerProvideImage
|
||||||
|
import io.provs.docker.exitAndRmContainer
|
||||||
|
import io.provs.docker.images.UbuntuPlusUser
|
||||||
|
import io.provs.local
|
||||||
|
import io.provs.processors.ContainerEndMode
|
||||||
|
import io.provs.processors.ContainerStartMode
|
||||||
|
import io.provs.processors.ContainerUbuntuHostProcessor
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
|
internal class ProvisionFirewallKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled
|
||||||
|
fun provisionFirewall() {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled
|
||||||
|
fun resetFirewall() {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun configureFirewall() {
|
||||||
|
// given
|
||||||
|
val dockerImage = UbuntuPlusUser()
|
||||||
|
local().dockerProvideImage(dockerImage)
|
||||||
|
val containerName = "firewall_test"
|
||||||
|
local().exitAndRmContainer(containerName)
|
||||||
|
local().cmd("sudo docker run --cap-add=NET_ADMIN -dit --name $containerName ${dockerImage.imageName()}")
|
||||||
|
val a = Prov.newInstance(
|
||||||
|
ContainerUbuntuHostProcessor(
|
||||||
|
containerName,
|
||||||
|
dockerImage.imageName(),
|
||||||
|
ContainerStartMode.USE_RUNNING_ELSE_CREATE, // already started in previous statement
|
||||||
|
ContainerEndMode.EXIT_AND_REMOVE
|
||||||
|
))
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.requireAll {
|
||||||
|
aptInstall("iptables")
|
||||||
|
provisionFirewall()
|
||||||
|
}
|
||||||
|
local().exitAndRmContainer(containerName)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package nexus
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nexus.provisionNexusWithDocker
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
|
||||||
|
internal class ProvisionNexusKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled("Find out how to run docker in docker")
|
||||||
|
fun provisionNexusWithDocker() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.requireAll {
|
||||||
|
provisionNexusWithDocker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.ubuntu.filesystem.base.replaceTextInFile
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.base.*
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileExists
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
|
internal class ProvisionNginxKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun provisionNginxStandAlone_customConfig() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
val config = """
|
||||||
|
events {} # event context have to be defined to consider config valid
|
||||||
|
|
||||||
|
http {
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
return 200 "Hello";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
a.aptInstall("curl")
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.requireAll {
|
||||||
|
provisionNginxStandAlone(NginxConf(config))
|
||||||
|
cmd("curl localhost")
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun provisionNginxStandAlone_defaultConfig() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.requireAll {
|
||||||
|
provisionNginxStandAlone()
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun provisionNginxStandAlone_sslConfig() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
a.def {
|
||||||
|
val file = "/etc/ssl/openssl.cnf"
|
||||||
|
if (fileExists(file)) {
|
||||||
|
replaceTextInFile(file, "RANDFILE", "#RANDFILE")
|
||||||
|
}
|
||||||
|
aptInstall("openssl")
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.def {
|
||||||
|
nginxCreateSelfSignedCertificate()
|
||||||
|
|
||||||
|
provisionNginxStandAlone(
|
||||||
|
NginxConf.nginxReverseProxySslConfig(
|
||||||
|
"localhost",
|
||||||
|
dirSslCert + "/" + certificateName + ".crt",
|
||||||
|
dirSslKey + "/" + certificateName + ".key"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
package io.provs.ubuntu.extensions.server_software.nginx.base
|
||||||
|
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileContainsText
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.configFile
|
||||||
|
import io.provs.ubuntu.extensions.server_software.nginx.provisionNginxStandAlone
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
|
||||||
|
internal class LocationsKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nginxIncludeLocationFolders() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
a.provisionNginxStandAlone()
|
||||||
|
a.createFile(configFile, NGINX_MINIMAL_CONF, sudo = true)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.nginxIncludeLocationFolders()
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
assertTrue(a.fileContainsText(
|
||||||
|
configFile, """listen 80;
|
||||||
|
include /etc/nginx/locations-enabled/port80*.conf
|
||||||
|
include /etc/nginx/locations-enabled/port443*.conf"""))
|
||||||
|
// just 1 occurrence
|
||||||
|
assertEquals("1", a.cmd("grep -o 'listen 80;' $configFile | wc -l").out?.trim())
|
||||||
|
}
|
||||||
|
}
|
166
src/test/kotlin/io/provs/ubuntu/extensions/test_keys/TestKeys.kt
Normal file
166
src/test/kotlin/io/provs/ubuntu/extensions/test_keys/TestKeys.kt
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package io.provs.ubuntu.extensions.test_keys
|
||||||
|
|
||||||
|
|
||||||
|
fun publicGPGSnakeoilKey(): String {
|
||||||
|
return """-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
KpkCQg==
|
||||||
|
=cS1b
|
||||||
|
-----END PGP PUBLIC KEY BLOCK----- """
|
||||||
|
}
|
||||||
|
|
||||||
|
fun privateGPGSnakeoilKey(): String {
|
||||||
|
return """-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
i+XV3Dazj3nq/DxUB0neLU/r1afAEiqZAkI=
|
||||||
|
=h5SJ
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publicSSHSnakeoilKey(): String {
|
||||||
|
return """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOtQOq8a/Z7SdZVPrh+Icaq5rr+Qg1TZP4IPuRoFgfujUztQ2dy5DfTEbabJ0qHyo+PKwBDQorVohrW7CwvCEVQQh2NLuGgnukBN2ut5Lam7a/fZBoMjAyTvD4bXyEsUr/Bl5CLoBDkKM0elUxsc19ndzSofnDWeGyQjJIWlkNkVk/ybErAnIHVE+D+g3UxwA+emd7BF72RPqdVN39Eu4ntnxYzX0eepc8rkpFolVn6+Ai4CYHE4FaJ7bJ9WGPbwLuDl0pw/Cp3ps17cB+JlQfJ2spOq0tTVk+GcdGnt+mq0WaOnvVeQsGJ2O1HpY3VqQd1AsC2UOyHhAQ00pw7Pi9 snake@oil.com"""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun privateSSHSnakeoilKey(): String {
|
||||||
|
return """
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAzrUDqvGv2e0nWVT64fiHGqua6/kINU2T+CD7kaBYH7o1M7UN
|
||||||
|
ncuQ30xG2mydKh8qPjysAQ0KK1aIa1uwsLwhFUEIdjS7hoJ7pATdrreS2pu2v32Q
|
||||||
|
aDIwMk7w+G18hLFK/wZeQi6AQ5CjNHpVMbHNfZ3c0qH5w1nhskIySFpZDZFZP8mx
|
||||||
|
KwJyB1RPg/oN1McAPnpnewRe9kT6nVTd/RLuJ7Z8WM19HnqXPK5KRaJVZ+vgIuAm
|
||||||
|
BxOBWie2yfVhj28C7g5dKcPwqd6bNe3AfiZUHydrKTqtLU1ZPhnHRp7fpqtFmjp7
|
||||||
|
1XkLBidjtR6WN1akHdQLAtlDsh4QENNKcOz4vQIDAQABAoIBAGrgAsZ28gJOcSLq
|
||||||
|
IlGF62zpv0800n6k3tXTT98qtYWqBGn4udKVdxFNYfD7aYNm27OUMSbV9CUWN7Cy
|
||||||
|
lre6fax8lIBxoWfZvU2/ylLUzZREIIf/xxNop6zLTiJUkaYV+P3E8CVt35mPhiLT
|
||||||
|
AYuRL/s8DPnHD9lmdqBxQ4hPVm4Bg7JZxbyN8in3PP1UkdWKxg91O1LYewIZHszq
|
||||||
|
y9BdklKyxQ+fcYP5DD9KkULAjdab48GIxQETrZKp7zV0KiGrjF4Axf5y5yT2jmFT
|
||||||
|
nZ1uZrC1MJTMYyKTBR7wsSpVBMSMUsh5XtxdJo4FuP6g9Kn6AkeQ/Y1shcWVfQgw
|
||||||
|
6009o8ECgYEA8J1PtnVCHxMLiVKZznzvgCe+EV0RkvuB9PGPdfpLfkHa1DKS+FzH
|
||||||
|
80D+Vqe0rQNLudG5Qj53MPghNirGyrjXwTYFW9xCqq9hrzfxEI4xIYOd4gHoPMMQ
|
||||||
|
pfWZylP9GYQp/uoa+e/fcdXRSv1IDLRwJZ5XpMtWAIfvMOyDhbfjehECgYEA2+yp
|
||||||
|
poey1y6RWuaIQd2a/PKuYk9jvLEETiz6q7t63MFd6e9cUYX02cG/6yzz6piTWUtx
|
||||||
|
pk9e9IjclLUgV/twVz8SUgSw5TcqBrMnuIT4yQ5rQNZqiEvpCfgb5itcW7I3ADGy
|
||||||
|
dsz2kgaAm7QVZlndQKIy7xRYBCnCD3VQ+TiWh+0CgYAT3qnKg3xmXIhDWtLgvmh4
|
||||||
|
yM9lV64v2R0uQRR7xaOeVYngpByG7gKFEATw2wCMmQ0T10HZOpdVL+huNLId443N
|
||||||
|
osxmfZXzym/irFf36gYcomXTWBz5h5JEYjfFAZKRHNzq9CIuKaTmHaYe7zOX+P6Z
|
||||||
|
3K2YKkJ74L3b6GwkCr96QQKBgQC6n0iTTSGg4h5skaXcpq2HqnP6br4G9/vcTuTk
|
||||||
|
Z/JpdBk6k2i2sULGqlguu/W8BH89Tf0CEOZWAfGUq2Ln5jE9iAMG4H4v9DDQgKTb
|
||||||
|
OtNW4cp3uburLydw0z7xgagdE80CeCmmEGXIIoZuGlHyiZ1r5HfuU0ghOEI6FeaB
|
||||||
|
pdhvPQKBgEpmHV66wqSzzxmYxKjUu8gl9rIniG8SWXHlvcoGVwt1qdOMtNtvwDgB
|
||||||
|
DnbUbANSjzIfFSqVwlx7nXG1e1yN7F1YuyUa3I5QEm4+5URoTSDghk03LTFH+kfM
|
||||||
|
OUxwE8Su4WnoQc7WjkTG0M3FECAu7TEcF9uqdcEsW+4+JMAhE5oo
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace
|
||||||
|
|
||||||
|
import io.provs.Password
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
|
||||||
|
internal class ProvisionWorkplaceKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun provisionWorkplace() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
// in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
|
||||||
|
val res = a.provisionWorkplace(
|
||||||
|
WorkplaceType.MINIMAL,
|
||||||
|
gitUserName = "testuser",
|
||||||
|
gitEmail = "testuser@test.org",
|
||||||
|
userPassword = Password("testuser")
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun provisionWorkplaceFromConfigFile() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
// in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
|
||||||
|
val config = readWorkplaceConfigFromFile("src/test/resources/WorkplaceConfigExample.json")
|
||||||
|
?: throw Exception("Could not read WorkplaceConfig")
|
||||||
|
val res = a.provisionWorkplace(
|
||||||
|
config.type,
|
||||||
|
config.ssh?.keyPair(),
|
||||||
|
config.gpg?.keyPair(),
|
||||||
|
config.gitUserName,
|
||||||
|
config.gitEmail,
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class FakturamaKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun installFakturama() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
// when
|
||||||
|
val res = a.def { installFakturama() }
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.ProvResult
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.docker.exitAndRmContainer
|
||||||
|
import io.provs.local
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import io.provs.test.tags.NonCi
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.keys.KeyPair
|
||||||
|
import io.provs.ubuntu.keys.base.configureGpgKeys
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import io.provs.ubuntu.extensions.test_keys.privateGPGSnakeoilKey
|
||||||
|
import io.provs.ubuntu.extensions.test_keys.publicGPGSnakeoilKey
|
||||||
|
|
||||||
|
|
||||||
|
internal class GopassBridgeKtTest {
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
fun test_downloadGopassBridge() {
|
||||||
|
// given
|
||||||
|
local().exitAndRmContainer("provs_test")
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
a.aptInstallCurl()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.downloadGopassBridge()
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
fun test_install_and_configure_GopassBridgeJsonApi() {
|
||||||
|
// given
|
||||||
|
local().exitAndRmContainer("provs_test")
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
val preparationResult = a.def {
|
||||||
|
aptInstallCurl()
|
||||||
|
configureGpgKeys(
|
||||||
|
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
|
||||||
|
trust = true,
|
||||||
|
skipIfExistin = false
|
||||||
|
)
|
||||||
|
installGopass()
|
||||||
|
if (!chk("gopass ls")) {
|
||||||
|
// configure/init gopass in default location with gpg-key-fingerprint of snakeoil keys
|
||||||
|
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
|
||||||
|
} else {
|
||||||
|
ProvResult(true, out = "gopass already configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(preparationResult.success)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.def {
|
||||||
|
installGopassBridgeJsonApi()
|
||||||
|
configureGopassBridgeJsonApi()
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
@NonCi
|
||||||
|
fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_jsonapi_version_installed() {
|
||||||
|
// given
|
||||||
|
local().exitAndRmContainer("provs_test")
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
val preparationResult = a.def {
|
||||||
|
aptInstallCurl()
|
||||||
|
|
||||||
|
configureGpgKeys(
|
||||||
|
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
|
||||||
|
trust = true,
|
||||||
|
skipIfExistin = false
|
||||||
|
)
|
||||||
|
installGopass("1.11.0", enforceVersion = true)
|
||||||
|
if (!chk("gopass ls")) {
|
||||||
|
// configure gopass in default location with gpg-key-fingerprint of snakeoil keys
|
||||||
|
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
|
||||||
|
} else {
|
||||||
|
ProvResult(true, out = "gopass already configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(preparationResult.success)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.def {
|
||||||
|
installGopassBridgeJsonApi()
|
||||||
|
configureGopassBridgeJsonApi()
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
@NonCi
|
||||||
|
fun test_install_GopassBridgeJsonApi_with_incompatible_gopass_version_installed() {
|
||||||
|
// given
|
||||||
|
local().exitAndRmContainer("provs_test")
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
val preparationResult = a.def {
|
||||||
|
aptInstallCurl()
|
||||||
|
configureGpgKeys(
|
||||||
|
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
|
||||||
|
trust = true,
|
||||||
|
skipIfExistin = false
|
||||||
|
)
|
||||||
|
installGopass("1.9.0", enforceVersion = true)
|
||||||
|
if (!chk("gopass ls")) {
|
||||||
|
// configure gopass in default location with gpg-key-fingerprint of snakeoil keys
|
||||||
|
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init 0x0674104CA81A4905")
|
||||||
|
} else {
|
||||||
|
ProvResult(true, out = "gopass already configured")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(preparationResult.success)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.def {
|
||||||
|
installGopassBridgeJsonApi()
|
||||||
|
configureGopassBridgeJsonApi()
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Prov.aptInstallCurl() = def {
|
||||||
|
cmd("apt-get update", sudo = true)
|
||||||
|
aptInstall("curl")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.remote
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import io.provs.ubuntu.filesystem.base.*
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.keys.KeyPair
|
||||||
|
import io.provs.ubuntu.keys.base.configureGpgKeys
|
||||||
|
import io.provs.ubuntu.keys.base.gpgFingerprint
|
||||||
|
import io.provs.ubuntu.secret.secretSources.GopassSecretSource
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import io.provs.ubuntu.extensions.test_keys.privateGPGSnakeoilKey
|
||||||
|
import io.provs.ubuntu.extensions.test_keys.publicGPGSnakeoilKey
|
||||||
|
|
||||||
|
|
||||||
|
internal class GopassKtTest {
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
fun test_installAndConfigureGopassAndMountStore() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
val gopassRootDir = ".password-store"
|
||||||
|
a.aptInstall("wget git gnupg")
|
||||||
|
a.createDir(gopassRootDir, "~/")
|
||||||
|
a.cmd("git init", "~/$gopassRootDir")
|
||||||
|
val fpr = a.gpgFingerprint(publicGPGSnakeoilKey())
|
||||||
|
println("+++++++++++++++++++++++++++++++++++++ $fpr +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++")
|
||||||
|
a.createFile("~/" + gopassRootDir + "/.gpg-id", fpr)
|
||||||
|
|
||||||
|
a.createDir("exampleStoreFolder", "~/")
|
||||||
|
a.createFile("~/exampleStoreFolder/.gpg-id", fpr)
|
||||||
|
|
||||||
|
a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.installGopass()
|
||||||
|
val res2 = a.configureGopass(a.userHome() + ".password-store")
|
||||||
|
val res3 = a.gopassMountStore("exampleStore", "~/exampleStoreFolder")
|
||||||
|
|
||||||
|
// then
|
||||||
|
a.fileContent("~/.config/gopass/config.yml") // displays the content in the logs
|
||||||
|
assertTrue(res.success)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(res3.success)
|
||||||
|
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "/home/testuser/.password-store"))
|
||||||
|
assertTrue(a.fileContainsText("~/.config/gopass/config.yml", "exampleStore"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled // Integrationtest; change user, host and keys, then remove this line to run this test
|
||||||
|
fun test_install_and_configure_Gopass_and_GopassBridgeJsonApi() {
|
||||||
|
// settings to change
|
||||||
|
val host = "192.168.56.135"
|
||||||
|
val user = "xxx"
|
||||||
|
val pubKey = GopassSecretSource("path-to/pub.key").secret()
|
||||||
|
val privateKey = GopassSecretSource("path-to/priv.key").secret()
|
||||||
|
|
||||||
|
// given
|
||||||
|
val a = remote(host, user)
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.def {
|
||||||
|
configureGpgKeys(
|
||||||
|
KeyPair(
|
||||||
|
pubKey,
|
||||||
|
privateKey
|
||||||
|
),
|
||||||
|
trust = true,
|
||||||
|
skipIfExistin = true
|
||||||
|
)
|
||||||
|
installGopass()
|
||||||
|
|
||||||
|
if (!chk("gopass ls")) {
|
||||||
|
// configure (=init) gopass
|
||||||
|
cmd("printf \"\\ntest\\ntest@test.org\\n\" | gopass init " + gpgFingerprint(pubKey.plain())) // gopass init in default location with gpg-key-fingerprint of given key
|
||||||
|
}
|
||||||
|
downloadGopassBridge()
|
||||||
|
installGopassBridgeJsonApi()
|
||||||
|
configureGopassBridgeJsonApi()
|
||||||
|
}
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package io.provs.ubuntu.extensions.workplace.base
|
||||||
|
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
|
||||||
|
|
||||||
|
internal class VSCodeKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled("Test currently not working, needs fix. VSC is installed by snapd which is not currently supported to run inside docker")
|
||||||
|
fun installVSC() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
a.aptInstall("xvfb libgbm-dev libasound2")
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.installVSC("python", "clojure")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
package io.provs.ubuntu.filesystem.base
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
|
internal class FilesystemKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun checkingCreatingDeletingFile() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res1 = prov.fileExists("testfile")
|
||||||
|
val res2 = prov.createFile("testfile", "some content")
|
||||||
|
val res3 = prov.fileExists("testfile")
|
||||||
|
val res4a = prov.fileContainsText("testfile", "some content")
|
||||||
|
val res4b = prov.fileContainsText("testfile", "some non-existing content")
|
||||||
|
val res5 = prov.deleteFile("testfile")
|
||||||
|
val res6 = prov.fileExists("testfile")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(res1)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(res3)
|
||||||
|
assertTrue(res4a)
|
||||||
|
assertFalse(res4b)
|
||||||
|
assertTrue(res5.success)
|
||||||
|
assertFalse(res6)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun checkingCreatingDeletingFileWithSudo() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val file = "/testfile"
|
||||||
|
val res1 = prov.fileExists(file)
|
||||||
|
val res2 = prov.createFile(file, "some content", sudo = true)
|
||||||
|
val res3 = prov.fileExists(file)
|
||||||
|
val res4a = prov.fileContainsText(file, "some content")
|
||||||
|
val res4b = prov.fileContainsText(file, "some non-existing content")
|
||||||
|
val res5 = prov.deleteFile(file)
|
||||||
|
val res6 = prov.fileExists(file)
|
||||||
|
val res7 = prov.deleteFile(file, true)
|
||||||
|
val res8 = prov.fileExists(file)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(res1)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(res3)
|
||||||
|
assertTrue(res4a)
|
||||||
|
assertFalse(res4b)
|
||||||
|
assertFalse(res5.success)
|
||||||
|
assertTrue(res6)
|
||||||
|
assertTrue(res7.success)
|
||||||
|
assertFalse(res8)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun checkingCreatingDeletingDir() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res1 = prov.dirExists("testdir")
|
||||||
|
val res2 = prov.createDir("testdir", "~/")
|
||||||
|
val res3 = prov.dirExists("testdir")
|
||||||
|
val res4 = prov.deleteDir("testdir", "~/")
|
||||||
|
val res5 = prov.dirExists("testdir")
|
||||||
|
|
||||||
|
val res6 = prov.dirExists("testdir", "~/test")
|
||||||
|
val res7 = prov.createDirs("test/testdir")
|
||||||
|
val res8 = prov.dirExists("testdir", "~/test")
|
||||||
|
prov.deleteDir("testdir", "~/test/")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(res1)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(res3)
|
||||||
|
assertTrue(res4.success)
|
||||||
|
assertFalse(res5)
|
||||||
|
assertFalse(res6)
|
||||||
|
assertTrue(res7.success)
|
||||||
|
assertTrue(res8)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun checkingCreatingDeletingDirWithSudo() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res1 = prov.dirExists("/testdir", sudo = true)
|
||||||
|
val res2 = prov.createDir("testdir", "/", sudo = true)
|
||||||
|
val res3 = prov.dirExists("/testdir", sudo = true)
|
||||||
|
val res4 = prov.deleteDir("testdir", "/", true)
|
||||||
|
val res5 = prov.dirExists("testdir", sudo = true)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(res1)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(res3)
|
||||||
|
assertTrue(res4.success)
|
||||||
|
assertFalse(res5)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun userHome() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res1 = prov.userHome()
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals("/home/testuser/", res1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun replaceTextInFile() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val file = "replaceTest"
|
||||||
|
val res1 = prov.createFile(file, "a\nb\nc\nd")
|
||||||
|
val res2 = prov.replaceTextInFile(file,"b", "hi\nho")
|
||||||
|
val res3 = prov.fileContent(file).equals("a\nhi\nho\nc\nd")
|
||||||
|
val res4 = prov.deleteFile(file)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res1.success)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(res3)
|
||||||
|
assertTrue(res4.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun replaceTextInFileRegex() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val file = "replaceTest"
|
||||||
|
val res1 = prov.createFile(file, "a\nbananas\nc\nd")
|
||||||
|
val res2 = prov.replaceTextInFile(file, Regex("b.*n?nas\n"), "hi\nho\n")
|
||||||
|
val res3 = prov.fileContent(file)
|
||||||
|
val res4 = prov.deleteFile(file)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res1.success)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertEquals("a\nhi\nho\nc\nd",res3)
|
||||||
|
assertTrue(res4.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun insertTextInFile() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val file = "insertTest"
|
||||||
|
val res1 = prov.createFile(file, "a\nbananas\nc\nd")
|
||||||
|
val res2 = prov.insertTextInFile(file, Regex("b.*n.nas\n"), "hi\n")
|
||||||
|
val res3 = prov.fileContent(file)
|
||||||
|
val res4 = prov.deleteFile(file)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res1.success)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertEquals("a\nbananas\nhi\nc\nd", res3)
|
||||||
|
assertTrue(res4.success)
|
||||||
|
}
|
||||||
|
}
|
45
src/test/kotlin/io/provs/ubuntu/git/base/GitKtTest.kt
Normal file
45
src/test/kotlin/io/provs/ubuntu/git/base/GitKtTest.kt
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package io.provs.ubuntu.git.base
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.ubuntu.install.base.aptInstall
|
||||||
|
import io.provs.ubuntu.keys.base.isHostKnown
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
|
internal class GitKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun trustGitServers(){
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
a.aptInstall("openssh-client")
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.trustGithub()
|
||||||
|
val known = a.isHostKnown("github.com")
|
||||||
|
val res2 = a.trustGitlab()
|
||||||
|
val known2 = a.isHostKnown("gitlab.com")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
assertTrue(known)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
assertTrue(known2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun gitClone() {
|
||||||
|
// given
|
||||||
|
val prov = defaultTestContainer()
|
||||||
|
prov.aptInstall("openssh-client ssh git")
|
||||||
|
|
||||||
|
// when
|
||||||
|
prov.trustGithub()
|
||||||
|
prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
|
||||||
|
val res = prov.gitClone("https://github.com/DomainDrivenArchitecture/dda-git-crate.git", "~/")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package io.provs.ubuntu.install.base
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
|
||||||
|
internal class InstallKtTest {
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
fun aptInstall_installsPackage() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.aptInstall("rolldice")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
@Disabled // run manually if needed;
|
||||||
|
// todo: replace zim by a smaller repo
|
||||||
|
fun aptInstallFromPpa_installsPackage() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
a.aptInstall("software-properties-common") // prereq for adding a repo to apt
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.aptInstallFromPpa("jaap.karssenberg", "zim", "zim")
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
}
|
27
src/test/kotlin/io/provs/ubuntu/keys/ProvisionKeysTest.kt
Normal file
27
src/test/kotlin/io/provs/ubuntu/keys/ProvisionKeysTest.kt
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package io.provs.ubuntu.keys
|
||||||
|
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.condition.EnabledOnOs
|
||||||
|
import org.junit.jupiter.api.condition.OS
|
||||||
|
|
||||||
|
internal class ProvisionKeysTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@EnabledOnOs(OS.LINUX)
|
||||||
|
fun provisionKeysCurrentUser() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.provisionKeysCurrentUser(
|
||||||
|
KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())),
|
||||||
|
KeyPair(Secret(publicSSHSnakeoilKey()), Secret(privateSSHSnakeoilKey()))
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert(res.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
166
src/test/kotlin/io/provs/ubuntu/keys/TestKeys.kt
Normal file
166
src/test/kotlin/io/provs/ubuntu/keys/TestKeys.kt
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package io.provs.ubuntu.keys
|
||||||
|
|
||||||
|
|
||||||
|
fun publicGPGSnakeoilKey(): String {
|
||||||
|
return """-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
KpkCQg==
|
||||||
|
=cS1b
|
||||||
|
-----END PGP PUBLIC KEY BLOCK----- """
|
||||||
|
}
|
||||||
|
|
||||||
|
fun privateGPGSnakeoilKey(): String {
|
||||||
|
return """-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
i+XV3Dazj3nq/DxUB0neLU/r1afAEiqZAkI=
|
||||||
|
=h5SJ
|
||||||
|
-----END PGP PRIVATE KEY BLOCK-----""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun publicSSHSnakeoilKey(): String {
|
||||||
|
return """ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDOtQOq8a/Z7SdZVPrh+Icaq5rr+Qg1TZP4IPuRoFgfujUztQ2dy5DfTEbabJ0qHyo+PKwBDQorVohrW7CwvCEVQQh2NLuGgnukBN2ut5Lam7a/fZBoMjAyTvD4bXyEsUr/Bl5CLoBDkKM0elUxsc19ndzSofnDWeGyQjJIWlkNkVk/ybErAnIHVE+D+g3UxwA+emd7BF72RPqdVN39Eu4ntnxYzX0eepc8rkpFolVn6+Ai4CYHE4FaJ7bJ9WGPbwLuDl0pw/Cp3ps17cB+JlQfJ2spOq0tTVk+GcdGnt+mq0WaOnvVeQsGJ2O1HpY3VqQd1AsC2UOyHhAQ00pw7Pi9 snake@oil.com"""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun privateSSHSnakeoilKey(): String {
|
||||||
|
return """
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAzrUDqvGv2e0nWVT64fiHGqua6/kINU2T+CD7kaBYH7o1M7UN
|
||||||
|
ncuQ30xG2mydKh8qPjysAQ0KK1aIa1uwsLwhFUEIdjS7hoJ7pATdrreS2pu2v32Q
|
||||||
|
aDIwMk7w+G18hLFK/wZeQi6AQ5CjNHpVMbHNfZ3c0qH5w1nhskIySFpZDZFZP8mx
|
||||||
|
KwJyB1RPg/oN1McAPnpnewRe9kT6nVTd/RLuJ7Z8WM19HnqXPK5KRaJVZ+vgIuAm
|
||||||
|
BxOBWie2yfVhj28C7g5dKcPwqd6bNe3AfiZUHydrKTqtLU1ZPhnHRp7fpqtFmjp7
|
||||||
|
1XkLBidjtR6WN1akHdQLAtlDsh4QENNKcOz4vQIDAQABAoIBAGrgAsZ28gJOcSLq
|
||||||
|
IlGF62zpv0800n6k3tXTT98qtYWqBGn4udKVdxFNYfD7aYNm27OUMSbV9CUWN7Cy
|
||||||
|
lre6fax8lIBxoWfZvU2/ylLUzZREIIf/xxNop6zLTiJUkaYV+P3E8CVt35mPhiLT
|
||||||
|
AYuRL/s8DPnHD9lmdqBxQ4hPVm4Bg7JZxbyN8in3PP1UkdWKxg91O1LYewIZHszq
|
||||||
|
y9BdklKyxQ+fcYP5DD9KkULAjdab48GIxQETrZKp7zV0KiGrjF4Axf5y5yT2jmFT
|
||||||
|
nZ1uZrC1MJTMYyKTBR7wsSpVBMSMUsh5XtxdJo4FuP6g9Kn6AkeQ/Y1shcWVfQgw
|
||||||
|
6009o8ECgYEA8J1PtnVCHxMLiVKZznzvgCe+EV0RkvuB9PGPdfpLfkHa1DKS+FzH
|
||||||
|
80D+Vqe0rQNLudG5Qj53MPghNirGyrjXwTYFW9xCqq9hrzfxEI4xIYOd4gHoPMMQ
|
||||||
|
pfWZylP9GYQp/uoa+e/fcdXRSv1IDLRwJZ5XpMtWAIfvMOyDhbfjehECgYEA2+yp
|
||||||
|
poey1y6RWuaIQd2a/PKuYk9jvLEETiz6q7t63MFd6e9cUYX02cG/6yzz6piTWUtx
|
||||||
|
pk9e9IjclLUgV/twVz8SUgSw5TcqBrMnuIT4yQ5rQNZqiEvpCfgb5itcW7I3ADGy
|
||||||
|
dsz2kgaAm7QVZlndQKIy7xRYBCnCD3VQ+TiWh+0CgYAT3qnKg3xmXIhDWtLgvmh4
|
||||||
|
yM9lV64v2R0uQRR7xaOeVYngpByG7gKFEATw2wCMmQ0T10HZOpdVL+huNLId443N
|
||||||
|
osxmfZXzym/irFf36gYcomXTWBz5h5JEYjfFAZKRHNzq9CIuKaTmHaYe7zOX+P6Z
|
||||||
|
3K2YKkJ74L3b6GwkCr96QQKBgQC6n0iTTSGg4h5skaXcpq2HqnP6br4G9/vcTuTk
|
||||||
|
Z/JpdBk6k2i2sULGqlguu/W8BH89Tf0CEOZWAfGUq2Ln5jE9iAMG4H4v9DDQgKTb
|
||||||
|
OtNW4cp3uburLydw0z7xgagdE80CeCmmEGXIIoZuGlHyiZ1r5HfuU0ghOEI6FeaB
|
||||||
|
pdhvPQKBgEpmHV66wqSzzxmYxKjUu8gl9rIniG8SWXHlvcoGVwt1qdOMtNtvwDgB
|
||||||
|
DnbUbANSjzIfFSqVwlx7nXG1e1yN7F1YuyUa3I5QEm4+5URoTSDghk03LTFH+kfM
|
||||||
|
OUxwE8Su4WnoQc7WjkTG0M3FECAu7TEcF9uqdcEsW+4+JMAhE5oo
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
72
src/test/kotlin/io/provs/ubuntu/keys/base/GpgKtTest.kt
Normal file
72
src/test/kotlin/io/provs/ubuntu/keys/base/GpgKtTest.kt
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package io.provs.ubuntu.keys.base
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import io.provs.ubuntu.keys.KeyPair
|
||||||
|
import io.provs.ubuntu.keys.privateGPGSnakeoilKey
|
||||||
|
import io.provs.ubuntu.keys.publicGPGSnakeoilKey
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class GpgKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun gpgFingerprint_returnsCorrectFingerprint() {
|
||||||
|
// when
|
||||||
|
val fingerprint = Prov.defaultInstance().gpgFingerprint(publicGPGSnakeoilKey())
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals("85052C6954262D61D4E9977E0674104CA81A4905", fingerprint)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun configureGpgKeys() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun configureGpgKeysTrusted() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())), true)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
val trustedKey = a.cmd("gpg -K | grep ultimate").out
|
||||||
|
assertEquals("uid [ultimate] snakeoil <snake@oil.com>", trustedKey?.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@ContainerTest
|
||||||
|
fun configureGpgKeysIsIdempotent() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
|
||||||
|
val res2 = a.configureGpgKeys(KeyPair(Secret(publicGPGSnakeoilKey()), Secret(privateGPGSnakeoilKey())))
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
assertTrue(res2.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
24
src/test/kotlin/io/provs/ubuntu/keys/base/SshKtTest.kt
Normal file
24
src/test/kotlin/io/provs/ubuntu/keys/base/SshKtTest.kt
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package io.provs.ubuntu.keys.base
|
||||||
|
|
||||||
|
import io.provs.Secret
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.ubuntu.keys.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
|
||||||
|
internal class SshKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun configureSshKeys() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.configureSshKeys(KeyPair(Secret(publicSSHSnakeoilKey()), Secret(privateSSHSnakeoilKey())))
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(res.success)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package io.provs.ubuntu.secret.secretSources
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Disabled
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class PromptSecretSourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Disabled // run manually
|
||||||
|
fun secret() {
|
||||||
|
println("Secret: " + PromptSecretSource().secret().plain())
|
||||||
|
}
|
||||||
|
}
|
33
src/test/kotlin/io/provs/ubuntu/user/ProvisionUserKtTest.kt
Normal file
33
src/test/kotlin/io/provs/ubuntu/user/ProvisionUserKtTest.kt
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package io.provs.ubuntu.user
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.ubuntu.keys.*
|
||||||
|
import io.provs.ubuntu.secret.SecretSourceType
|
||||||
|
import io.provs.ubuntu.user.base.configureUser
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.condition.EnabledOnOs
|
||||||
|
import org.junit.jupiter.api.condition.OS
|
||||||
|
|
||||||
|
|
||||||
|
internal class ProvisionUserKtTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@EnabledOnOs(OS.LINUX)
|
||||||
|
fun configureUser() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.configureUser(
|
||||||
|
UserConfig(
|
||||||
|
"testuser",
|
||||||
|
"test@mail.com",
|
||||||
|
KeyPairSource(SecretSourceType.PLAIN, publicGPGSnakeoilKey(), privateGPGSnakeoilKey()),
|
||||||
|
KeyPairSource(SecretSourceType.PLAIN, publicSSHSnakeoilKey(), privateSSHSnakeoilKey())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert(res.success)
|
||||||
|
}
|
||||||
|
}
|
23
src/test/kotlin/io/provs/ubuntu/utils/UtilsKtTest.kt
Normal file
23
src/test/kotlin/io/provs/ubuntu/utils/UtilsKtTest.kt
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package io.provs.ubuntu.utils
|
||||||
|
|
||||||
|
import io.provs.Prov
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class UtilsKtTest {
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
fun printToShell_escapes_successfully() {
|
||||||
|
// given
|
||||||
|
val a = Prov.defaultInstance()
|
||||||
|
|
||||||
|
// when
|
||||||
|
val testString = "test if newline \n and apostrophe's ' \" and special chars !§$%[]\\ äöüß are handled correctly"
|
||||||
|
val res = a.cmd(printToShell(testString)).out
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(testString, res)
|
||||||
|
}
|
||||||
|
}
|
30
src/test/kotlin/io/provs/ubuntu/web/base/WebKtTest.kt
Normal file
30
src/test/kotlin/io/provs/ubuntu/web/base/WebKtTest.kt
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package io.provs.ubuntu.web.base
|
||||||
|
|
||||||
|
import io.provs.test.defaultTestContainer
|
||||||
|
import io.provs.test.tags.ContainerTest
|
||||||
|
import io.provs.ubuntu.filesystem.base.createFile
|
||||||
|
import io.provs.ubuntu.filesystem.base.fileContent
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
internal class WebKtTest {
|
||||||
|
|
||||||
|
@ContainerTest
|
||||||
|
@Test
|
||||||
|
fun downloadFromURL_downloadsFile() {
|
||||||
|
// given
|
||||||
|
val a = defaultTestContainer()
|
||||||
|
val file = "file1"
|
||||||
|
a.createFile("/tmp/" + file, "hello")
|
||||||
|
|
||||||
|
// when
|
||||||
|
val res = a.downloadFromURL("file:///tmp/" + file, "file2", "/tmp")
|
||||||
|
|
||||||
|
// then
|
||||||
|
val res2 = a.fileContent("/tmp/file2")
|
||||||
|
|
||||||
|
assertTrue(res.success)
|
||||||
|
assertEquals("hello", res2)
|
||||||
|
}
|
||||||
|
}
|
7
src/test/resources/WorkplaceConfigExample.json
Normal file
7
src/test/resources/WorkplaceConfigExample.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "MINIMAL",
|
||||||
|
"ssh": null,
|
||||||
|
"gpg": null,
|
||||||
|
"gitUserName": "mygitusername",
|
||||||
|
"gitEmail": "my@git.email"
|
||||||
|
}
|
Loading…
Reference in a new issue