Introduce DDD & add terraform installation

This commit is contained in:
jem 2021-09-07 17:59:03 +02:00
parent 25c60b28a2
commit a5856fb422
13 changed files with 294 additions and 49 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@
/.gradle/
/.idea/
/MyIdeConfig.yaml

View file

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="cli-MyIdeConfig.yaml" type="Application" factoryName="Application">
<option name="ALTERNATIVE_JRE_PATH" value="Kotlin SDK" />
<option name="MAIN_CLASS_NAME" value="org.domaindrivenarchitecture.provs.application.CliKt" />
<module name="provs.main" />
<option name="PROGRAM_PARAMETERS" value="MyIdeConfig.yaml" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

View file

@ -68,6 +68,10 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.2"
implementation "org.jetbrains.kotlinx:kotlinx-cli:0.3.2"
implementation 'com.charleskorn.kaml:kaml:0.35.2'
implementation group: 'com.hierynomus', name: 'sshj', version: '0.31.0'
api "org.slf4j:slf4j-api:1.7.32"
@ -84,7 +88,7 @@ task fatJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'org.domaindrivenarchitecture.provs.core.entry.EntryKt'
'Main-Class': 'org.domaindrivenarchitecture.provs.application.CliKt'
}
archivesBaseName = project.name + '-all'
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
@ -104,7 +108,7 @@ task fatJarLatest(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'org.domaindrivenarchitecture.provs.core.entry.EntryKt'
'Main-Class': 'org.domaindrivenarchitecture.provs.application.CliKt'
}
with jar
archiveFileName = 'provs-fat-latest.jar'
@ -126,7 +130,7 @@ task uberJar(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'org.domaindrivenarchitecture.provs.core.entry.EntryKt'
'Main-Class': 'org.domaindrivenarchitecture.provs.application.CliKt'
}
archiveClassifier = 'uber'
}
@ -147,7 +151,7 @@ task uberJarLatest(type: Jar) {
manifest {
attributes 'Implementation-Title': 'Gradle Jar File Example',
'Implementation-Version': project.version,
'Main-Class': 'org.domaindrivenarchitecture.provs.core.entry.EntryKt'
'Main-Class': 'org.domaindrivenarchitecture.provs.application.CliKt'
}
archiveFileName = 'provs-latest.jar'
}

View file

@ -0,0 +1,18 @@
package org.domaindrivenarchitecture.provs.application
import org.domaindrivenarchitecture.provs.core.Password
import org.domaindrivenarchitecture.provs.core.Prov
import org.domaindrivenarchitecture.provs.core.ProvResult
import org.domaindrivenarchitecture.provs.domain.WorkplaceConfig
import org.domaindrivenarchitecture.provs.domain.WorkplaceType
import org.domaindrivenarchitecture.provs.infrastructure.installDevOps
/**
* Use case for provisioning repos
*/
fun Prov.provision(conf: WorkplaceConfig, let: Password?) = def {
if (conf.type == WorkplaceType.IDE) {
installDevOps()
}
ProvResult(true)
}

View file

