diff --git a/nomad/.terraform.lock.hcl b/nomad/.terraform.lock.hcl index 9845800..d0bc9c1 100644 --- a/nomad/.terraform.lock.hcl +++ b/nomad/.terraform.lock.hcl @@ -19,6 +19,25 @@ provider "registry.terraform.io/hashicorp/consul" { ] } +provider "registry.terraform.io/hashicorp/external" { + version = "2.2.2" + hashes = [ + "h1:BKQ5f5ijzeyBSnUr+j0wUi+bYv6KBQVQNDXNRVEcfJE=", + "zh:0b84ab0af2e28606e9c0c1289343949339221c3ab126616b831ddb5aaef5f5ca", + "zh:10cf5c9b9524ca2e4302bf02368dc6aac29fb50aeaa6f7758cce9aa36ae87a28", + "zh:56a016ee871c8501acb3f2ee3b51592ad7c3871a1757b098838349b17762ba6b", + "zh:719d6ef39c50e4cffc67aa67d74d195adaf42afcf62beab132dafdb500347d39", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7fbfc4d37435ac2f717b0316f872f558f608596b389b895fcb549f118462d327", + "zh:8ac71408204db606ce63fe8f9aeaf1ddc7751d57d586ec421e62d440c402e955", + "zh:a4cacdb06f114454b6ed0033add28006afa3f65a0ea7a43befe45fc82e6809fb", + "zh:bb5ce3132b52ae32b6cc005bc9f7627b95259b9ffe556de4dad60d47d47f21f0", + "zh:bb60d2976f125ffd232a7ccb4b3f81e7109578b23c9c6179f13a11d125dca82a", + "zh:f9540ecd2e056d6e71b9ea5f5a5cf8f63dd5c25394b9db831083a9d4ea99b372", + "zh:ffd998b55b8a64d4335a090b6956b4bf8855b290f7554dd38db3302de9c41809", + ] +} + provider "registry.terraform.io/hashicorp/nomad" { version = "1.4.16" hashes = [ diff --git a/nomad/levant/.terraform.lock.hcl b/nomad/levant/.terraform.lock.hcl new file mode 100644 index 0000000..8bbed29 --- /dev/null +++ b/nomad/levant/.terraform.lock.hcl @@ -0,0 +1,40 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/external" { + version = "2.2.2" + hashes = [ + "h1:BKQ5f5ijzeyBSnUr+j0wUi+bYv6KBQVQNDXNRVEcfJE=", + "zh:0b84ab0af2e28606e9c0c1289343949339221c3ab126616b831ddb5aaef5f5ca", + "zh:10cf5c9b9524ca2e4302bf02368dc6aac29fb50aeaa6f7758cce9aa36ae87a28", + "zh:56a016ee871c8501acb3f2ee3b51592ad7c3871a1757b098838349b17762ba6b", + "zh:719d6ef39c50e4cffc67aa67d74d195adaf42afcf62beab132dafdb500347d39", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7fbfc4d37435ac2f717b0316f872f558f608596b389b895fcb549f118462d327", + "zh:8ac71408204db606ce63fe8f9aeaf1ddc7751d57d586ec421e62d440c402e955", + "zh:a4cacdb06f114454b6ed0033add28006afa3f65a0ea7a43befe45fc82e6809fb", + "zh:bb5ce3132b52ae32b6cc005bc9f7627b95259b9ffe556de4dad60d47d47f21f0", + "zh:bb60d2976f125ffd232a7ccb4b3f81e7109578b23c9c6179f13a11d125dca82a", + "zh:f9540ecd2e056d6e71b9ea5f5a5cf8f63dd5c25394b9db831083a9d4ea99b372", + "zh:ffd998b55b8a64d4335a090b6956b4bf8855b290f7554dd38db3302de9c41809", + ] +} + +provider "registry.terraform.io/hashicorp/nomad" { + version = "1.4.17" + hashes = [ + "h1:oWV3VXZhqPZ8Ia07nlIZLeXDBqVULMg9lP3dVMczDCo=", + "zh:146f97eacd9a0c78b357a6cfd2cb12765d4b18e9660a75500ee3e748c6eba41a", + "zh:2eb89a6e5cee9aea03a96ea9f141096fe3baf219b2700ce30229d2d882f5015f", + "zh:3d0f971f79b615c1014c75e2f99f34bd4b4da542ca9f31d5ea7fadc4e9de39c1", + "zh:46099a750c752ce05aa14d663a86478a5ad66d95aff3d69367f1d3628aac7792", + "zh:71e56006b013dcfe1e4e059b2b07148b44fcd79351ae2c357e0d97e27ae0d916", + "zh:74febd25d776688f0558178c2f5a0e6818bbf4cdaa2e160d7049da04103940f0", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:af18c064a5f0dd5422d6771939274841f635b619ab392c73d5bf9720945fdb85", + "zh:c133d7a862079da9f06e301c530eacbd70e9288fa2276ec0704df907270ee328", + "zh:c894cf98d239b9f5a4b7cde9f5c836face0b5b93099048ee817b0380ea439c65", + "zh:c918642870f0cafdbe4d7dd07c909701fc3ddb47cac8357bdcde1327bf78c11d", + "zh:f8f5655099a57b4b9c0018a2d49133771e24c7ff8262efb1ceb140fd224aa9b6", + ] +} diff --git a/nomad/levant/README.md b/nomad/levant/README.md new file mode 100644 index 0000000..e5c863e --- /dev/null +++ b/nomad/levant/README.md @@ -0,0 +1,7 @@ +# Terraform Levant + +This module renders a levant template and then creates a Nomad job based on that template. + +It only covers a subset of levant capabilities because much else can be done with Terraform already. + +required: diff --git a/nomad/levant/levant.py b/nomad/levant/levant.py new file mode 100755 index 0000000..60375c0 --- /dev/null +++ b/nomad/levant/levant.py @@ -0,0 +1,60 @@ +#! /usr/bin/env python3 +import json +import sys +from subprocess import check_output +from typing import Optional +from typing import overload +from typing import TypeVar + + +T = TypeVar("T") + + +@overload +def get_json(d: dict[str, str], key: str, default: None = None) -> None: + ... + + +@overload +def get_json(d: dict[str, str], key: str, default: T = None) -> T: + ... + + +def get_json(d: dict[str, str], key: str, default: Optional[T] = None) -> Optional[T]: + if key not in d: + return default + + return json.loads(d[key]) + + +query = json.load(sys.stdin) + +# Required +template_path = query["template_path"] + +# Optional +consul_address = query.get("consul_address") +# Need to parse JSON back +variables = [ + f'--var={key}={value}' for key, value in get_json(query, "variables", {}).items() +] +variable_files = [ + f'--var-file={value}' for value in get_json(query, "var_files", []) +] + +args: list[str] = list( + filter( + None, + ["levant", "render", consul_address] + + variables + + variable_files + + [template_path], + ) +) + +# print(" ".join(args), file=sys.stderr) +# exit(1) + +template = check_output(args, stderr=sys.stderr) + +print(json.dumps({"template": template.decode()})) diff --git a/nomad/levant/main.tf b/nomad/levant/main.tf new file mode 100644 index 0000000..b7a9a7b --- /dev/null +++ b/nomad/levant/main.tf @@ -0,0 +1,37 @@ +variable "template_path" { + type = string + nullable = false +} +variable "consul_address" { + type = string + default = null + nullable = true + description = "Consul host and port for making KeyValue lookups" +} + +variable "variables" { + type = map(string) + description = "Variables to be passed into nomad-pack with values in JSON form" + default = {} +} + +variable "var_files" { + type = list(string) + description = "HCL files containing variables to be used by nomad-pack" + default = [] +} + +data "external" "levant" { + program = ["${path.module}/levant.py"] + + query = { + template_path = var.template_path + consul_address = var.consul_address + variables = jsonencode(var.variables) + var_files = jsonencode(var.var_files) + } +} + +resource "nomad_job" "levant" { + jobspec = data.external.levant.result.template +} diff --git a/nomad/levant/test.nomad b/nomad/levant/test.nomad new file mode 100644 index 0000000..beffd62 --- /dev/null +++ b/nomad/levant/test.nomad @@ -0,0 +1,2 @@ +job { +} diff --git a/nomad/service.nomad b/nomad/service.nomad new file mode 100644 index 0000000..a28ea11 --- /dev/null +++ b/nomad/service.nomad @@ -0,0 +1,145 @@ +# Vars +# name = string* +# image = string* +# service_port = int +# ingress = bool +# args = json(list[str]) +# resources = dict(cpu = int, mem = int) +# templates = json(list(dict( +# data = str, +# dest = str, +# change_mode = str, +# change_signal = str, +# left_delimiter = str, +# right_delimiter = str, +# ))) +# healthcheck = "/" +# mysql = bool +# redis = bool +job "[[.name]]" { + region = "global" + datacenters = ["dc1"] + + type = "service" + + group "[[.name]]" { + [[ with .count ]]count = [[ . ]][[end]] + network { + mode = "bridge" + [[ if not (empty .service_port) ]] + port "main" { + [[ if default false .ingress ]] + host_network = "loopback" + [[ end ]] + to = [[.service_port]] + } + [[ end ]] + } + + [[ if not (empty .service_port) ]] + service { + name = "[[.name | replace "_" "-"]]" + port = "main" + + [[ if default false .ingress ]] + connect { + sidecar_service { + proxy { + local_service_port = [[.service_port]] + [[ if default false .mysql ]] + upstreams { + destination_name = "mysql-server" + local_bind_port = 4040 + } + [[ end -]] + [[ if default false .redis ]] + upstreams { + destination_name = "redis" + local_bind_port = 6379 + } + [[ end ]] + } + } + + sidecar_task { + resources { + cpu = 50 + memory = 50 + } + } + } + [[ end ]] + + check { + type = "http" + path = "[[ or .healthcheck "/" ]]" + port = "main" + interval = "10s" + timeout = "10s" + } + + tags = [ + [[ if default false .ingress -]] + "traefik.enable=true", + "traefik.http.routers.[[.name]].entryPoints=websecure", + [[ end -]] + ] + } + [[ end ]] + + task "[[.name]]" { + driver = "docker" + + config { + image = "[[.image]]" + [[ if not (empty .service_port) -]] + ports = ["main"] + [[- end ]] + [[ if not (empty .args) -]] + args = ["[[ .args | parseJSON | join `", "` ]]"] + [[- end ]] + + [[ with .templates]] + [[ range $t := . | parseJSON ]] + mount { + type = "bind" + target = "[[ $t.dest ]]" + source = "local/[[ $t.dest ]]" + } + [[ end ]] + [[ end ]] + } + + [[ with .env -]] + env = { + [[- range $k, $v := . ]] + "[[$k]]" = "[[$v]]" + [[- end ]] + } + [[ end ]] + + [[ with .templates ]] + [[ range $t := . | parseJSON ]] + template { + data = <