add modules to desktop cli

merge-requests/1/merge
ansgarz 2 years ago
parent c8e880eba4
commit 70b411baa8

@ -1,6 +1,3 @@
# type is required
type: MINIMAL # MINIMAL, OFFICE or IDE
# fields below are optional, either remove them or update them with your data
ssh:
sourceType: FILE # FILE or GOPASS

@ -1,11 +1,7 @@
package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.serialization.SerializationException
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.desktop.domain.provisionWorkplace
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopConfig
import org.domaindrivenarchitecture.provs.desktop.domain.provisionDesktop
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
import java.io.FileNotFoundException
import kotlin.system.exitProcess
@ -15,7 +11,7 @@ import kotlin.system.exitProcess
*/
fun main(args: Array<String>) {
val cmd = CliArgumentsParser("java -jar provs-desktop.jar").parseWorkplaceArguments(args)
val cmd = CliArgumentsParser("java -jar provs-desktop.jar").parseCommand(args)
if (!cmd.isValid()) {
println("Arguments are not valid, pls try option -h for help.")
exitProcess(1)

@ -1,35 +1,29 @@
package org.domaindrivenarchitecture.provs.desktop.application
import kotlinx.cli.ArgType
import kotlinx.cli.default
import kotlinx.cli.multiple
import kotlinx.cli.Subcommand
import org.domaindrivenarchitecture.provs.configuration.application.CliTargetParser
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.desktop.domain.Scope
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopCliCommand
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType
open class CliArgumentsParser(name: String) : CliTargetParser(name) {
val configFileName by argument(
ArgType.String,
"configFilename",
"the filename containing the yaml config for the desktop"
)
private val modules: List<DesktopSubcommand> = listOf(Basic(), Office(), Ide())
val scopes by option (
type = ArgType.Choice<Scope>(),
shortName = "s",
fullName = "scope",
description = "only provision component in scope."
).multiple()
init {
subcommands(*modules.toTypedArray())
}
fun parseWorkplaceArguments(args: Array<String>): DesktopCliCommand {
fun parseCommand(args: Array<String>): DesktopCliCommand {
super.parse(args)
val module = modules.first { it.parsed }
return DesktopCliCommand(
ConfigFileName(configFileName),
scopes,
DesktopType.valueOf(module.name.uppercase()),
TargetCliCommand(
localHost,
remoteHost,
@ -37,7 +31,28 @@ open class CliArgumentsParser(name: String) : CliTargetParser(name) {
sshWithPasswordPrompt,
sshWithGopassPath,
sshWithKey
)
),
module.configFileName
)
}
abstract class DesktopSubcommand(name: String, description: String) : Subcommand(name, description) {
var parsed: Boolean = false
var configFileName: ConfigFileName? = null
val cliConfigFileName by option(
ArgType.String,
"config-file",
"c",
"the filename containing the yaml config",
)
override fun execute() {
configFileName = cliConfigFileName?.let { ConfigFileName(it) }
parsed = true
}
}
class Basic : DesktopSubcommand("basic", "basic desktop for a user")
class Office : DesktopSubcommand("office", "includes office software like Thunderbird, LibreOffice, etc")
class Ide : DesktopSubcommand("ide", "includes office software as well as ides like VSCode, etc")
}

@ -1,21 +1,15 @@
package org.domaindrivenarchitecture.provs.desktop.application
package org.domaindrivenarchitecture.provs.desktop.domain
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.desktop.domain.Scope
class DesktopCliCommand(
val configFile: ConfigFileName,
val scopes: List<Scope>,
val type: DesktopType,
val target: TargetCliCommand,
val configFile: ConfigFileName?,
) {
fun isValid(): Boolean {
return configFile.fileName.isNotEmpty() && target.isValid()
}
fun haScope(): Boolean {
return scopes.isNotEmpty()
return target.isValid()
}
}

@ -6,7 +6,6 @@ import kotlinx.serialization.Serializable
@Serializable
class DesktopConfig(
val type: WorkplaceType = WorkplaceType.MINIMAL,
val ssh: KeyPairSource? = null,
val gpg: KeyPairSource? = null,
val gitUserName: String? = null,

@ -1,6 +1,5 @@
package org.domaindrivenarchitecture.provs.desktop.domain
import org.domaindrivenarchitecture.provs.desktop.application.DesktopCliCommand
import org.domaindrivenarchitecture.provs.desktop.infrastructure.*
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.domaindrivenarchitecture.provs.framework.core.Prov
@ -14,13 +13,12 @@ import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.base.gpgFingerpr
import org.domaindrivenarchitecture.provs.framework.ubuntu.keys.provisionKeys
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.currentUserCanSudo
import org.domaindrivenarchitecture.provs.framework.ubuntu.user.base.whoami
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
fun provisionDesktop(prov: Prov, cmd: DesktopCliCommand) {
// retrieve config
val conf = getConfig(cmd.configFile.fileName)
val conf = if (cmd.configFile != null) getConfig(cmd.configFile.fileName) else DesktopConfig()
with(conf) {
prov.provisionWorkplace(type, ssh?.keyPair(), gpg?.keyPair(), gitUserName, gitEmail, cmd)
prov.provisionWorkplace(cmd.type, ssh?.keyPair(), gpg?.keyPair(), gitUserName, gitEmail)
}
}
@ -33,106 +31,85 @@ fun provisionDesktop(prov: Prov, cmd: DesktopCliCommand) {
* Prerequisites: user must be able to sudo without entering the password
*/
fun Prov.provisionWorkplace(
workplaceType: WorkplaceType = WorkplaceType.MINIMAL,
desktopType: DesktopType = DesktopType.BASIC,
ssh: KeyPair? = null,
gpg: KeyPair? = null,
gitUserName: String? = null,
gitEmail: String? = null,
cmd: DesktopCliCommand
) = requireAll {
if (!currentUserCanSudo()) {
throw Exception("Current user ${whoami()} cannot execute sudo without entering a password! This is necessary to execute provisionWorkplace")
}
if (cmd.haScope()) {
if (cmd.scopes.contains(Scope.PROVS)) {
downloadFromURL(
url="https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/2046149473/artifacts/file/build/libs/provs-server.jar",
filename = "provs-server.jar",
path = "/usr/local/bin/",
sha256sum = "cec1c8762ce310694bacef587ad26b3bb7b8482a8548330ccaf9c9d3eb052409",
sudo = true
)
downloadFromURL(
url="https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/2046149473/artifacts/file/build/libs/provs-desktop.jar",
filename = "provs-desktop.jar",
path = "/usr/local/bin/",
sha256sum = "61bad1380809325aca95bfbcb7cf27928ee070ed886c5de7e300797961d1fa58",
sudo = true
)
cmd("chmod 755 /usr/local/bin/provs-server.jar" , sudo = true)
cmd("chmod 755 /usr/local/bin/provs-desktop.jar", sudo = true)
}
ProvResult(true)
} else {
aptInstall(KEY_MANAGEMENT)
aptInstall(VERSION_MANAGEMENT)
aptInstall(NETWORK_TOOLS)
aptInstall(KEY_MANAGEMENT)
aptInstall(VERSION_MANAGEMENT)
aptInstall(NETWORK_TOOLS)
provisionKeys(gpg, ssh)
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
provisionKeys(gpg, ssh)
provisionGit(gitUserName ?: whoami(), gitEmail, gpg?.let { gpgFingerprint(it.publicKey.plain()) })
installVirtualBoxGuestAdditions()
installVirtualBoxGuestAdditions()
aptPurge(
"remove-power-management xfce4-power-manager " +
"xfce4-power-manager-plugins xfce4-power-manager-data"
)
aptPurge("abiword gnumeric")
aptPurge("popularity-contest")
aptPurge(
"remove-power-management xfce4-power-manager " +
"xfce4-power-manager-plugins xfce4-power-manager-data"
)
aptPurge("abiword gnumeric")
aptPurge("popularity-contest")
configureNoSwappiness()
configureNoSwappiness()
configureBash()
configureBash()
if (workplaceType == WorkplaceType.OFFICE || workplaceType == WorkplaceType.IDE) {
aptInstall(KEY_MANAGEMENT_GUI)
aptInstall(BASH_UTILS)
aptInstall(OS_ANALYSIS)
aptInstall(ZIP_UTILS)
aptInstall(PASSWORD_TOOLS)
if (desktopType == DesktopType.OFFICE || desktopType == DesktopType.IDE) {
aptInstall(KEY_MANAGEMENT_GUI)
aptInstall(BASH_UTILS)
aptInstall(OS_ANALYSIS)
aptInstall(ZIP_UTILS)
aptInstall(PASSWORD_TOOLS)
aptInstall(BROWSER)
aptInstall(EMAIL_CLIENT)
aptInstall(OFFICE_SUITE)
aptInstall(CLIP_TOOLS)
aptInstall(BROWSER)
aptInstall(EMAIL_CLIENT)
aptInstall(OFFICE_SUITE)
aptInstall(CLIP_TOOLS)
installZimWiki()
installGopass()
aptInstallFromPpa("nextcloud-devs", "client", "nextcloud-client")
installZimWiki()
installGopass()
aptInstallFromPpa("nextcloud-devs", "client", "nextcloud-client")
optional {
aptInstall(DRAWING_TOOLS)
}
optional {
aptInstall(DRAWING_TOOLS)
}
aptInstall(SPELLCHECKING_DE)
aptInstall(SPELLCHECKING_DE)
installRedshift()
configureRedshift()
}
installRedshift()
configureRedshift()
if (workplaceType == WorkplaceType.IDE) {
installProvsBinaries()
}
aptInstall(JAVA_JDK)
if (desktopType == DesktopType.IDE) {
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
aptInstall(JAVA_JDK)
installDocker()
aptInstall(OPEN_VPM)
aptInstall(OPENCONNECT)
aptInstall(VPNC)
// IDEs
installVSC("python", "clojure")
aptInstall(CLOJURE_TOOLS)
installShadowCljs()
installDocker()
installIntelliJ()
// IDEs
installVSC("python", "clojure")
aptInstall(CLOJURE_TOOLS)
installShadowCljs()
installDevOps()
installIntelliJ()
installPython()
}
ProvResult(true)
installDevOps()
installPython()
}
ProvResult(true)
}

@ -1,5 +1,5 @@
package org.domaindrivenarchitecture.provs.desktop.domain
enum class WorkplaceType {
MINIMAL, OFFICE, IDE
enum class DesktopType {
BASIC, OFFICE, IDE
}

@ -0,0 +1,33 @@
package org.domaindrivenarchitecture.provs.desktop.infrastructure
import org.domaindrivenarchitecture.provs.framework.core.Prov
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.createDirs
import org.domaindrivenarchitecture.provs.framework.ubuntu.web.base.downloadFromURL
fun Prov.installProvsBinaries() = task {
// check for latest stable release on: https://gitlab.com/domaindrivenarchitecture/provs/-/releases
// release 0.9.6
val jobId = "2083873496"
val installationPath = " /usr/local/bin/"
val provsServerSha256sum = "1127d0a939e1d3eec8e15cd3969f565732b89d9ca10bbaf134840d25aeb3f03b"
val provsDesktopSha256sum = "626f1e01fca5845a54ddd1e645e52bb4b05d04a4cfa060cd18f1ad15a5d387ad"
createDirs(installationPath, sudo = true)
downloadFromURL(
"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/$jobId/artifacts/raw/build/libs/provs-server.jar",
path = installationPath,
filename = "provs-server.jar",
sha256sum = provsServerSha256sum,
sudo = true
)
downloadFromURL(
"https://gitlab.com/domaindrivenarchitecture/provs/-/jobs/$jobId/artifacts/raw/build/libs/provs-desktop.jar",
path = installationPath,
filename = "provs-desktop.jar",
sha256sum = provsDesktopSha256sum,
sudo = true
)
}

@ -0,0 +1,15 @@
package org.domaindrivenarchitecture.provs.framework.core.processors
class DummyProcessor : Processor {
override fun x(vararg args: String): ProcessResult
{
return ProcessResult(0, args = args)
}
override fun xNoLog(vararg args: String): ProcessResult
{
return ProcessResult(0, args = args)
}
}

@ -8,11 +8,10 @@ import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
import org.domaindrivenarchitecture.provs.server.domain.ServerCliCommand
import org.domaindrivenarchitecture.provs.server.domain.ServerType
class CliArgumentsParser(
name: String
) : CliTargetParser(name) {
class CliArgumentsParser(name: String) : CliTargetParser(name) {
private val modules: List<ServerSubcommand> = listOf(K3s(), K3d())
init {
subcommands(*modules.toTypedArray())
}
@ -36,12 +35,12 @@ class CliArgumentsParser(
)
}
abstract class ServerSubcommand(name: String, description: String): Subcommand(name, description) {
abstract class ServerSubcommand(name: String, description: String) : Subcommand(name, description) {
var parsed: Boolean = false
var configFileName: ConfigFileName? = null
}
class K3s: ServerSubcommand("k3s", "the k3s module") {
class K3s : ServerSubcommand("k3s", "the k3s module") {
val cliConfigFileName by argument(
ArgType.String,
"configFilename",
@ -54,13 +53,12 @@ class CliArgumentsParser(
}
}
class K3d: ServerSubcommand("k3d", "the k3s module") {
class K3d : ServerSubcommand("k3d", "the k3s module") {
override fun execute() {
TODO("Not yet implemented")
}
}
}

@ -0,0 +1,17 @@
package org.domaindrivenarchitecture.provs.desktop.application
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
internal class CliArgumentsParserTest {
@Test
fun parse_cliCommand_with_module_and_local_target() {
val cli = CliArgumentsParser("test").parseCommand(args = arrayOf("basic", "-l"))
assertTrue(cli.isValid())
assertEquals(null, cli.configFile)
assertEquals(true, cli.target.localHost)
}
}

@ -4,13 +4,14 @@ import ch.qos.logback.classic.Level
import io.mockk.*
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.desktop.domain.DesktopConfig
import org.domaindrivenarchitecture.provs.desktop.domain.WorkplaceType
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.framework.core.*
import org.domaindrivenarchitecture.provs.framework.core.cli.retrievePassword
import org.domaindrivenarchitecture.provs.framework.core.processors.PrintOnlyProcessor
import org.domaindrivenarchitecture.provs.framework.core.processors.DummyProcessor
import org.domaindrivenarchitecture.provs.test.setRootLoggingLevel
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Assertions.assertEquals
@ -23,32 +24,32 @@ internal class CliWorkplaceKtTest {
companion object {
val testConfig = DesktopConfig(WorkplaceType.MINIMAL, gitUserName = "gittestuser", gitEmail = "git@test.mail")
val testConfig = DesktopConfig(gitUserName = "gittestuser", gitEmail = "git@test.mail")
val cmd = DesktopCliCommand(
ConfigFileName("bla"),
listOf(),
TargetCliCommand(null, null, null, false, null, false)
DesktopType.BASIC,
TargetCliCommand(null, null, null, false, null, false),
ConfigFileName("bla")
)
@BeforeAll
@JvmStatic
internal fun beforeAll() {
val printOnlyProv = Prov.newInstance(PrintOnlyProcessor())
val dummyProv = Prov.newInstance(DummyProcessor())
mockkObject(Prov)
every { Prov.newInstance(any(), any(), any(), any(), ) } returns printOnlyProv
every { Prov.newInstance(any(), any(), any(), any(), ) } returns dummyProv
mockkStatic(::local)
every { local() } returns printOnlyProv
every { local() } returns dummyProv
mockkStatic(::remote)
every { remote(any(), any(), any(), any()) } returns printOnlyProv
every { remote(any(), any(), any(), any()) } returns dummyProv
mockkStatic(::getConfig)
every { getConfig("testconfig.yaml") } returns testConfig
mockkStatic(Prov::provisionWorkplace)
every { any<Prov>().provisionWorkplace(any(), any(), any(), any(), any(), any()) } returns ProvResult(
every { any<Prov>().provisionWorkplace(any(), any(), any(), any(), any(), ) } returns ProvResult(
true,
cmd = "mocked command"
)
@ -73,18 +74,17 @@ internal class CliWorkplaceKtTest {
fun provision_workplace_remotely() {
// when
main(arrayOf("-i", "-r", "host123.xyz", "-u", "user123", "testconfig.yaml"))
main(arrayOf("basic", "-i", "-r", "host123.xyz", "-u", "user123", "-c", "testconfig.yaml"))
// then
verify { remote("host123.xyz", "user123", Secret("sec"), any()) }
verify {
any<Prov>().provisionWorkplace(
WorkplaceType.MINIMAL,
DesktopType.BASIC,
null,
null,
testConfig.gitUserName,
testConfig.gitEmail,
any() // todo should be: cmd , but needs to be fixed
)
}
}
@ -103,7 +103,7 @@ internal class CliWorkplaceKtTest {
System.setErr(PrintStream(errContent))
// when
main(arrayOf("-l", "idontexist.yaml"))
main(arrayOf("basic", "-c", "idontexist.yaml", "-r", "remotehost", "-u", "someuser", "-k"))
// then
System.setOut(originalOut)
@ -113,7 +113,7 @@ internal class CliWorkplaceKtTest {
"Error: File\u001B[31m ConfigFileName(fileName=idontexist.yaml) \u001B[0m was not found.Pls copy file \u001B[31m WorkplaceConfigExample.yaml \u001B[0m to file \u001B[31m ConfigFileName(fileName=idontexist.yaml) \u001B[0m and change the content according to your needs."
assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", ""))
verify(exactly = 0) { any<Prov>().provisionWorkplace(any(), cmd = cmd) }
verify(exactly = 0) { any<Prov>().provisionWorkplace(any(), any(), any(), any(), any(), ) }
}
@Test
@ -130,7 +130,7 @@ internal class CliWorkplaceKtTest {
System.setErr(PrintStream(errContent))
// when
main(arrayOf("-l", "src/test/resources/InvalidWorkplaceConfig.yaml"))
main(arrayOf("basic", "-c", "src/test/resources/InvalidWorkplaceConfig.yaml", "-r", "remotehost", "-u", "someuser", "-k"))
// then
System.setOut(originalOut)
@ -140,6 +140,6 @@ internal class CliWorkplaceKtTest {
"Error: File \"ConfigFileName(fileName=src/test/resources/InvalidWorkplaceConfig.yaml)\" has an invalid format and or invalid data."
assertEquals(expectedOutput, outContent.toString().replace("\r", "").replace("\n", ""))
verify(exactly = 0) { any<Prov>().provisionWorkplace(any(), cmd = cmd) }
verify(exactly = 0) { any<Prov>().provisionWorkplace(any(), any(), any(), any(), any(), ) }
}
}

@ -3,7 +3,6 @@ package org.domaindrivenarchitecture.provs.desktop.infrastructure
import com.charleskorn.kaml.InvalidPropertyValueException
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType
import org.domaindrivenarchitecture.provs.desktop.domain.WorkplaceType
import org.domaindrivenarchitecture.provs.server.infrastructure.k3s.getK3sConfig
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
@ -18,7 +17,6 @@ internal class ConfigRepositoryKtTest {
val config = getConfig("src/test/resources/TestWorkplaceConfig.yaml")
// then
assertEquals(WorkplaceType.OFFICE, config.type)
assertEquals("username", config.gitUserName)
assertEquals("for@git.email", config.gitEmail)
@ -36,7 +34,7 @@ internal class ConfigRepositoryKtTest {
val exception = assertThrows<InvalidPropertyValueException> {
getConfig("src/test/resources/InvalidWorkplaceConfig.yaml")
}
assertEquals("Value for 'type' is invalid: Value 'WRONGTYPE' is not a valid option, permitted choices are: IDE, MINIMAL, OFFICE", exception.message)
assertEquals("Value for 'sourceType' is invalid: Value 'xxx' is not a valid option, permitted choices are: FILE, GOPASS, PASS, PLAIN, PROMPT", exception.message)
}
@Test

@ -2,10 +2,10 @@ 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.application.DesktopCliCommand
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.WorkplaceType
import org.domaindrivenarchitecture.provs.desktop.domain.DesktopType
import org.domaindrivenarchitecture.provs.desktop.domain.provisionWorkplace
import org.domaindrivenarchitecture.provs.desktop.infrastructure.getConfig
import org.junit.jupiter.api.Assertions.assertTrue
@ -14,9 +14,9 @@ import org.junit.jupiter.api.Test
internal class ProvisionWorkplaceKtTest {
val cmd = DesktopCliCommand(
ConfigFileName("bla"),
listOf(),
TargetCliCommand(null, null, null, false, null, false)
DesktopType.BASIC,
TargetCliCommand(null, null, null, false, null, false),
ConfigFileName("bla")
)
@Test
@ -28,10 +28,9 @@ internal class ProvisionWorkplaceKtTest {
// when
// in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
val res = a.provisionWorkplace(
WorkplaceType.MINIMAL,
DesktopType.BASIC,
gitUserName = "testuser",
gitEmail = "testuser@test.org",
cmd = cmd
)
// then
@ -49,12 +48,11 @@ internal class ProvisionWorkplaceKtTest {
// in order to test WorkplaceType.OFFICE: fix installing libreoffice for a fresh container as it hangs the first time but succeeds 2nd time
val config = getConfig("src/test/resources/WorkplaceConfigExample.json")
val res = a.provisionWorkplace(
config.type,
DesktopType.BASIC,
config.ssh?.keyPair(),
config.gpg?.keyPair(),
config.gitUserName,
config.gitEmail,
cmd,
)
// then

@ -1 +1,3 @@
type: WRONGTYPE # IDE, OFFICE or MINIMAL
ssh:
sourceType: xxx

Loading…
Cancel
Save