change cli to new format

This commit is contained in:
ansgarz 2022-02-24 19:21:18 +01:00
parent 4c6d3ba3c3
commit 478f058fd7
11 changed files with 110 additions and 135 deletions

View file

@ -6,49 +6,24 @@ import kotlinx.cli.default
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
open class CliTargetParser(name: String) : ArgParser(name) { open class CliTargetParser(name: String) : ArgParser(name) {
val remoteHost by option( val target by argument(
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, ArgType.String,
shortName = "u", description = "target: either 'local' or remote with 'user[:password]@host' (e.g. 'username@somehost.com' without password for ssh-key authorization)",
description = "user for remote provisioning."
) )
val sshWithGopassPath by option( val passwordInteractive by option(
ArgType.String,
shortName = "p",
description = "password stored at gopass path"
)
val sshWithPasswordPrompt by option(
ArgType.Boolean, ArgType.Boolean,
shortName = "i", "password-interactive",
description = "prompt for password interactive" "p",
).default(false) "prompt for password",
val sshWithKey by option(
ArgType.Boolean,
shortName = "k",
description = "provision over ssh using user & ssh key"
).default(false) ).default(false)
} }
fun parseTarget( fun parseTarget(
programName: String = "java -jar provs.jar", programName: String = "provs",
args: Array<String> args: Array<String>
): TargetCliCommand { ): TargetCliCommand {
val parser = CliTargetParser(programName) val parser = CliTargetParser(programName)
parser.parse(args) parser.parse(args)
return TargetCliCommand( return TargetCliCommand(parser.target, parser.passwordInteractive)
parser.localHost,
parser.remoteHost,
parser.userName,
parser.sshWithPasswordPrompt,
parser.sshWithGopassPath,
parser.sshWithKey
)
} }

View file

