diff --git a/.gitignore b/.gitignore index 7041416..de9beb0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.gradle/ /.idea/ +/MyIdeConfig.yaml diff --git a/.idea-configs/cli-MyIdeConfig.yaml.run.xml b/.idea-configs/cli-MyIdeConfig.yaml.run.xml new file mode 100644 index 0000000..d09b3c6 --- /dev/null +++ b/.idea-configs/cli-MyIdeConfig.yaml.run.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index ddab90a..f403fd5 100644 --- a/build.gradle +++ b/build.gradle @@ -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' } @@ -183,4 +187,4 @@ publishing { mavenLocal() } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/application/Application.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/application/Application.kt new file mode 100644 index 0000000..4646c3c --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/application/Application.kt @@ -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) +} diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/application/Cli.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/application/Cli.kt new file mode 100644 index 0000000..8ca937f --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/application/Cli.kt @@ -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) { + 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 +} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/application/CliCommand.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/application/CliCommand.kt new file mode 100644 index 0000000..4f1bf37 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/application/CliCommand.kt @@ -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): 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 +} diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/domain/WorkplaceConfig.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/domain/WorkplaceConfig.kt new file mode 100644 index 0000000..58a6746 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/domain/WorkplaceConfig.kt @@ -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, +) + diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/domain/WorkplaceType.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/domain/WorkplaceType.kt new file mode 100644 index 0000000..c241147 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/domain/WorkplaceType.kt @@ -0,0 +1,5 @@ +package org.domaindrivenarchitecture.provs.domain + +enum class WorkplaceType { + MINIMAL, OFFICE, IDE +} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplace.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplace.kt index f51ee21..4159c72 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplace.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplace.kt @@ -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) { 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(), diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/WorkplaceConfig.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/WorkplaceConfig.kt deleted file mode 100644 index de75195..0000000 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/WorkplaceConfig.kt +++ /dev/null @@ -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)) } -} diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/infrastructure/ConfigRepository.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/infrastructure/ConfigRepository.kt new file mode 100644 index 0000000..a23cb16 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/infrastructure/ConfigRepository.kt @@ -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)) } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/infrastructure/DevOps.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/infrastructure/DevOps.kt new file mode 100644 index 0000000..b176d47 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/infrastructure/DevOps.kt @@ -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) +} + diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplaceKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplaceKtTest.kt index 752a210..74834db 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplaceKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/workplace/ProvisionWorkplaceKtTest.kt @@ -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