@ -0,0 +1,81 @@
package org.domaindrivenarchitecture.provs.application
import org.domaindrivenarchitecture.provs.core.*
import org.domaindrivenarchitecture.provs.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.GopassSecretSource
import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.PromptSecretSource
import java.lang.RuntimeException
/**
* Provisions according to the options either a meissa workplace, reposOnly or gopassOnly.
* Locally or on a remote machine. If remotely, the remote host and remote user are specified by args parameters.
*
* Get help with:
* java -jar build/libs/provs-meissa-latest.jar meissa.provs.application.CliKt main -h
*/
fun main(args: Array<String>) {
val cliCommand = parseCli(args)
if (cliCommand.isValid()) {
provision(cliCommand)
} else {
println("Invalid command line options.\nPlease use option -h for help.")
System.exit(1)
}
}
private fun provision(cliCommand: CliCommand) {
val filename = cliCommand.configFileName
// TODO: mv try-catch down to repository, throw runtime exc.
try {
val conf = getConfig(filename)
val password: Secret? = retrievePassword(cliCommand)
val prov: Prov = createProvInstance(cliCommand, password)
prov.provision(conf, password?.let { Password(password.plain()) })
} catch (e: IllegalArgumentException) {
println(
"Error: File\u001b[31m $filename \u001b[0m was not found.\n" +
"Pls copy file \u001B[31m MeissaWorkplaceConfigExample.yaml \u001B[0m to file \u001B[31m $filename \u001B[0m " +
"and change the content according to your needs.\n"
)
}
}
private fun createProvInstance(
cliCommand: CliCommand,
password: Secret?
): Prov {
if (cliCommand.isValid()) {
if (cliCommand.isValidRemote()) {
val host = cliCommand.remoteHost!!
val remoteUser = cliCommand.userName!!
if (cliCommand.sshWithKey) {
return remote(host, remoteUser)
} else {
require(
password != null,
{ "No password available for provisioning without ssh keys. Either specify provisioning by ssh-keys or provide password." })
return remote(host, remoteUser, password)
}
} else {
return local()
}
} else {
throw RuntimeException("Invalid cliCommand")
}
}
private fun retrievePassword(cliCommand: CliCommand): Secret? {
var password: Secret? = null
if (cliCommand.isValidRemote()) {
if (cliCommand.sshWithPasswordPrompt) {
password =
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret()
} else if (cliCommand.sshWithGopassPath != null) {
password = GopassSecretSource(cliCommand.sshWithGopassPath).secret()
}
}
return password
}

View file

@ -0,0 +1,75 @@
package org.domaindrivenarchitecture.provs.application
import kotlinx.cli.ArgParser
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.optional
class CliCommand(
val remoteHost: String?,
val userName: String?,
val sshWithGopassPath: String?,
val sshWithPasswordPrompt: Boolean,
val sshWithKey: Boolean,
_configFileName: String?
) {
val configFileName: String
init {
configFileName = _configFileName ?: "WorkplaceConfig.yaml"
}
fun isValidLocalhost(): Boolean {
return remoteHost == null && userName == null && sshWithGopassPath == null &&
!sshWithPasswordPrompt && !sshWithKey
}
fun hasValidPasswordOption(): Boolean {
return (sshWithGopassPath != null) xor sshWithPasswordPrompt xor sshWithKey
}
fun isValidRemote(): Boolean {
return remoteHost != null && userName != null && hasValidPasswordOption()
}
fun isValid(): Boolean {
return (isValidLocalhost() || isValidRemote())
}
}
fun parseCli(args: Array<String>): CliCommand {
val parser = ArgParser("meissa.provs.application.CliKt main")
val configFileName by parser.argument(ArgType.String, description = "the config file name to apply").optional()
val remoteHost by parser.option(
ArgType.String, shortName =
"r", description = "provision remote host"
)
val userName by parser.option(
ArgType.String,
shortName = "u",
description = "user for remote provisioning."
)
val sshWithGopassPath by parser.option(
ArgType.String,
shortName = "p",
description = "password stored at gopass path"
)
val sshWithPasswordPrompt by parser.option(
ArgType.Boolean,
shortName = "i",
description = "prompt for password interactive"
).default(false)
val sshWithKey by parser.option(
ArgType.Boolean,
shortName = "k",
description = "provision over ssh using user & ssh key"
).default(false)
parser.parse(args)
val cliCommand =
CliCommand(
remoteHost, userName, sshWithGopassPath, sshWithPasswordPrompt, sshWithKey, configFileName
)
return cliCommand
}

View file

@ -0,0 +1,19 @@
package org.domaindrivenarchitecture.provs.domain
import com.charleskorn.kaml.Yaml
import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPairSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.domaindrivenarchitecture.provs.core.tags.Api
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,
)

View file

@ -0,0 +1,5 @@
package org.domaindrivenarchitecture.provs.domain
enum class WorkplaceType {
MINIMAL, OFFICE, IDE
}

View file