@ -1,29 +1,64 @@
package org.domaindrivenarchitecture.provs.configuration.domain 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( class TargetCliCommand(
val localHost: Boolean?, val target: String,
val remoteHost: String?, val passwordInteractive: Boolean = false
val userName: String?,
val sshWithPasswordPrompt: Boolean,
val sshWithGopassPath: String?,
val sshWithKey: Boolean
) { ) {
fun isValidLocalhost(): Boolean { private var remoteTarget: RemoteTarget? = null
return (localHost ?: false) && remoteHost == null && userName == null && sshWithGopassPath == null &&
!sshWithPasswordPrompt && !sshWithKey init {
remoteTarget = parseRemoteTarget()
} }
fun hasValidPasswordOption(): Boolean { fun isValidLocalhost(): Boolean {
return (sshWithGopassPath != null) xor sshWithPasswordPrompt xor sshWithKey return target == "local"
} }
fun isValidRemote(): Boolean { fun isValidRemote(): Boolean {
return remoteHost != null && userName != null && hasValidPasswordOption() return (remoteTarget != null)
} }
fun isValid(): Boolean { fun isValid(): Boolean {
return (isValidLocalhost() || isValidRemote()) 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?)
} }

View file

@ -25,12 +25,8 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
return DesktopCliCommand( return DesktopCliCommand(
DesktopType.valueOf(module.name.uppercase()), DesktopType.valueOf(module.name.uppercase()),
TargetCliCommand( TargetCliCommand(
localHost, target,
remoteHost, passwordInteractive
userName,
sshWithPasswordPrompt,
sshWithGopassPath,
sshWithKey
), ),
module.configFileName module.configFileName
) )

View file

@ -5,7 +5,6 @@ import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.core.Secret import org.domaindrivenarchitecture.provs.framework.core.Secret
import org.domaindrivenarchitecture.provs.framework.core.local import org.domaindrivenarchitecture.provs.framework.core.local
import org.domaindrivenarchitecture.provs.framework.core.remote 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.secret.secretSources.PromptSecretSource
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.makeUserSudoerWithNoSudoPasswordRequired
@ -26,15 +25,16 @@ fun createProvInstance(
remoteHostSetSudoWithoutPasswordRequired: Boolean = false remoteHostSetSudoWithoutPasswordRequired: Boolean = false
): Prov { ): Prov {
if (targetCommand.isValid()) { 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()) { if (targetCommand.isValidLocalhost()) {
return local() return local()
} else if (targetCommand.isValidRemote()) { } else if (targetCommand.isValidRemote() && remoteTarget != null) {
return createProvInstanceRemote( return createProvInstanceRemote(
targetCommand.remoteHost!!, remoteTarget.host,
targetCommand.userName!!, remoteTarget.user,
targetCommand.sshWithKey, remoteTarget.password == null,
password, password,
remoteHostSetSudoWithoutPasswordRequired remoteHostSetSudoWithoutPasswordRequired
) )
@ -81,15 +81,13 @@ private fun createProvInstanceRemote(
} }
// todo: consider removal as password can be retrieved by PromptSecretSource
internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? { internal fun retrievePassword(cliCommand: TargetCliCommand): Secret? {
var password: Secret? = null var password: Secret? = null
if (cliCommand.isValidRemote()) { if (cliCommand.isValidRemote() && cliCommand.passwordInteractive) {
if (cliCommand.sshWithPasswordPrompt) {
password = password =
PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret() PromptSecretSource("Password for user $cliCommand.userName!! on $cliCommand.remoteHost!!").secret()
} else if (cliCommand.sshWithGopassPath != null) {
password = GopassSecretSource(cliCommand.sshWithGopassPath).secret()
}
} }
return password return password
} }

View file

@ -24,12 +24,8 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) {
return ServerCliCommand( return ServerCliCommand(
ServerType.valueOf(module.name.uppercase()), ServerType.valueOf(module.name.uppercase()),
TargetCliCommand( TargetCliCommand(
localHost, target,
remoteHost, passwordInteractive
userName,
sshWithPasswordPrompt,
sshWithGopassPath,
sshWithKey
), ),
module.configFileName module.configFileName
) )
@ -41,14 +37,15 @@ class CliArgumentsParser(name: String) : CliTargetParser(name) {
} }
class K3s : ServerSubcommand("k3s", "the k3s module") { class K3s : ServerSubcommand("k3s", "the k3s module") {
val cliConfigFileName by argument( val cliConfigFileName by option(
ArgType.String, ArgType.String,
"configFilename", "config-file",
"c",
"the filename containing the yaml config for k3s" "the filename containing the yaml config for k3s"
) )
override fun execute() { override fun execute() {
super.configFileName = ConfigFileName(cliConfigFileName) super.configFileName = cliConfigFileName?.let { ConfigFileName(it) }
super.parsed = true super.parsed = true
} }
} }

View file

@ -1,64 +1,47 @@
package org.domaindrivenarchitecture.provs.configuration.application package org.domaindrivenarchitecture.provs.configuration.application
import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class CliTargetParserTest { internal class CliTargetParserTest {
@Test
fun parse_localhost_with_default() {
val parseCli = parseTarget(args = emptyArray())
assertFalse(parseCli.isValidLocalhost())
assertFalse(parseCli.isValidRemote())
assertFalse(parseCli.isValid())
}
@Test @Test
fun parse_localhost() { fun parse_localhost() {
val parseCli = parseTarget(args = arrayOf("-l")) val cliCommand = parseTarget(args = arrayOf("local"))
assertTrue(parseCli.isValidLocalhost()) assertTrue(cliCommand.isValidLocalhost())
assertFalse(parseCli.isValidRemote()) assertFalse(cliCommand.isValidRemote())
assertTrue(parseCli.isValid()) assertTrue(cliCommand.isValid())
} }
@Test @Test
fun parse_remote_with_missing_passwordoption() { fun parse_remote_with_given_pasword() {
val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user")) val cliCommand = parseTarget(args = arrayOf("user:mypassword@1.2.3.4"))
assertFalse(parseCli.isValidLocalhost()) assertFalse(cliCommand.isValidLocalhost())
assertEquals("1.2.3.4", parseCli.remoteHost) assertEquals("1.2.3.4", cliCommand.remoteTarget()?.host)
assertEquals("user", parseCli.userName) assertEquals("user", cliCommand.remoteTarget()?.user)
assertFalse(parseCli.isValidRemote()) assertEquals("mypassword", cliCommand.remoteTarget()?.password?.plain())
assertFalse(parseCli.isValid()) assertTrue(cliCommand.isValid())
} }
@Test @Test
fun parse_remote_with_remote_key() { fun parse_remote_with_ssh_key() {
val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-k")) val cliCommand = parseTarget(args = arrayOf("user@1.2.3.4"))
assertFalse(parseCli.isValidLocalhost()) assertFalse(cliCommand.isValidLocalhost())
assertEquals("1.2.3.4", parseCli.remoteHost) assertEquals("1.2.3.4", cliCommand.remoteTarget()?.host)
assertEquals("user", parseCli.userName) assertEquals("user", cliCommand.remoteTarget()?.user)
assertTrue(parseCli.isValid()) assertTrue(cliCommand.isValid())
} }
@Test @Test
fun parse_remote_with_remote_password_prompt() { @Disabled // enable to enter manually the password when prompted
val parseCli = parseTarget(args = arrayOf("-r", "1.2.3.4", "-u", "user", "-i")) 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("1.2.3.4", cliCommand.remoteTarget()?.host)
assertEquals("user", parseCli.userName) assertEquals("user", cliCommand.remoteTarget()?.user)
assertTrue(parseCli.isValid()) 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"))
assertEquals("1.2.3.4", parseCli.remoteHost)
assertEquals("user", parseCli.userName)
assertEquals("gopass/path", parseCli.sshWithGopassPath)
assertTrue(parseCli.isValid())
} }
} }

View file

@ -41,7 +41,7 @@ internal class CliTargetCommandKtTest {
@Test @Test
fun createProvInstance_local() { fun createProvInstance_local() {
// given // given
val cliCommand = TargetCliCommand(true, null, null, false, null, false) val cliCommand = TargetCliCommand("local", false)
// when // when
createProvInstance(cliCommand) createProvInstance(cliCommand)
@ -53,7 +53,7 @@ internal class CliTargetCommandKtTest {
@Test @Test
fun createProvInstance_remote_with_sshKey() { fun createProvInstance_remote_with_sshKey() {
// given // given
val cliCommand = TargetCliCommand(false, "host123", "user123", false, null, true) val cliCommand = TargetCliCommand("user123@host123", false)
// when // when
createProvInstance(cliCommand) createProvInstance(cliCommand)
@ -65,7 +65,7 @@ internal class CliTargetCommandKtTest {
@Test @Test
fun createProvInstance_remote_with_interactive_password_retrieval() { fun createProvInstance_remote_with_interactive_password_retrieval() {
// given // given
val cliCommand = TargetCliCommand(false, "host123", "user123", true, null, false) val cliCommand = TargetCliCommand("user123:sec@host123", false)
// when // when
createProvInstance(cliCommand) createProvInstance(cliCommand)

View file

@ -8,10 +8,10 @@ internal class CliArgumentsParserTest {
@Test @Test
fun parse_cliCommand_with_module_and_local_target() { 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()) assertTrue(cli.isValid())
assertEquals(null, cli.configFile) assertEquals(null, cli.configFile)
assertEquals(true, cli.target.localHost) assertEquals(true, cli.target.isValidLocalhost())
} }
} }

View file

@ -27,7 +27,7 @@ internal class CliWorkplaceKtTest {
val testConfig = DesktopConfig(gitUserName = "gittestuser", gitEmail = "git@test.mail") val testConfig = DesktopConfig(gitUserName = "gittestuser", gitEmail = "git@test.mail")
val cmd = DesktopCliCommand( val cmd = DesktopCliCommand(
DesktopType.BASIC, DesktopType.BASIC,
TargetCliCommand(null, null, null, false, null, false), TargetCliCommand("user@host", false),
ConfigFileName("bla") ConfigFileName("bla")
) )
@ -74,7 +74,7 @@ internal class CliWorkplaceKtTest {
fun provision_workplace_remotely() { fun provision_workplace_remotely() {
// when // when
main(arrayOf("basic", "-i", "-r", "host123.xyz", "-u", "user123", "-c", "testconfig.yaml")) main(arrayOf("basic", "user123:sec@host123.xyz", "-c", "testconfig.yaml"))
// then // then
verify { remote("host123.xyz", "user123", Secret("sec"), any()) } verify { remote("host123.xyz", "user123", Secret("sec"), any()) }
@ -103,7 +103,7 @@ internal class CliWorkplaceKtTest {
System.setErr(PrintStream(errContent)) System.setErr(PrintStream(errContent))
// when // when
main(arrayOf("basic", "-c", "idontexist.yaml", "-r", "remotehost", "-u", "someuser", "-k")) main(arrayOf("basic", "someuser@remotehost", "-c", "idontexist.yaml"))
// then // then
System.setOut(originalOut) System.setOut(originalOut)
@ -130,7 +130,7 @@ internal class CliWorkplaceKtTest {
System.setErr(PrintStream(errContent)) System.setErr(PrintStream(errContent))
// when // 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 // then
System.setOut(originalOut) System.setOut(originalOut)

View file

@ -1,24 +1,15 @@
package org.domaindrivenarchitecture.provs.framework.extensions.workplace 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.DesktopType
import org.domaindrivenarchitecture.provs.desktop.domain.provisionWorkplace import org.domaindrivenarchitecture.provs.desktop.domain.provisionWorkplace
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig 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.Assertions.assertTrue
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
internal class ProvisionWorkplaceKtTest { internal class ProvisionWorkplaceKtTest {
val cmd = DesktopCliCommand(
DesktopType.BASIC,
TargetCliCommand(null, null, null, false, null, false),
ConfigFileName("bla")
)
@Test @Test
@ContainerTest @ContainerTest
fun provisionWorkplace() { fun provisionWorkplace() {

View file

@ -12,7 +12,7 @@ internal class CliArgumentParserTest {
val parser = CliArgumentsParser("test") val parser = CliArgumentsParser("test")
// when // when
val result = parser.parseCommand(args = arrayOf("k3s", "-l", "config.yaml")) val result = parser.parseCommand(args = arrayOf("k3s", "local", "-c", "config.yaml"))
// then // then
assertTrue(result.isValid()) assertTrue(result.isValid())