diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/Utils.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/Utils.kt index 6a58c4b..ff06374 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/core/Utils.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/core/Utils.kt @@ -40,22 +40,25 @@ fun getCallingMethodName(): String? { } -fun String.escapeNewline(): String = this.replace("\r", "\\r").replace("\n", "\\n") -fun String.escapeControlChars(): String = this.replace("\r", "\\r").replace("\n", "\\n").replace("\t", "\\t").replace("[\\p{Cntrl}]".toRegex(), "\\?") -fun String.escapeBackslash(): String = this.replace("\\", "\\\\") -fun String.escapeDoubleQuote(): String = this.replace("\"", "\\\"") -fun String.escapeSingleQuote(): String = this.replace("'", "\'") -fun String.escapeSingleQuoteForShell(): String = this.replace("'", "'\"'\"'") -fun String.escapeProcentForPrintf(): String = this.replace("%", "%%") +fun String.escapeNewline(): String = replace("\r", "\\r").replace("\n", "\\n") +fun String.escapeControlChars(): String = replace("\r", "\\r").replace("\n", "\\n").replace("\t", "\\t").replace("[\\p{Cntrl}]".toRegex(), "\\?") +fun String.escapeBackslash(): String = replace("\\", "\\\\") +fun String.escapeDoubleQuote(): String = replace("\"", "\\\"") +fun String.escapeSingleQuote(): String = replace("'", "\'") +fun String.escapeSingleQuoteForShell(): String = replace("'", "'\"'\"'") +fun String.escapeProcentForPrintf(): String = replace("%", "%%") +fun String.endingWithFileSeparator(): String = if (length > 0 && (last() != fileSeparatorChar())) this + fileSeparator() else this + // see https://www.shellscript.sh/escape.html fun String.escapeAndEncloseByDoubleQuoteForShell(): String { return "\"" + this.escapeBackslash().replace("`", "\\`").escapeDoubleQuote().replace("$", "\\$") + "\"" } - -fun hostUserHome(): String = System.getProperty("user.home") + fileSeparator() -fun newline(): String = System.getProperty("line.separator") fun fileSeparator(): String = File.separator +fun fileSeparatorChar(): Char = File.separatorChar +fun newline(): String = System.getProperty("line.separator") +fun hostUserHome(): String = System.getProperty("user.home") + fileSeparator() + fun getResourceAsText(path: String): String { val resource = Thread.currentThread().contextClassLoader.getResource(path) diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt index 92fb275..096558a 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/ubuntu/filesystem/base/Filesystem.kt @@ -10,6 +10,22 @@ fun Prov.fileExists(file: String, sudo: Boolean = false): Boolean { } +fun Prov.createFileFromResource( + fullyQualifiedFilename: String, + resourceFilename: String, + resourcePath: String = "", + posixFilePermission: String? = null, + sudo: Boolean = false +): ProvResult { + return createFile( + fullyQualifiedFilename, + getResourceAsText(resourcePath.endingWithFileSeparator() + resourceFilename), + posixFilePermission, + sudo + ) +} + + fun Prov.createFile( fullyQualifiedFilename: String, text: String?, @@ -69,6 +85,14 @@ fun Prov.fileContent(file: String, sudo: Boolean = false): String? { } +fun Prov.addTextToFile( + text: String, + file: String, + doNotAddIfExisting: Boolean = true, + sudo: Boolean = false +): ProvResult = addTextToFile(text, File(file), doNotAddIfExisting, sudo) + + fun Prov.addTextToFile( text: String, file: File, diff --git a/src/main/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOps.kt b/src/main/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOps.kt index fdf2b7f..eeb3773 100644 --- a/src/main/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOps.kt +++ b/src/main/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOps.kt @@ -2,19 +2,16 @@ package org.domaindrivenarchitecture.provs.workplace.infrastructure import org.domaindrivenarchitecture.provs.core.Prov import org.domaindrivenarchitecture.provs.core.ProvResult -import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs -import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createFile -import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.dirExists -import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileExists +import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.* import org.domaindrivenarchitecture.provs.ubuntu.install.base.aptInstall import org.domaindrivenarchitecture.provs.ubuntu.web.base.downloadFromURL fun Prov.installDevOps() = def { installTerraform() - //installAwsCredentials("", "") // TODO: get credentials from gopass - installKubectl() + installKubectlAndTools() installYq() + installAwsCredentials() } @@ -22,10 +19,10 @@ fun Prov.installYq( version: String = "4.13.2", sha256sum: String = "d7c89543d1437bf80fee6237eadc608d1b121c21a7cbbe79057d5086d74f8d79" ): ProvResult = def { - var path = "/usr/bin/" - var filename = "yq" - if(!fileExists(path + filename)) { - val result = downloadFromURL( + val path = "/usr/bin/" + val filename = "yq" + if (!fileExists(path + filename)) { + downloadFromURL( "https://github.com/mikefarah/yq/releases/download/v$version/yq_linux_amd64", filename, path, @@ -38,97 +35,82 @@ fun Prov.installYq( } } -fun Prov.installKubectl(): ProvResult = def { - var kubeConfigFile = "~/.bashrc.d/kubectl.sh" - if(!fileExists(kubeConfigFile)) { - aptInstall("kubectl") - cmd("kubectl completion bash >> /etc/bash_completion.d/kubernetes", sudo = true) - // TODO: externalize to file - trippeld escaping is realy ugly & does not work - /*var kubeConfig = """ - # Set the default kube context if present - DEFAULT_KUBE_CONTEXTS="$HOME/.kube/config" - if test -f "${DEFAULT_KUBE_CONTEXTS}" - then - export KUBECONFIG="$DEFAULT_KUBE_CONTEXTS" - fi - - # Additional contexts should be in ~/.kube/custom-contexts/ - CUSTOM_KUBE_CONTEXTS="$HOME/.kube/custom-contexts" - mkdir -p "${CUSTOM_KUBE_CONTEXTS}" - - OIFS="$IFS" - IFS=$'\n' - for contextFile in `find "${CUSTOM_KUBE_CONTEXTS}" -type f -name "*.yml"` - do - export KUBECONFIG="$contextFile:$KUBECONFIG" - done - IFS="$OIFS" - - """.trimIndent() - */ +fun Prov.installKubectlAndTools(): ProvResult = def { + val resourcePath = "workplace/infrastructure/" + + task("installKubectl") { + val kubeConfigFile = "~/.bashrc.d/kubectl.sh" + if (!fileExists(kubeConfigFile)) { + // prerequisites -- see https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/ + cmd("sudo apt-get update") + aptInstall("apt-transport-https ca-certificates curl") + cmd("sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg") + cmd("echo \"deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main\" | sudo tee /etc/apt/sources.list.d/kubernetes.list") + + // kubectl and bash completion + cmd("sudo apt-get update") + aptInstall("kubectl") + addTextToFile("\nkubectl completion bash\n", "/etc/bash_completion.d/kubernetes", sudo = true) + createFileFromResource(kubeConfigFile, "kubectl.sh", resourcePath) + } else { + ProvResult(true, out = "Kubectl already installed") + } } - val tunnelAliasFile = "~/.bashrc.d/ssh_alias.sh" - if(!fileExists(tunnelAliasFile)) { - var tunnelAlias = """ - alias sshu='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' - alias ssht='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -L 8002:localhost:8002 -L 6443:192.168.5.1:6443' - """.trimIndent() - createFile(tunnelAliasFile, tunnelAlias, "640") + task("install tunnel alias") { + val tunnelAliasFile = "~/.bashrc.d/ssh_alias.sh" + if (!fileExists(tunnelAliasFile)) { + val tunnelAlias = """ + alias sshu='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' + alias ssht='ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -L 8002:localhost:8002 -L 6443:192.168.5.1:6443' + """.trimIndent() + createFile(tunnelAliasFile, tunnelAlias, "640") + } else { + ProvResult(true, out = "tunnel alias already installed") + } } - val k8sContextFile = "/usr/local/bin/k8s-create-context.sh" - if(!fileExists(k8sContextFile)) { - // TODO: externalize to file - trippeld escaping is realy ugly & does not work - var k8sContext = """ - function main() { - local cluster_name="${1}"; shift - - ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@\$\{cluster_name}.meissa-gmbh.de \ - "cat /etc/kubernetes/admin.conf" | \ - yq e ".clusters[0].name=\"\$\{cluster_name}\" \ - | .clusters[0].cluster.server=\"https://kubernetes:6443\" \ - | .contexts[0].context.cluster=\"\$\{cluster_name}\" \ - | .contexts[0].context.user=\"\$\{cluster_name}\" \ - | .contexts[0].name=\"\$\{cluster_name}\" \ - | del(.current-context) \ - | del(.preferences) \ - | .users[0].name=\"\$\{cluster_name}\"" - \ - > ~/.kube/custom-contexts/\$\{cluster_name}.yml + task("install k8sCreateContext") { + val k8sContextFile = "/usr/local/bin/k8s-create-context.sh" + if (!fileExists(k8sContextFile)) { + createFileFromResource( + k8sContextFile, + "k8s-create-context.sh", + resourcePath, + "555", + sudo = true + ) + } else { + ProvResult(true) } - - main $1 - - """.trimIndent() - createFile(k8sContextFile, k8sContext, "555", sudo = true) - } else { - ProvResult(true) } } fun Prov.installTerraform(): ProvResult = def { val dir = "/usr/lib/tfenv/" - if(!dirExists(dir)) { + if (!dirExists(dir)) { createDirs(dir, sudo = true) cmd("git clone https://github.com/tfutils/tfenv.git " + dir, sudo = true) cmd("rm " + dir + ".git/ -rf", sudo = true) cmd("ln -s " + dir + "bin/* /usr/local/bin", sudo = true) } - cmd ("tfenv install", sudo = true) - cmd ("tfenv install latest:^0.13", sudo = true) - cmd ("tfenv use latest:^0.13", sudo = true) + cmd("tfenv install", sudo = true) + cmd("tfenv install latest:^1.0.8", sudo = true) + cmd("tfenv use latest:^1.0.8", sudo = true) } -fun Prov.installAwsCredentials(id:String, key:String): ProvResult = def { + +// -------------------------------------------- AWS credentials file ----------------------------------------------- +fun Prov.installAwsCredentials(id: String = "REPLACE_WITH_YOUR_ID", key: String = "REPLACE_WITH_YOUR_KEY"): ProvResult = def { val dir = "~/.aws" - if(!dirExists(dir)) { + if (!dirExists(dir)) { createDirs(dir) createFile("~/.aws/config", awsConfig()) createFile("~/.aws/credentials", awsCredentials(id, key)) } else { - ProvResult(true, "aws credential file already installed") + ProvResult(true, "aws credential folder already installed") } } @@ -140,7 +122,7 @@ fun awsConfig(): String { """.trimIndent() } -fun awsCredentials(id:String, key:String): String { +fun awsCredentials(id: String, key: String): String { return """ [default] aws_access_key_id = $id diff --git a/src/main/resources/workplace/infrastructure/k8s-create-context.sh b/src/main/resources/workplace/infrastructure/k8s-create-context.sh new file mode 100644 index 0000000..bd5a751 --- /dev/null +++ b/src/main/resources/workplace/infrastructure/k8s-create-context.sh @@ -0,0 +1,17 @@ +function main() { + local cluster_name="${1}"; shift + + ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no root@${cluster_name}.meissa-gmbh.de \ + "cat /etc/kubernetes/admin.conf" | \ + yq e ".clusters[0].name=\"${cluster_name}\" \ + | .clusters[0].cluster.server=\"https://kubernetes:6443\" \ + | .contexts[0].context.cluster=\"${cluster_name}\" \ + | .contexts[0].context.user=\"${cluster_name}\" \ + | .contexts[0].name=\"${cluster_name}\" \ + | del(.current-context) \ + | del(.preferences) \ + | .users[0].name=\"${cluster_name}\"" - \ + > ~/.kube/custom-contexts/${cluster_name}.yml +} + +main $1 diff --git a/src/main/resources/workplace/infrastructure/kubectl.sh b/src/main/resources/workplace/infrastructure/kubectl.sh new file mode 100644 index 0000000..4d69a08 --- /dev/null +++ b/src/main/resources/workplace/infrastructure/kubectl.sh @@ -0,0 +1,18 @@ +# Set the default kube context if present +DEFAULT_KUBE_CONTEXTS="$HOME/.kube/config" +if test -f "${DEFAULT_KUBE_CONTEXTS}" +then + export KUBECONFIG="$DEFAULT_KUBE_CONTEXTS" +fi + +# Additional contexts should be in ~/.kube/custom-contexts/ +CUSTOM_KUBE_CONTEXTS="$HOME/.kube/custom-contexts" +mkdir -p "${CUSTOM_KUBE_CONTEXTS}" + +OIFS="$IFS" +IFS=$'\n' +for contextFile in $(find "${CUSTOM_KUBE_CONTEXTS}" -type f -name "*.yml") +do + export KUBECONFIG="$contextFile:$KUBECONFIG" +done +IFS="$OIFS" diff --git a/src/test/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOpsKtTest.kt b/src/test/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOpsKtTest.kt new file mode 100644 index 0000000..a9d6d65 --- /dev/null +++ b/src/test/kotlin/org/domaindrivenarchitecture/provs/workplace/infrastructure/DevOpsKtTest.kt @@ -0,0 +1,39 @@ +package org.domaindrivenarchitecture.provs.workplace.infrastructure + +import org.domaindrivenarchitecture.provs.core.getResourceAsText +import org.domaindrivenarchitecture.provs.test.defaultTestContainer +import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDir +import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.createDirs +import org.domaindrivenarchitecture.provs.ubuntu.filesystem.base.fileContainsText +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +internal class DevOpsKtTest { + + @Test + fun installKubectlAndTools() { + // given + defaultTestContainer().def { + createDirs("/etc/bash_completion.d", sudo = true) + createDir(".bashrc.d") + } + + //when + val res = defaultTestContainer().installKubectlAndTools() + + // then + assertTrue(res.success) + assertTrue( + defaultTestContainer().fileContainsText( + "~/.bashrc.d/kubectl.sh", + getResourceAsText("workplace/infrastructure/kubectl.sh") + ) + ) + assertTrue( + defaultTestContainer().fileContainsText( + "/etc/bash_completion.d/kubernetes", + "\nkubectl completion bash\n" + ) + ) + } +}