From 9dd8f55de8ccdfcccded0d277e8e105e176a097c Mon Sep 17 00:00:00 2001 From: az Date: Mon, 6 Dec 2021 15:04:41 +0100 Subject: [PATCH] refactor cli parse and add cli command for k3d --- build.gradle | 4 + .../core/cli/{CliCommand.kt => CliUtils.kt} | 99 ++++--------------- .../provs/core/cli/TargetCliCommand.kt | 56 +++++++++++ .../provs/core/cli/TargetParser.kt | 46 +++++++++ .../server_software/k3s/application/Cli.kt | 17 +++- .../k3s/application/CliK3sArgumentsParser.kt | 19 ++++ .../k3s/application/CliK3sCommand.kt | 36 +++++++ ...CommandTest.kt => TargetCliCommandTest.kt} | 14 +-- .../k3s/application/CliK3sCommandKtTest.kt | 48 +++++++++ 9 files changed, 250 insertions(+), 89 deletions(-) rename src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/{CliCommand.kt => CliUtils.kt} (50%) create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommand.kt create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetParser.kt create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sArgumentsParser.kt create mode 100644 src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommand.kt rename src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/{CliCommandTest.kt => TargetCliCommandTest.kt} (75%) create mode 100644 src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommandKtTest.kt diff --git a/build.gradle b/build.gradle index e673e27..1db101e 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,10 @@ test { } useJUnitPlatform { + def excludedTags = System.getProperty("excludeTags") + if (System.getProperty("excludeTags") != null) { + excludeTags(excludedTags) + } if (System.getenv("CI_JOB_TOKEN") != null) { excludeTags('containernonci') } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliUtils.kt similarity index 50% rename from src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt rename to src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliUtils.kt index 0e1e8b2..bdbde81 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommand.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliUtils.kt @@ -1,8 +1,5 @@ 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 @@ -15,85 +12,31 @@ 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 - ) -} - - +/** + * Returns a Prov instance according to the targetCommand. + * E.g. it returns a local Prov instance if targetCommand.isValidLocalhost() is true or + * returns a remote Prov instance if targetCommand.isValidRemote() is true. + * + * If the target is remote and if parameter remoteHostSetSudoWithoutPasswordRequired is set to true, + * it will enable sudo without password on the remote machine (in case this was not yet enabled). + */ internal fun createProvInstance( - cliCommand: CliCommand, + targetCommand: TargetCliCommand, remoteHostSetSudoWithoutPasswordRequired: Boolean = false ): Prov { - if (cliCommand.isValid()) { - val password: Secret? = if (cliCommand.isValidRemote()) retrievePassword(cliCommand) else null + if (targetCommand.isValid()) { + val password: Secret? = if (targetCommand.isValidRemote()) retrievePassword(targetCommand) else null - if (cliCommand.isValidLocalhost()) { + if (targetCommand.isValidLocalhost()) { return local() - } else if (cliCommand.isValidRemote()) { - return createProvInstanceRemote(cliCommand.remoteHost!!, cliCommand.userName!!, cliCommand.sshWithKey, password, remoteHostSetSudoWithoutPasswordRequired) + } else if (targetCommand.isValidRemote()) { + return createProvInstanceRemote( + targetCommand.remoteHost!!, + targetCommand.userName!!, + targetCommand.sshWithKey, + password, + remoteHostSetSudoWithoutPasswordRequired + ) } else { throw IllegalArgumentException("Error: neither a valid localHost nor a valid remoteHost was specified! Use option -h for help.") } @@ -134,7 +77,7 @@ private fun createProvInstanceRemote( } -internal fun retrievePassword(cliCommand: CliCommand): Secret? { +internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? { var password: Secret? = null if (cliCommand.isValidRemote()) { if (cliCommand.sshWithPasswordPrompt) { diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommand.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommand.kt new file mode 100644 index 0000000..585bc5d --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommand.kt @@ -0,0 +1,56 @@ +package org.domaindrivenarchitecture.provs.core.cli + +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 TargetCliCommand( + 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 parseTarget( + programName: String = "java -jar provs.jar", + args: Array +): TargetCliCommand { + val parser = TargetParser(programName) + parser.parse(args) + + return TargetCliCommand( + parser.localHost, + parser.remoteHost, + parser.userName, + parser.sshWithPasswordPrompt, + parser.sshWithGopassPath, + parser.sshWithKey + ) +} diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetParser.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetParser.kt new file mode 100644 index 0000000..5a22400 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetParser.kt @@ -0,0 +1,46 @@ +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 + +open class TargetParser(name: String) : ArgParser(name) { + val remoteHost by option( + ArgType.String, shortName = + "r", description = "provision to remote host - either localHost or remoteHost must be specified" + ) + val localHost by option( + ArgType.Boolean, shortName = + "l", description = "provision to local machine - either localHost or remoteHost must be specified" + ) + val userName by option( + ArgType.String, + shortName = "u", + description = "user for remote provisioning." + ) + val sshWithGopassPath by option( + ArgType.String, + shortName = "p", + description = "password stored at gopass path" + ) + val sshWithPasswordPrompt by option( + ArgType.Boolean, + shortName = "i", + description = "prompt for password interactive" + ).default(false) + val sshWithKey by option( + ArgType.Boolean, + shortName = "k", + description = "provision over ssh using user & ssh key" + ).default(false) +} 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 bdc6bee..25e7f42 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,7 +1,8 @@ package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application import org.domaindrivenarchitecture.provs.core.cli.createProvInstance -import org.domaindrivenarchitecture.provs.core.cli.parseCli +import org.domaindrivenarchitecture.provs.extensions.server_software.k3s.domain.installK3sAsContainers +import kotlin.system.exitProcess /** @@ -10,8 +11,16 @@ import org.domaindrivenarchitecture.provs.core.cli.parseCli * Get help with option -h */ fun main(args: Array) { - val cmd = parseCli("java -jar provs-server.jar", args) - val prov = createProvInstance(cmd) - prov.provisionK3s() + val cmd = parseServerArguments("java -jar provs-server.jar", args) + if (!cmd.isValid()) { + println("Arguments are not valid, pls try -h for help.") + exitProcess(1) + } + val prov = createProvInstance(cmd.target) + + when (cmd.type()) { + CliK3sArgumentsParser.K3sType.K3S -> prov.provisionK3s() + CliK3sArgumentsParser.K3sType.K3D -> prov.installK3sAsContainers() + } } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sArgumentsParser.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sArgumentsParser.kt new file mode 100644 index 0000000..89f6d5b --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sArgumentsParser.kt @@ -0,0 +1,19 @@ +package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application + +import kotlinx.cli.ArgType +import kotlinx.cli.default +import org.domaindrivenarchitecture.provs.core.cli.TargetParser + +class CliK3sArgumentsParser(name: String) : TargetParser(name) { + + enum class K3sType() { + K3S, K3D + } + + val type by option( + ArgType.String, + "type", + "t", + "either k3s (for standalone) or k3d for k3s running in a container" + ).default("k3s") +} \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommand.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommand.kt new file mode 100644 index 0000000..61764f9 --- /dev/null +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommand.kt @@ -0,0 +1,36 @@ +package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application + +import org.domaindrivenarchitecture.provs.core.cli.TargetCliCommand + + +class ServerCliCommand(private val k3sType: String, val target: TargetCliCommand) { + fun isValid(): Boolean { + return target.isValid() && hasValidK3sType() + } + fun hasValidK3sType(): Boolean { + return CliK3sArgumentsParser.K3sType.values().map { it.name }.contains(k3sType.uppercase()) + } + fun type() = CliK3sArgumentsParser.K3sType.valueOf(k3sType.uppercase()) + +} + +fun parseServerArguments( + programName: String = "java -jar provs.jar", + args: Array +): ServerCliCommand { + val parser = CliK3sArgumentsParser(programName) + parser.parse(args) + + return ServerCliCommand( + parser.type, + TargetCliCommand( + parser.localHost, + parser.remoteHost, + parser.userName, + parser.sshWithPasswordPrompt, + parser.sshWithGopassPath, + parser.sshWithKey + )) +} + + diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommandTest.kt similarity index 75% rename from src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt rename to src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommandTest.kt index e42e112..9bcdfef 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/CliCommandTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/core/cli/TargetCliCommandTest.kt @@ -3,11 +3,11 @@ package org.domaindrivenarchitecture.provs.core.cli import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test -internal class CliCommandTest { +internal class TargetCliCommandTest { @Test fun parse_localhost_with_default() { - val parseCli = parseCli(args = emptyArray()) + val parseCli = parseTarget(args = emptyArray()) assertFalse(parseCli.isValidLocalhost()) assertFalse(parseCli.isValidRemote()) @@ -16,7 +16,7 @@ internal class CliCommandTest { @Test fun parse_localhost() { - val parseCli = parseCli(args = arrayOf("-l")) + val parseCli = parseTarget(args = arrayOf("-l")) assertTrue(parseCli.isValidLocalhost()) assertFalse(parseCli.isValidRemote()) assertTrue(parseCli.isValid()) @@ -24,7 +24,7 @@ internal class CliCommandTest { @Test fun parse_remote_with_missing_passwordoption() { - val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user")) + val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user")) assertFalse(parseCli.isValidLocalhost()) assertEquals("1.2.3.4", parseCli.remoteHost) @@ -35,7 +35,7 @@ internal class CliCommandTest { @Test fun parse_remote_with_remote_key() { - val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-k")) + val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-k")) assertFalse(parseCli.isValidLocalhost()) assertEquals("1.2.3.4", parseCli.remoteHost) @@ -45,7 +45,7 @@ internal class CliCommandTest { @Test fun parse_remote_with_remote_password_prompt() { - val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-i")) + val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-i")) assertEquals("1.2.3.4", parseCli.remoteHost) assertEquals("user", parseCli.userName) @@ -54,7 +54,7 @@ internal class CliCommandTest { @Test fun parse_remote_with_remote_password_gopass_path() { - val parseCli = parseCli(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-p", "gopass/path")) + val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-p", "gopass/path")) assertEquals("1.2.3.4", parseCli.remoteHost) assertEquals("user", parseCli.userName) diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommandKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommandKtTest.kt new file mode 100644 index 0000000..e0334a4 --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/extensions/server_software/k3s/application/CliK3sCommandKtTest.kt @@ -0,0 +1,48 @@ +package org.domaindrivenarchitecture.provs.extensions.server_software.k3s.application + +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* + +internal class CliK3sCommandKtTest { + + @Test + fun test_parseServerArguments_are_valid_for_k3s() { + // when + val cmd = parseServerArguments(args = arrayOf("-l", "-t", "k3s")) + + // then + assertTrue(cmd.isValid()) + assertEquals(CliK3sArgumentsParser.K3sType.K3S, cmd.type()) + } + + @Test + fun test_parseServerArguments_are_invalid_without_target() { + // when + val cmd = parseServerArguments(args = arrayOf("-t", "k3s")) + + // then + assertFalse(cmd.isValid()) + assertEquals(CliK3sArgumentsParser.K3sType.K3S, cmd.type()) + } + + @Test + fun test_parseServerArguments_has_default_type_k3s() { + // when + val cmd = parseServerArguments(args = arrayOf("-l")) + + // then + assertTrue(cmd.isValid()) + assertEquals(CliK3sArgumentsParser.K3sType.K3S, cmd.type()) + } + + @Test + fun test_parseServerArguments_are_valid_for_k3d() { + // when + val cmd = parseServerArguments(args = arrayOf("-l", "-t", "k3d")) + + // then + assertTrue(cmd.isValid()) + assertEquals(CliK3sArgumentsParser.K3sType.K3D, cmd.type()) + } +} \ No newline at end of file