@ -2,7 +2,10 @@ package org.domaindrivenarchitecture.provs.extensions.workplace
import org.domaindrivenarchitecture.provs.core.*
import org.domaindrivenarchitecture.provs.core.processors.RemoteProcessor
import org.domaindrivenarchitecture.provs.domain.WorkplaceConfig
import org.domaindrivenarchitecture.provs.domain.WorkplaceType
import org.domaindrivenarchitecture.provs.extensions.workplace.base.*
import org.domaindrivenarchitecture.provs.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.ubuntu.git.provisionGit
import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall
import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstallFromPpa
@ -18,11 +21,6 @@ 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.
@ -128,7 +126,7 @@ fun provisionRemote(args: Array<String>) {
val pwSecret = PromptSecretSource("Password for user $userName on $host").secret()
val pwFromSecret = Password(pwSecret.plain())
val config = readWorkplaceConfigFromFile() ?: WorkplaceConfig()
val config = getConfig() ?: WorkplaceConfig()
Prov.newInstance(RemoteProcessor(host, userName, pwFromSecret)).provisionWorkplace(
config.type,
config.ssh?.keyPair(),

View file

@ -1,38 +0,0 @@
package org.domaindrivenarchitecture.provs.extensions.workplace
import org.domaindrivenarchitecture.provs.ubuntu.keys.KeyPairSource
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)) }
}

View file

@ -0,0 +1,45 @@
package org.domaindrivenarchitecture.provs.infrastructure
import com.charleskorn.kaml.Yaml
import kotlinx.serialization.json.Json
import org.domaindrivenarchitecture.provs.core.tags.Api
import org.domaindrivenarchitecture.provs.domain.WorkplaceConfig
import java.io.*
internal fun getConfig(filename: String = "WorkplaceConfig.json"): WorkplaceConfig {
val file = File(filename)
require(file.exists(), { "File not found: " + filename })
val config =
try {
// read from file
val inputAsString = BufferedReader(FileReader(filename)).use { it.readText() }
// serializing objects
if (filename.lowercase().endsWith(".yaml")) {
Yaml.default.decodeFromString(WorkplaceConfig.serializer(), inputAsString)
} else {
Json.decodeFromString(WorkplaceConfig.serializer(), inputAsString)
}
} catch (e: FileNotFoundException) {
throw IllegalArgumentException("File not found: " + filename, e)
}
return config
}
@Api
internal fun writeConfig(config: WorkplaceConfig, fileName: String = "WorkplaceConfig.yaml") {
if (fileName.lowercase().endsWith(".yaml")) {
FileWriter(fileName).use {
it.write(
Yaml.default.encodeToString(
WorkplaceConfig.serializer(),
config
)
)
}
} else {
FileWriter(fileName).use { it.write(Json.encodeToString(WorkplaceConfig.serializer(), config)) }
}
}

View file

@ -0,0 +1,25 @@
package org.domaindrivenarchitecture.provs.infrastructure
import org.domaindrivenarchitecture.provs.core.Prov
import org.domaindrivenarchitecture.provs.core.ProvResult
import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs
import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.dirExists
fun Prov.installDevOps() = def {
installTerraform()
}
fun Prov.installTerraform(): ProvResult = def {
val dir = "/usr/lib/tfenv/"
if(!dirExists(dir)) {
createDirs(dir, sudo = true)
cmd("git clone https://github.com/tfutils/tfenv.git " + dir, sudo = true)
cmd("rm " + dir + ".git/ -rf", sudo = true)
cmd("ln -s " + dir + "bin/* /usr/local/bin", sudo = true)
}
cmd ("tfenv install", sudo = true)
cmd ("tfenv install latest:^0.13", sudo = true)
cmd ("tfenv use latest:^0.13", sudo = true)
}

View file

@ -1,6 +1,7 @@
package org.domaindrivenarchitecture.provs.extensions.workplace
import org.domaindrivenarchitecture.provs.core.Password
import org.domaindrivenarchitecture.provs.domain.WorkplaceType
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.domaindrivenarchitecture.provs.test.defaultTestContainer