From b3bdc26e3fe183be2ec12ce823e6f1083b3fcdaa Mon Sep 17 00:00:00 2001 From: az Date: Wed, 1 Dec 2021 21:19:08 +0100 Subject: [PATCH] add server jar for k3s --- build.gradle | 17 +++ .../provs/core/cli/CliCommand.kt | 141 ++++++++++++++++++ .../k3s/application/Application.kt | 13 ++ .../server_software/k3s/application/Cli.kt | 40 ++--- .../provs/core/cli/CliCommandTest.kt | 64 ++++++++ .../k3s/application/CliKtTest.kt | 14 ++ 6 files changed, 262 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Application.kt create mode 100644 src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt create mode 100644 src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliKtTest.kt diff --git a/build.gradle b/build.gradle index b45e326..f4a596d 100644 --- a/build.gradle +++ b/build.gradle @@ -101,6 +101,23 @@ task fatJarLatest(type: Jar) { archiveFileName = 'provs.jar' } +task fatJarK3s(type: Jar) { + from { + configurations.runtimeClasspath.findAll { it.name.endsWith('jar') }.collect { zipTree(it) } + } + exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' + + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) + + manifest { + attributes 'Implementation-Title': 'Fatjar of provs k3s', + 'Implementation-Version': project.version, + 'Main-Class': 'org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application.CliKt' + } + with jar + archiveFileName = 'provs-server.jar' +} + task uberjarWorkplace(type: Jar) { from sourceSets.main.output diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt new file mode 100644 index 0000000..5813328 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt @@ -0,0 +1,141 @@ +package org.domaindrivenarchitecture.provs.core.cli + +import kotlinx.cli.ArgParser +import kotlinx.cli.ArgType +import kotlinx.cli.default +import org.domaindrivenarchitecture.provs.core.Prov +import org.domaindrivenarchitecture.provs.core.Secret +import org.domaindrivenarchitecture.provs.core.local +import org.domaindrivenarchitecture.provs.core.remote +import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.GopassSecretSource +import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.PromptSecretSource +import org.domaindrivenarchitecture.provs.ubuntu.user.base.currentUserCanSudo +import org.domaindrivenarchitecture.provs.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired +import org.domaindrivenarchitecture.provs.ubuntu.user.base.whoami +import kotlin.system.exitProcess + + +class CliCommand( + val localHost: Boolean?, + val remoteHost: String?, + val userName: String?, + val sshWithPasswordPrompt: Boolean, + val sshWithGopassPath: String?, + val sshWithKey: Boolean +) { + fun isValidLocalhost(): Boolean { + return (localHost ?: false) && 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( + programName: String = "java -jar provs.jar", + args: Array +): CliCommand { + val parser = ArgParser(programName) + + val remoteHost by parser.option( + ArgType.String, shortName = + "r", description = "provision to remote host - either localHost or remoteHost must be specified" + ) + val localHost by parser.option( + ArgType.Boolean, shortName = + "l", description = "provision to local machine - either localHost or remoteHost must be specified" + ) + 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) + + return CliCommand( + localHost, remoteHost, userName, sshWithPasswordPrompt, sshWithGopassPath, sshWithKey + ) +} + + +internal fun createProvInstance( + cliCommand: CliCommand, + remoteHostSetSudoWithoutPasswordRequired: Boolean = false +): Prov { + if (cliCommand.isValid()) { + val password: Secret? = if (cliCommand.isValidRemote()) retrievePassword(cliCommand) else null + + if (cliCommand.isValidLocalhost()) { + return local() + } else if (cliCommand.isValidRemote()) { + val host = cliCommand.remoteHost!! + val remoteUser = cliCommand.userName!! + + val prov = + if (cliCommand.sshWithKey) { + remote(host, remoteUser) + } else { + require( + password != null, + { "No password available for provisioning without ssh keys. Either specify provisioning by ssh-keys or provide password." }) + remote(host, remoteUser, password) + } + + if (!prov.currentUserCanSudo()) { + if (remoteHostSetSudoWithoutPasswordRequired) { + require( + password != null, + { "User ${prov.whoami()} not able to sudo on remote machine without password and no password available for the user." }) + prov.makeUserSudoerWithNoSudoPasswordRequired(password) + } else { + throw IllegalStateException("User ${prov.whoami()} not able to sudo on remote machine without password and option not set to enable user to sudo without password.") + } + } + return prov + } else { + throw IllegalArgumentException("Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help.") + } + } else { + println("Invalid command line options.\nPlease use option -h for help.") + exitProcess(1) + } +} + + +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 +} diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Application.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Application.kt new file mode 100644 index 0000000..12fce49 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Application.kt @@ -0,0 +1,13 @@ +package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application + +import org.domaindrivenarchitecture.provs.core.Prov +import org.domaindrivenarchitecture.provs.extensions.server_software.k3s.installK3sServer + + +/** + * Performs use case of provisioning a k3s server + */ +fun Prov.provisionK3s() = task { + installK3sServer() +} + diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Cli.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Cli.kt index 13297f1..bdc6bee 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Cli.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/Cli.kt @@ -1,31 +1,17 @@ -import org.domaindrivenarchitecture.provs.core.remote -import org.domaindrivenarchitecture.provs.extensions.server_software.k3s.applyK3sConfig -import org.domaindrivenarchitecture.provs.extensions.server_software.k3s.infrastructure.apple.appleConfig -import org.domaindrivenarchitecture.provs.extensions.server_software.k3s.infrastructure.apple.checkAppleService -import org.domaindrivenarchitecture.provs.extensions.server_software.k3s.installK3sServer -import org.domaindrivenarchitecture.provs.ubuntu.secret.secretSources.PromptSecretSource +package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application -fun main() { +import org.domaindrivenarchitecture.provs.core.cli.createProvInstance +import org.domaindrivenarchitecture.provs.core.cli.parseCli - val host = "192.168.56.141" - val remoteUser = "usr" - val passwordK3sUser = PromptSecretSource("Enter Password").secret() - val prov = remote(host, remoteUser, passwordK3sUser) - // alternatively run local: val prov = local() +/** + * Provisions a k3s server, either locally or on a remote machine depending on the given arguments. + * + * Get help with option -h + */ +fun main(args: Array) { + val cmd = parseCli("java -jar provs-server.jar", args) + val prov = createProvInstance(cmd) - prov.task { - - installK3sServer() - - // print pods for information purpose - println(cmd("sudo k3s kubectl get pods --all-namespaces").out) - - applyK3sConfig(appleConfig()) - - // print pods for information purpose - println(cmd("sudo k3s kubectl get services").out) - - checkAppleService() - } -} \ No newline at end of file + prov.provisionK3s() +} diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt new file mode 100644 index 0000000..e42e112 --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt @@ -0,0 +1,64 @@ +package org.domaindrivenarchitecture.provs.core.cli + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +internal class CliCommandTest { + + @Test + fun parse_localhost_with_default() { + val parseCli = parseCli(args = emptyArray()) + + assertFalse(parseCli.isValidLocalhost()) + assertFalse(parseCli.isValidRemote()) + assertFalse(parseCli.isValid()) + } + + @Test + fun parse_localhost() { + val parseCli = parseCli(args = arrayOf("-l")) + assertTrue(parseCli.isValidLocalhost()) + assertFalse(parseCli.isValidRemote()) + assertTrue(parseCli.isValid()) + } + + @Test + fun parse_remote_with_missing_passwordoption() { + val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user")) + + assertFalse(parseCli.isValidLocalhost()) + assertEquals("1.2.3.4", parseCli.remoteHost) + assertEquals("user", parseCli.userName) + assertFalse(parseCli.isValidRemote()) + assertFalse(parseCli.isValid()) + } + + @Test + fun parse_remote_with_remote_key() { + val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-k")) + + assertFalse(parseCli.isValidLocalhost()) + assertEquals("1.2.3.4", parseCli.remoteHost) + assertEquals("user", parseCli.userName) + assertTrue(parseCli.isValid()) + } + + @Test + fun parse_remote_with_remote_password_prompt() { + val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-i")) + + assertEquals("1.2.3.4", parseCli.remoteHost) + assertEquals("user", parseCli.userName) + assertTrue(parseCli.isValid()) + } + + @Test + fun parse_remote_with_remote_password_gopass_path() { + val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-p", "gopass/path")) + + assertEquals("1.2.3.4", parseCli.remoteHost) + assertEquals("user", parseCli.userName) + assertEquals("gopass/path", parseCli.sshWithGopassPath) + assertTrue(parseCli.isValid()) + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliKtTest.kt new file mode 100644 index 0000000..c8f8ba9 --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliKtTest.kt @@ -0,0 +1,14 @@ +package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +internal class CliKtTest { + + @Test + @Disabled // run manually -- todo mock execution + fun provision_remotely() { + + main(arrayOf("-r", "192.168.56.141", "-u", "user", "-i")) + } +} \ No newline at end of file