diff --git a/Makefile b/Makefile index 6a09f4eaf..19d60a433 100644 --- a/Makefile +++ b/Makefile @@ -21,3 +21,8 @@ test:: # be useful to run to review the differences with git diff. test_accept:: PULUMI_ACCEPT=1 go test -v -count=1 -cover -timeout 2h -parallel ${TESTPARALLELISM} ./... + +generate_builtins_test:: + if [ ! -d ./scripts/venv ]; then python -m venv ./scripts/venv; fi + . ./scripts/venv/*/activate && python -m pip install -r ./scripts/requirements.txt + . ./scripts/venv/*/activate && python ./scripts/generate_builtins.py \ No newline at end of file diff --git a/pkg/tf2pulumi/convert/testdata/builtins/main.tf b/pkg/tf2pulumi/convert/testdata/builtins/main.tf index 737fd96d0..f6929ebba 100644 --- a/pkg/tf2pulumi/convert/testdata/builtins/main.tf +++ b/pkg/tf2pulumi/convert/testdata/builtins/main.tf @@ -1,3 +1,98 @@ -output "funcLength" { - value = length([1, 2]) -} \ No newline at end of file +# DO NOT EDIT BY HAND +# This file is auto generated by ./scripts/generate_builtins.py + +locals { + # A load of the examples in the docs use `path.module` which _should_ resolve to the file system path of + # the current module, but tf2pulumi doesn't support that so we replace it with local.path_module. + path_module = "some/path" +} + +# Examples for element +output "funcElement0" { + value = element(["a", "b", "c"], 1) +} + +output "funcElement1" { + value = element(["a", "b", "c"], 3) +} + +output "funcElement2" { + value = element(["a", "b", "c"], length(["a", "b", "c"])-1) +} + + +# Examples for file +output "funcFile" { + value = file("${local.path_module}/hello.txt") +} + + +# Examples for filebase64 +output "funcFilebase64" { + value = filebase64("${local.path_module}/hello.txt") +} + + +# Examples for filebase64sha256 +output "funcFilebase64sha256" { + value = filebase64sha256("hello.txt") +} + + +# Examples for jsonencode +output "funcJsonencode" { + value = jsonencode({"hello"="world"}) +} + + +# Examples for length +output "funcLength0" { + value = length([]) +} + +output "funcLength1" { + value = length(["a", "b"]) +} + +output "funcLength2" { + value = length({"a" = "b"}) +} + +output "funcLength3" { + value = length("hello") +} + +output "funcLength4" { + value = length("👾🕹️") +} + + +# Examples for lookup +output "funcLookup0" { + value = lookup({a="ay", b="bee"}, "a", "what?") +} + +output "funcLookup1" { + value = lookup({a="ay", b="bee"}, "c", "what?") +} + + +# Examples for sha1 +output "funcSha1" { + value = sha1("hello world") +} + + +# Examples for split +output "funcSplit0" { + value = split(",", "foo,bar,baz") +} + +output "funcSplit1" { + value = split(",", "foo") +} + +output "funcSplit2" { + value = split(",", "") +} + diff --git a/pkg/tf2pulumi/convert/testdata/builtins/pcl/main.pp b/pkg/tf2pulumi/convert/testdata/builtins/pcl/main.pp index b65c52b3a..df16905f2 100644 --- a/pkg/tf2pulumi/convert/testdata/builtins/pcl/main.pp +++ b/pkg/tf2pulumi/convert/testdata/builtins/pcl/main.pp @@ -1,3 +1,66 @@ -output funcLength { - value = length([1, 2]) -} \ No newline at end of file + # A load of the examples in the docs use `path.module` which _should_ resolve to the file system path of + # the current module, but tf2pulumi doesn't support that so we replace it with local.path_module. + pathModule = "some/path" +# Examples for element +output funcElement0 { + value = element(["a", "b", "c"], 1) +} +output funcElement1 { + value = element(["a", "b", "c"], 3) +} +output funcElement2 { + value = element(["a", "b", "c"], length(["a", "b", "c"])-1) +} +# Examples for file +output funcFile { + value = readFile("${pathModule}/hello.txt") +} +# Examples for filebase64 +output funcFilebase64 { + value = filebase64("${pathModule}/hello.txt") +} +# Examples for filebase64sha256 +output funcFilebase64sha256 { + value = filebase64sha256("hello.txt") +} +# Examples for jsonencode +output funcJsonencode { + value = toJSON({"hello"="world"}) +} +# Examples for length +output funcLength0 { + value = length([]) +} +output funcLength1 { + value = length(["a", "b"]) +} +output funcLength2 { + value = length({"a" = "b"}) +} +output funcLength3 { + value = length("hello") +} +output funcLength4 { + value = length("👾🕹️") +} +# Examples for lookup +output funcLookup0 { + value = lookup({a="ay", b="bee"}, "a", "what?") +} +output funcLookup1 { + value = lookup({a="ay", b="bee"}, "c", "what?") +} +# Examples for sha1 +output funcSha1 { + value = sha1("hello world") +} +# Examples for split +output funcSplit0 { + value = split(",", "foo,bar,baz") +} +output funcSplit1 { + value = split(",", "foo") +} +output funcSplit2 { + value = split(",", "") +} diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 000000000..f5e96dbfa --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +venv \ No newline at end of file diff --git a/scripts/generate_builtins.py b/scripts/generate_builtins.py new file mode 100644 index 000000000..3e1fbed9f --- /dev/null +++ b/scripts/generate_builtins.py @@ -0,0 +1,227 @@ +#!python3 + +# Generating a file with _all_ the terraform builtins is a lot of copy and paste! +# But we can just grab code for all of these from their website examples! + +import re +import os +from git import Repo +import tempfile + +def trimext(file: str) -> str: + i = file.rindex(".") + if i == -1: + return file + return file[0:i] + +# Some functions don't have any examples in the Terraform docs. They _must_ have overrides here else we error on generation. +overrides = { + "base64gzip": "base64gzip(\"test\")", + "filebase64sha256": "filebase64sha256(\"hello.txt\")", + "filebase64sha512": "filebase64sha512(\"hello.txt\")", + "filemd5": "filemd5(\"hello.txt\")", + "filesha1": "filesha1(\"hello.txt\")", + "filesha256": "filesha256(\"hello.txt\")", + "filesha512": "filesha512(\"hello.txt\")", + "list": "list(1, 2, 3)", + "map": "map(\"a\", \"b\", \"c\", \"d\")", + "templatefile": [ # These are taken by hand from the docs because our parser can't handle the multiline example + """templatefile("${path.module}/backends.tftpl", { port = 8080, ip_addrs = ["10.0.0.1", "10.0.0.2"] })""", + """templatefile( + "${path.module}/config.tftpl", + { + config = { + "x" = "y" + "foo" = "bar" + "key" = "value" + } + } + )""" + ], +} + +# There's a number of functions we _don't_ support yet, so we exclude emitting these to the file +unsupported = set([ + "abs", + "abspath", + "alltrue", + "anytrue", + "base64decode", + "base64encode", + "base64gzip", + "base64sha256", + "base64sha512", + "basename", + "bcrypt", + "can", + "ceil", + "chomp", + "chunklist", + "cidrnetmask", + "cidrsubnet", + "cidrhost", + "cidrsubnets", + "coalesce", + "coalescelist", + "compact", + "concat", + "contains", + "csvdecode", + "dirname", + "distinct", + "endswith", + "filebase64sha512", + "fileexists", + "filemd5", + "fileset", + "filesha1", + "filesha256", + "filesha512", + "flatten", + "floor", + "format", + "formatdate", + "formatlist", + "indent", + "index", + "join", + "jsondecode", + "keys", + "list", + "log", + "lower", + "map", + "matchkeys", + "max", + "md5", + "merge", + "min", + "nonsensitive", + "one", + "parseint", + "pathexpand", + "pow", + "range", + "regex", + "regexall", + "replace", + "reverse", + "rsadecrypt", + "sensitive", + "setintersection", + "setproduct", + "setsubtract", + "setunion", + "sha256", + "sha512", + "signum", + "slice", + "sort", + "startswith", + "strrev", + "substr", + "sum", + "templatefile", + "textdecodebase64", + "textencodebase64", + "timeadd", + "timecmp", + "timestamp", + "title", + "tobool", + "tolist", + "tomap", + "tonumber", + "toset", + "tostring", + "transpose", + "trim", + "trimprefix", + "trimspace", + "trimsuffix", + "try", + "type", + "upper", + "urlencode", + "uuid", + "uuidv5", + "values", + "yamldecode", + "yamlencode", + "zipmap", +]) + +if __name__ == "__main__": + with tempfile.TemporaryDirectory() as dir: + repo = Repo.clone_from("https://github.com/hashicorp/terraform.git", dir) + + hcl = """# DO NOT EDIT BY HAND +# This file is auto generated by ./scripts/generate_builtins.py + +locals { + # A load of the examples in the docs use `path.module` which _should_ resolve to the file system path of + # the current module, but tf2pulumi doesn't support that so we replace it with local.path_module. + path_module = "some/path" +} +""" + + functions_path = os.path.join(dir, "website", "docs", "language", "functions") + for file in sorted(os.listdir(functions_path)): + if file == "index.mdx": + continue # skip the index file + + with open(os.path.join(functions_path, file)) as f: + mdx = f.read().splitlines() + + # Default to the file name minus extension, but look for "# `NAME` Function" in the code below + function_name = trimext(os.path.basename(file)) + + # Look for the first ``` after ## Examples + in_code = False + in_examples = False + example_code = [] + for line in mdx: + m = re.match("# `([a-z]+)` Function", line) + if m: + function_name = m.group(1) + + if "## Examples" in line: + in_examples = True + continue + + if in_examples: + if line.startswith("```"): + in_code = not in_code + continue + + if in_code: + if line.startswith("> "): + code = line[1:].strip().replace("path.module", "local.path_module") + example_code.append(code) + + if function_name in overrides: + override = overrides[function_name] + if isinstance(override, list): + example_code = override + else: + example_code = [override] + elif not example_code: + raise Exception(f"No examples found for {function_name}") + + if function_name not in unsupported: + # Only write out the example if we support it + hcl += "\n# Examples for " + function_name + "\n" + for index, example in enumerate(example_code): + suffix = "" + if len(example_code) > 1: + # If we have more than one example suffix with the index + suffix = str(index) + + hcl += "output \"func" + function_name.capitalize() + suffix + "\" {\n" + hcl += " value = " + example + "\n" + hcl += "}\n\n" + + + targetFile = os.path.join(os.path.dirname(__file__), "..", "pkg", "tf2pulumi", "convert", "testdata", "builtins", "main.tf") + with open(targetFile, "w") as f: + f.write(hcl) \ No newline at end of file diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 000000000..59348f98e --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +gitpython