From 478f058fd7ed148daf060884ced27255701e6a3f Mon Sep 17 00:00:00 2001 From: ansgarz Date: Thu, 24 Feb 2022 19:21:18 +0100 Subject: [PATCH] change cli to new format --- .../application/CliTargetParser.kt | 41 +++--------- .../configuration/domain/CliTargetCommand.kt | 59 ++++++++++++---- .../desktop/application/CliArgumentsParser.kt | 8 +-- .../provs/framework/core/cli/CliUtils.kt | 24 +++---- .../server/application/CliArgumentsParser.kt | 15 ++--- .../application/CliTargetParserTest.kt | 67 +++++++------------ .../domain/CliTargetCommandTest.kt | 6 +- .../application/CliArgumentsParserTest.kt | 4 +- .../desktop/application/CliWorkplaceKtTest.kt | 8 +-- .../workplace/ProvisionWorkplaceKtTest.kt | 13 +--- .../application/CliArgumentParserTest.kt | 2 +- 11 files changed, 111 insertions(+), 136 deletions(-) diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParser.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParser.kt index deab71c..5a904a3 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParser.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParser.kt @@ -6,49 +6,24 @@ import kotlinx.cli.default import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand open class CliTargetParser(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( + val target by argument( ArgType.String, - shortName = "p", - description = "password stored at gopass path" + description = "target: either 'local' or remote with 'user[:password]@host' (e.g. 'username@somehost.com' without password for ssh-key authorization)", ) - val sshWithPasswordPrompt by option( + val passwordInteractive 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" + "password-interactive", + "p", + "prompt for password", ).default(false) } fun parseTarget( - programName: String = "java -jar provs.jar", + programName: String = "provs", args: Array ): TargetCliCommand { val parser = CliTargetParser(programName) parser.parse(args) - return TargetCliCommand( - parser.localHost, - parser.remoteHost, - parser.userName, - parser.sshWithPasswordPrompt, - parser.sshWithGopassPath, - parser.sshWithKey - ) + return TargetCliCommand(parser.target, parser.passwordInteractive) } \ No newline at end of file diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommand.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommand.kt index 815657e..0cffca0 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommand.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommand.kt @@ -1,29 +1,64 @@ package org.domaindrivenarchitecture.provs.configuration.domain +import org.domaindrivenarchitecture.provs.framework.core.Secret +import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PlainSecretSource +import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource + + +private const val USER_HOST_DELIMITER = "@" +private const val USER_PW_DELIMITER = ":" + class TargetCliCommand( - val localHost: Boolean?, - val remoteHost: String?, - val userName: String?, - val sshWithPasswordPrompt: Boolean, - val sshWithGopassPath: String?, - val sshWithKey: Boolean + val target: String, + val passwordInteractive: Boolean = false ) { - fun isValidLocalhost(): Boolean { - return (localHost ?: false) && remoteHost == null && userName == null && sshWithGopassPath == null && - !sshWithPasswordPrompt && !sshWithKey + private var remoteTarget: RemoteTarget? = null + + init { + remoteTarget = parseRemoteTarget() } - fun hasValidPasswordOption(): Boolean { - return (sshWithGopassPath != null) xor sshWithPasswordPrompt xor sshWithKey + fun isValidLocalhost(): Boolean { + return target == "local" } fun isValidRemote(): Boolean { - return remoteHost != null && userName != null && hasValidPasswordOption() + return (remoteTarget != null) } fun isValid(): Boolean { return (isValidLocalhost() || isValidRemote()) } + + private fun parseRemoteTarget(): RemoteTarget? { + val user: String? + val host: String? + var password: Secret? = null + + if (!target.contains(USER_HOST_DELIMITER)) { + return null + } + + host = target.substringAfter(USER_HOST_DELIMITER) + + val userPw = target.substringBefore(USER_HOST_DELIMITER) + if (!userPw.contains(USER_PW_DELIMITER)) { + user = userPw + } else { + user = userPw.substringBefore(USER_PW_DELIMITER) + password = PlainSecretSource(userPw.substringAfter(USER_PW_DELIMITER)).secret() + } + if (passwordInteractive) { + password = PromptSecretSource("Password for $user on $host").secretNullable() + } + return RemoteTarget(user, host, password) + } + + fun remoteTarget(): RemoteTarget? { + return remoteTarget + } + + class RemoteTarget(val user: String, val host: String, val password: Secret?) } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParser.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParser.kt index 53fa9f0..8d73978 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParser.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParser.kt @@ -25,12 +25,8 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) { return DesktopCliCommand( DesktopType.valueOf(module.name.uppercase()), TargetCliCommand( - localHost, - remoteHost, - userName, - sshWithPasswordPrompt, - sshWithGopassPath, - sshWithKey + target, + passwordInteractive ), module.configFileName ) diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt index 38372fe..6b3b74a 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/framework/core/cli/CliUtils.kt @@ -5,7 +5,6 @@ import org.domaindrivenarchitecture.provs.framework.core.Prov import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.local import org.domaindrivenarchitecture.provs.framework.core.remote -import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.GopassSecretSource import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.secretSources.PromptSecretSource import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired @@ -26,15 +25,16 @@ fun createProvInstance( remoteHostSetSudoWithoutPasswordRequired: Boolean = false ): Prov { if (targetCommand.isValid()) { - val password: Secret? = if (targetCommand.isValidRemote()) retrievePassword(targetCommand) else null + val password: Secret? = targetCommand.remoteTarget()?.password + val remoteTarget = targetCommand.remoteTarget() if (targetCommand.isValidLocalhost()) { return local() - } else if (targetCommand.isValidRemote()) { + } else if (targetCommand.isValidRemote() && remoteTarget != null) { return createProvInstanceRemote( - targetCommand.remoteHost!!, - targetCommand.userName!!, - targetCommand.sshWithKey, + remoteTarget.host, + remoteTarget.user, + remoteTarget.password == null, password, remoteHostSetSudoWithoutPasswordRequired ) @@ -81,15 +81,13 @@ private fun createProvInstanceRemote( } +// todo: consider removal as password can be retrieved by PromptSecretSource internal fun retrievePassword(cliCommand: TargetCliCommand): 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() - } + if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) { + password = + PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret() + } return password } diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentsParser.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentsParser.kt index c4656fc..716fb10 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentsParser.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentsParser.kt @@ -24,12 +24,8 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) { return ServerCliCommand( ServerType.valueOf(module.name.uppercase()), TargetCliCommand( - localHost, - remoteHost, - userName, - sshWithPasswordPrompt, - sshWithGopassPath, - sshWithKey + target, + passwordInteractive ), module.configFileName ) @@ -41,14 +37,15 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) { } class K3s : ServerSubcommand("k3s", "the k3s module") { - val cliConfigFileName by argument( + val cliConfigFileName by option( ArgType.String, - "configFilename", + "config-file", + "c", "the filename containing the yaml config for k3s" ) override fun execute() { - super.configFileName = ConfigFileName(cliConfigFileName) + super.configFileName = cliConfigFileName?.let { ConfigFileName(it) } super.parsed = true } } diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParserTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParserTest.kt index 66057ef..dee51f2 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParserTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/application/CliTargetParserTest.kt @@ -1,64 +1,47 @@ package org.domaindrivenarchitecture.provs.configuration.application import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test internal class CliTargetParserTest { - @Test - fun parse_localhost_with_default() { - val parseCli = parseTarget(args = emptyArray()) - - assertFalse(parseCli.isValidLocalhost()) - assertFalse(parseCli.isValidRemote()) - assertFalse(parseCli.isValid()) - } - @Test fun parse_localhost() { - val parseCli = parseTarget(args = arrayOf("-l")) - assertTrue(parseCli.isValidLocalhost()) - assertFalse(parseCli.isValidRemote()) - assertTrue(parseCli.isValid()) + val cliCommand = parseTarget(args = arrayOf("local")) + assertTrue(cliCommand.isValidLocalhost()) + assertFalse(cliCommand.isValidRemote()) + assertTrue(cliCommand.isValid()) } @Test - fun parse_remote_with_missing_passwordoption() { - val parseCli = parseTarget(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 = parseTarget(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()) + fun parse_remote_with_given_pasword() { + val cliCommand = parseTarget(args = arrayOf("user:mypassword@1.2.3.4")) + + assertFalse(cliCommand.isValidLocalhost()) + assertEquals("1.2.3.4", cliCommand.remoteTarget()?.host) + assertEquals("user", cliCommand.remoteTarget()?.user) + assertEquals("mypassword", cliCommand.remoteTarget()?.password?.plain()) + assertTrue(cliCommand.isValid()) } @Test - fun parse_remote_with_remote_password_prompt() { - val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-i")) + fun parse_remote_with_ssh_key() { + val cliCommand = parseTarget(args = arrayOf("user@1.2.3.4")) - assertEquals("1.2.3.4", parseCli.remoteHost) - assertEquals("user", parseCli.userName) - assertTrue(parseCli.isValid()) + assertFalse(cliCommand.isValidLocalhost()) + assertEquals("1.2.3.4", cliCommand.remoteTarget()?.host) + assertEquals("user", cliCommand.remoteTarget()?.user) + assertTrue(cliCommand.isValid()) } @Test - fun parse_remote_with_remote_password_gopass_path() { - val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-p", "gopass/path")) + @Disabled // enable to enter manually the password when prompted + fun parse_remote_with_password_prompt() { + val cliCommand = parseTarget(args = arrayOf("user@1.2.3.4", "-p")) - assertEquals("1.2.3.4", parseCli.remoteHost) - assertEquals("user", parseCli.userName) - assertEquals("gopass/path", parseCli.sshWithGopassPath) - assertTrue(parseCli.isValid()) + assertEquals("1.2.3.4", cliCommand.remoteTarget()?.host) + assertEquals("user", cliCommand.remoteTarget()?.user) + assertTrue(cliCommand.isValid()) } } \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt index 96b7c6b..c69409b 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/configuration/domain/CliTargetCommandTest.kt @@ -41,7 +41,7 @@ internal class CliTargetCommandKtTest { @Test fun createProvInstance_local() { // given - val cliCommand = TargetCliCommand(true, null, null, false, null, false) + val cliCommand = TargetCliCommand("local", false) // when createProvInstance(cliCommand) @@ -53,7 +53,7 @@ internal class CliTargetCommandKtTest { @Test fun createProvInstance_remote_with_sshKey() { // given - val cliCommand = TargetCliCommand(false, "host123", "user123", false, null, true) + val cliCommand = TargetCliCommand("user123@host123", false) // when createProvInstance(cliCommand) @@ -65,7 +65,7 @@ internal class CliTargetCommandKtTest { @Test fun createProvInstance_remote_with_interactive_password_retrieval() { // given - val cliCommand = TargetCliCommand(false, "host123", "user123", true, null, false) + val cliCommand = TargetCliCommand("user123:sec@host123", false) // when createProvInstance(cliCommand) diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParserTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParserTest.kt index 1f14888..35e4a32 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParserTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliArgumentsParserTest.kt @@ -8,10 +8,10 @@ internal class CliArgumentsParserTest { @Test fun parse_cliCommand_with_module_and_local_target() { - val cli = CliArgumentsParser("test").parseCommand(args = arrayOf("basic", "-l")) + val cli = CliArgumentsParser("test").parseCommand(args = arrayOf("basic", "local")) assertTrue(cli.isValid()) assertEquals(null, cli.configFile) - assertEquals(true, cli.target.localHost) + assertEquals(true, cli.target.isValidLocalhost()) } } \ No newline at end of file diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliWorkplaceKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliWorkplaceKtTest.kt index 6eaa19b..0abd928 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliWorkplaceKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/desktop/application/CliWorkplaceKtTest.kt @@ -27,7 +27,7 @@ internal class CliWorkplaceKtTest { val testConfig = DesktopConfig(gitUserName = "gittestuser", gitEmail = "git@test.mail") val cmd = DesktopCliCommand( DesktopType.BASIC, - TargetCliCommand(null, null, null, false, null, false), + TargetCliCommand("user@host", false), ConfigFileName("bla") ) @@ -74,7 +74,7 @@ internal class CliWorkplaceKtTest { fun provision_workplace_remotely() { // when - main(arrayOf("basic", "-i", "-r", "host123.xyz", "-u", "user123", "-c", "testconfig.yaml")) + main(arrayOf("basic", "user123:sec@host123.xyz", "-c", "testconfig.yaml")) // then verify { remote("host123.xyz", "user123", Secret("sec"), any()) } @@ -103,7 +103,7 @@ internal class CliWorkplaceKtTest { System.setErr(PrintStream(errContent)) // when - main(arrayOf("basic", "-c", "idontexist.yaml", "-r", "remotehost", "-u", "someuser", "-k")) + main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml")) // then System.setOut(originalOut) @@ -130,7 +130,7 @@ internal class CliWorkplaceKtTest { System.setErr(PrintStream(errContent)) // when - main(arrayOf("basic", "-c", "src/test/resources/InvalidWorkplaceConfig.yaml", "-r", "remotehost", "-u", "someuser", "-k")) + main(arrayOf("basic", "someuser@remotehost", "-c", "src/test/resources/InvalidWorkplaceConfig.yaml")) // then System.setOut(originalOut) diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/workplace/ProvisionWorkplaceKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/workplace/ProvisionWorkplaceKtTest.kt index ab03e0d..c7c5124 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/workplace/ProvisionWorkplaceKtTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/framework/extensions/workplace/ProvisionWorkplaceKtTest.kt @@ -1,24 +1,15 @@ package org.domaindrivenarchitecture.provs.framework.extensions.workplace -import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName -import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand -import org.domaindrivenarchitecture.provs.desktop.domain.DesktopCliCommand -import org.domaindrivenarchitecture.provs.test.defaultTestContainer -import org.domaindrivenarchitecture.provs.test.tags.ContainerTest import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType import org.domaindrivenarchitecture.provs.desktop.domain.provisionWorkplace import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig +import org.domaindrivenarchitecture.provs.test.defaultTestContainer +import org.domaindrivenarchitecture.provs.test.tags.ContainerTest import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test internal class ProvisionWorkplaceKtTest { - val cmd = DesktopCliCommand( - DesktopType.BASIC, - TargetCliCommand(null, null, null, false, null, false), - ConfigFileName("bla") - ) - @Test @ContainerTest fun provisionWorkplace() { diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentParserTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentParserTest.kt index 0ebf8c4..0cf0779 100644 --- a/src/test/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentParserTest.kt +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/server/application/CliArgumentParserTest.kt @@ -12,7 +12,7 @@ internal class CliArgumentParserTest { val parser = CliArgumentsParser("test") // when - val result = parser.parseCommand(args = arrayOf("k3s", "-l", "config.yaml")) + val result = parser.parseCommand(args = arrayOf("k3s", "local", "-c", "config.yaml")) // then assertTrue(result.isValid())