add syspec
This commit is contained in:
parent
c364d25113
commit
394dc9edf2
13 changed files with 361 additions and 5 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,3 +7,5 @@
|
||||||
/My*
|
/My*
|
||||||
/my*
|
/my*
|
||||||
/server-config.yaml
|
/server-config.yaml
|
||||||
|
/desktop-config.yaml
|
||||||
|
/syspec-config.yaml
|
||||||
|
|
|
@ -17,7 +17,7 @@ fun main(args: Array<String>) {
|
||||||
|
|
||||||
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
|
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
|
||||||
|
|
||||||
val cmd = CliArgumentsParser("java -jar provs-server.jar").parseCommand(checkedArgs)
|
val cmd = CliArgumentsParser("provs-server.jar").parseCommand(checkedArgs)
|
||||||
if (!cmd.isValid()) {
|
if (!cmd.isValid()) {
|
||||||
println("Arguments are not valid, pls try -h for help.")
|
println("Arguments are not valid, pls try -h for help.")
|
||||||
exitProcess(1)
|
exitProcess(1)
|
||||||
|
@ -25,5 +25,6 @@ fun main(args: Array<String>) {
|
||||||
val prov = createProvInstance(cmd.target)
|
val prov = createProvInstance(cmd.target)
|
||||||
when(cmd.serverType) {
|
when(cmd.serverType) {
|
||||||
ServerType.K3S -> prov.provisionK3s(cmd as K3sCliCommand)
|
ServerType.K3S -> prov.provisionK3s(cmd as K3sCliCommand)
|
||||||
|
else -> { throw RuntimeException("Unknown serverType") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package org.domaindrivenarchitecture.provs.server.domain.k3s
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.*
|
import org.domaindrivenarchitecture.provs.server.infrastructure.*
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.k3s.getK3sConfig
|
import org.domaindrivenarchitecture.provs.server.infrastructure.getK3sConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs a k3s server.
|
* Installs a k3s server.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.domaindrivenarchitecture.provs.server.infrastructure.k3s
|
package org.domaindrivenarchitecture.provs.server.infrastructure
|
||||||
|
|
||||||
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
||||||
import org.domaindrivenarchitecture.provs.framework.core.readFromFile
|
import org.domaindrivenarchitecture.provs.framework.core.readFromFile
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.application
|
||||||
|
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.cli.createProvInstance
|
||||||
|
import org.domaindrivenarchitecture.provs.syspec.domain.verifySpec
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a system check, either locally or on a remote machine depending on the given arguments.
|
||||||
|
*
|
||||||
|
* Get help with option -h
|
||||||
|
*/
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
|
||||||
|
val checkedArgs = if (args.isEmpty()) arrayOf("-h") else args
|
||||||
|
|
||||||
|
val cmd = CliArgumentsParser("provs-syspec.jar").parseCommand(checkedArgs)
|
||||||
|
|
||||||
|
createProvInstance(cmd.target).verifySpec(cmd.configFileName)
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.application
|
||||||
|
|
||||||
|
import kotlinx.cli.ArgType
|
||||||
|
import kotlinx.cli.default
|
||||||
|
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.syspec.domain.SyspecCliCommand
|
||||||
|
|
||||||
|
class CliArgumentsParser(name: String) : CliTargetParser(name) {
|
||||||
|
|
||||||
|
val cliConfigFileName by option(
|
||||||
|
ArgType.String,
|
||||||
|
"config-file",
|
||||||
|
"c",
|
||||||
|
"the filename containing the yaml config"
|
||||||
|
).default("syspec-config.yaml")
|
||||||
|
|
||||||
|
fun parseCommand(args: Array<String>): SyspecCliCommand {
|
||||||
|
super.parse(args)
|
||||||
|
|
||||||
|
return SyspecCliCommand(
|
||||||
|
TargetCliCommand(
|
||||||
|
target,
|
||||||
|
passwordInteractive
|
||||||
|
),
|
||||||
|
ConfigFileName(cliConfigFileName)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.domain
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SpecConfig(
|
||||||
|
val command: List<CommandSpec>? = null,
|
||||||
|
val file: List<FileSpec>? = null,
|
||||||
|
val host: List<HostSpec>? = null,
|
||||||
|
val `package`: List<PackageSpec>? = null,
|
||||||
|
val netcat: List<NetcatSpec>? = null,
|
||||||
|
val socket: List<SocketSpec>? = null,
|
||||||
|
val certificate: List<CertificateFileSpec>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that a command executes successfully and
|
||||||
|
* (if provided) the specified output is contained in the actual output
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class CommandSpec(val command: String, val out: String? = null)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FileSpec(val name: String, val exists: Boolean = true)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class HostSpec(val url: String, val expirationDays: Long? = null)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PackageSpec(val name: String, val installed: Boolean = true)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NetcatSpec(val host: String, val port: Int = 80, val reachable: Boolean = true)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SocketSpec(
|
||||||
|
val processName: String,
|
||||||
|
val port: Int,
|
||||||
|
val running: Boolean = true,
|
||||||
|
val ip: String? = null,
|
||||||
|
val protocol: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CertificateFileSpec(val name: String, val expirationDays: Long)
|
|
@ -0,0 +1,9 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.domain
|
||||||
|
|
||||||
|
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
||||||
|
import org.domaindrivenarchitecture.provs.configuration.domain.TargetCliCommand
|
||||||
|
|
||||||
|
class SyspecCliCommand (
|
||||||
|
val target: TargetCliCommand,
|
||||||
|
val configFileName: ConfigFileName?,
|
||||||
|
)
|
|
@ -0,0 +1,19 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.domain
|
||||||
|
|
||||||
|
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
||||||
|
import org.domaindrivenarchitecture.provs.syspec.infrastructure.findSpecConfigFromFile
|
||||||
|
import org.domaindrivenarchitecture.provs.syspec.infrastructure.verifySpecConfig
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.verifySpec(config: ConfigFileName?) = task {
|
||||||
|
val spec = findSpecConfigFromFile(config)
|
||||||
|
|
||||||
|
if (spec == null) {
|
||||||
|
ProvResult(false, "Could not read file: ${config?.fileName}")
|
||||||
|
} else {
|
||||||
|
verifySpecConfig(spec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.infrastructure
|
||||||
|
|
||||||
|
import com.charleskorn.kaml.Yaml
|
||||||
|
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.readFromFile
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.yamlToType
|
||||||
|
import org.domaindrivenarchitecture.provs.syspec.domain.CommandSpec
|
||||||
|
import org.domaindrivenarchitecture.provs.syspec.domain.SpecConfig
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileWriter
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
private const val DEFAULT_CONFIG_FILE = "syspec-config.yaml"
|
||||||
|
|
||||||
|
internal fun writeSpecConfigToFile(
|
||||||
|
fileName: String = DEFAULT_CONFIG_FILE,
|
||||||
|
config: SpecConfig
|
||||||
|
) {
|
||||||
|
FileWriter(fileName).use {
|
||||||
|
it.write(
|
||||||
|
Yaml.default.encodeToString(
|
||||||
|
SpecConfig.serializer(),
|
||||||
|
config
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun getSpecConfigFromFile(file: ConfigFileName? = null): SpecConfig {
|
||||||
|
val filename = file?.fileName ?: DEFAULT_CONFIG_FILE
|
||||||
|
|
||||||
|
if ((filename.substringAfterLast("/") == DEFAULT_CONFIG_FILE) && !File(filename).exists()) {
|
||||||
|
// provide default config
|
||||||
|
writeSpecConfigToFile(filename, SpecConfig(listOf(CommandSpec("echo just_for_demo", "just_for_demo"))))
|
||||||
|
}
|
||||||
|
return readFromFile(filename).yamlToType()
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun findSpecConfigFromFile(file: ConfigFileName? = null): SpecConfig? {
|
||||||
|
return try {
|
||||||
|
val config = getSpecConfigFromFile(file)
|
||||||
|
config
|
||||||
|
} catch (e: IOException) {
|
||||||
|
println("Error: " + e.message)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
package org.domaindrivenarchitecture.provs.syspec.infrastructure
|
||||||
|
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.Prov
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.core.ProvResult
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.filesystem.base.checkFile
|
||||||
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.install.base.isPackageInstalled
|
||||||
|
import org.domaindrivenarchitecture.provs.syspec.domain.*
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
fun Prov.verifySpecConfig(conf: SpecConfig) = task {
|
||||||
|
// required to guarantee that at least one result can be provided
|
||||||
|
val dummySuccess = ProvResult(true)
|
||||||
|
|
||||||
|
conf.command?.let { task("CommandSpecs") { for (spec in conf.command) verify(spec); dummySuccess } }
|
||||||
|
conf.file?.let { task("FileSpecs") { for (spec in conf.file) verify(spec); dummySuccess } }
|
||||||
|
conf.host?.let { task("HostSpecs") { for (spec in conf.host) verify(spec); dummySuccess } }
|
||||||
|
conf.`package`?.let { task("PackageSpecs") { for (spec in conf.`package`) verify(spec); dummySuccess } }
|
||||||
|
conf.netcat?.let { task("NetcatSpecs") { for (spec in conf.netcat) verify(spec); dummySuccess } }
|
||||||
|
conf.socket?.let { task("SocketSpecs") { for (spec in conf.socket) verify(spec); dummySuccess } }
|
||||||
|
conf.certificate?.let { task("CertificateFileSpecs") { for (spec in conf.certificate) verify(spec); dummySuccess } }
|
||||||
|
|
||||||
|
dummySuccess // to be sure that at least one result is provided
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(cmd: CommandSpec) {
|
||||||
|
val res = cmdNoEval(cmd.command)
|
||||||
|
if (!res.success) {
|
||||||
|
verify(false, "Command [${cmd.command}] is not executable due to error: ${res.err}")
|
||||||
|
} else {
|
||||||
|
if (cmd.out == null) {
|
||||||
|
verify(true, "Command is executable [${cmd.command}]")
|
||||||
|
} else {
|
||||||
|
val expected = cmd.out
|
||||||
|
val actual = res.out?.trimEnd('\n')
|
||||||
|
val contains = (actual?.contains(expected) ?: false)
|
||||||
|
verify(
|
||||||
|
contains,
|
||||||
|
"Output of command '${cmd.command}' does ${contains.falseToNot()}contain $expected ${if (!contains) "(Actual output: $actual)" else ""}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(file: FileSpec) {
|
||||||
|
val actualExists = checkFile(file.name)
|
||||||
|
verify(actualExists == file.exists, "File [${file.name}] does ${actualExists.falseToNot()}exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(hostspec: HostSpec) {
|
||||||
|
// see https://serverfault.com/questions/661978/displaying-a-remote-ssl-certificate-details-using-cli-tools
|
||||||
|
val res =
|
||||||
|
cmdNoEval("echo | openssl s_client -showcerts -servername ${hostspec.url} -connect ${hostspec.url}:443 2>/dev/null | openssl x509 -inform pem -noout -enddate")
|
||||||
|
|
||||||
|
if (!res.success) {
|
||||||
|
verify(false, "Could not retrieve certificate from ${hostspec.url} due to error: ${res.err}")
|
||||||
|
} else {
|
||||||
|
if (hostspec.expirationDays == null) {
|
||||||
|
verify(true, "Found a certificate on ${hostspec.url}")
|
||||||
|
} else {
|
||||||
|
verifyCertExpiration(res.out, hostspec.url, hostspec.expirationDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(pkg: PackageSpec) {
|
||||||
|
val res = isPackageInstalled(pkg.name)
|
||||||
|
verify(res == pkg.installed, "Package ${pkg.name} is ${res.falseToNot()}installed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(ncConf: NetcatSpec) {
|
||||||
|
val timeout = 10 // sec
|
||||||
|
val res = cmdNoEval("nc ${ncConf.host} ${ncConf.port} -z -w $timeout")
|
||||||
|
verify(
|
||||||
|
res.success == ncConf.reachable,
|
||||||
|
"Host ${ncConf.host} is ${res.success.falseToNot()}reachable at port ${ncConf.port}."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Prov.verify(socketConf: SocketSpec): ProvResult {
|
||||||
|
val res = cmdNoEval("ss -tulpen", sudo = true)
|
||||||
|
val lines: List<String> = res.out?.trim()?.split("\n") ?: emptyList()
|
||||||
|
return if (lines.isEmpty()) {
|
||||||
|
verify(false, "Could not get socketStats due to ${res.err}")
|
||||||
|
} else {
|
||||||
|
verifySocketSpec(socketConf, lines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Prov.verify(cert: CertificateFileSpec) {
|
||||||
|
val res = cmdNoEval("openssl x509 -in ${cert.name} -noout -enddate")
|
||||||
|
if (!res.success) {
|
||||||
|
verify(false, "Could not retrieve certificate from ${cert.name} due to error: ${res.err}")
|
||||||
|
} else {
|
||||||
|
verifyCertExpiration(res.out, cert.name, cert.expirationDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------------- helper ---------------------------------
|
||||||
|
|
||||||
|
fun Prov.verifySocketSpec(socketConf: SocketSpec, outputLines: List<String>): ProvResult {
|
||||||
|
val headLine = outputLines[0]
|
||||||
|
val processRange = "Process +".toRegex().find(headLine)?.range
|
||||||
|
val ipRange = " +Local Address".toRegex().find(headLine)?.range
|
||||||
|
val portRange = "Port +".toRegex().find(headLine)?.range
|
||||||
|
val protocolRange = "Netid +".toRegex().find(headLine)?.range
|
||||||
|
|
||||||
|
if (processRange == null || ipRange == null || portRange == null || protocolRange == null) {
|
||||||
|
return verify(false, "Could not parse a headline from: $headLine")
|
||||||
|
} else {
|
||||||
|
val factLines: List<String> = outputLines.drop(1).filter { it.length == headLine.length }
|
||||||
|
|
||||||
|
var matchingLine: String? = null
|
||||||
|
for (line in factLines) {
|
||||||
|
val process = "\"(.+)\"".toRegex().find(line.substring(processRange))?.groups?.get(1)?.value
|
||||||
|
if (socketConf.processName == process &&
|
||||||
|
socketConf.port.toString() == line.substring(portRange).trim() &&
|
||||||
|
(socketConf.ip == null || socketConf.ip == line.substring(ipRange)) &&
|
||||||
|
(socketConf.protocol == null || socketConf.protocol == line.substring(protocolRange))
|
||||||
|
) {
|
||||||
|
matchingLine = line
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val found = matchingLine != null
|
||||||
|
return verify(found == socketConf.running, "Did ${found.falseToNot()}find [$socketConf]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Boolean.falseToNot(suffix: String = " ") = if (this) "" else "not$suffix"
|
||||||
|
|
||||||
|
private fun Prov.verify(success: Boolean, message: String): ProvResult {
|
||||||
|
return verify<Any>(success, message, null, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> Prov.verify(success: Boolean, message: String, expected: T? = null, actual: T? = null): ProvResult {
|
||||||
|
val expectedText = expected?.let { " | Expected value [$expected]" } ?: ""
|
||||||
|
val actualText = expected?.let { " | Actual value [$actual]" } ?: ""
|
||||||
|
val msg = ": $message $expectedText$actualText"
|
||||||
|
|
||||||
|
return task("Verification") {
|
||||||
|
ProvResult(
|
||||||
|
success,
|
||||||
|
cmd = if (success) msg else null,
|
||||||
|
err = if (!success) msg else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class DiffResult(val diff: Long? = null, val err: String? = null) {
|
||||||
|
init {
|
||||||
|
require(diff != null || err != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun diffSslDateToToday(enddate: String?): DiffResult {
|
||||||
|
val format = SimpleDateFormat("MMM d HH:mm:ss yyyy zzz", Locale.ENGLISH)
|
||||||
|
return try {
|
||||||
|
val expirationDate = format.parse(enddate)
|
||||||
|
val diffInMillisec: Long = expirationDate.time - Date().time
|
||||||
|
val diffInDays = TimeUnit.MILLISECONDS.toDays(diffInMillisec)
|
||||||
|
DiffResult(diff = diffInDays)
|
||||||
|
} catch (e: ParseException) {
|
||||||
|
DiffResult(err = "Could not parse date '$enddate' with pattern '$format' - Parse error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Prov.verifyCertExpiration(enddate: String?, certName: String, expirationDays: Long) {
|
||||||
|
val enddateCleaned = enddate?.replace("notAfter=", "")?.trimEnd('\n')
|
||||||
|
val (diffInDays, err) = diffSslDateToToday(enddateCleaned)
|
||||||
|
if (diffInDays == null) {
|
||||||
|
verify(false, err ?: ("Could not parse date: $enddateCleaned"))
|
||||||
|
} else {
|
||||||
|
verify(
|
||||||
|
diffInDays > expirationDays,
|
||||||
|
"Certificate of [$certName] expires on [${enddateCleaned}] in $diffInDays days (expected > $expirationDays days)",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ package org.domaindrivenarchitecture.provs.desktop.infrastructure
|
||||||
import com.charleskorn.kaml.InvalidPropertyValueException
|
import com.charleskorn.kaml.InvalidPropertyValueException
|
||||||
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
import org.domaindrivenarchitecture.provs.configuration.domain.ConfigFileName
|
||||||
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType
|
import org.domaindrivenarchitecture.provs.framework.ubuntu.secret.SecretSourceType
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.k3s.getK3sConfig
|
import org.domaindrivenarchitecture.provs.server.infrastructure.getK3sConfig
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
|
|
@ -7,7 +7,6 @@ import org.domaindrivenarchitecture.provs.server.domain.k3s.Certmanager
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sConfig
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.K3sConfig
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.k3s.Loopback
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.Loopback
|
||||||
import org.domaindrivenarchitecture.provs.server.domain.k3s.Node
|
import org.domaindrivenarchitecture.provs.server.domain.k3s.Node
|
||||||
import org.domaindrivenarchitecture.provs.server.infrastructure.k3s.getK3sConfig
|
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.assertThrows
|
import org.junit.jupiter.api.assertThrows
|
||||||
|
|
Loading…
Reference in a new issue