diff --git a/src/main/antlr4/io/codeka/gaia/hcl/antlr/hcl.g4 b/src/main/antlr4/io/codeka/gaia/hcl/antlr/hcl.g4 index a8548bcbd..9bfea0388 100644 --- a/src/main/antlr4/io/codeka/gaia/hcl/antlr/hcl.g4 +++ b/src/main/antlr4/io/codeka/gaia/hcl/antlr/hcl.g4 @@ -5,10 +5,20 @@ file ; directive - : variableDirective + : providerDirective + | resourceDirective + | variableDirective | outputDirective ; +providerDirective + : 'provider' STRING object + ; + +resourceDirective + : 'resource' STRING STRING object + ; + variableDirective : 'variable' STRING variableBlock ; diff --git a/src/main/java/io/codeka/gaia/hcl/HclParserImpl.kt b/src/main/java/io/codeka/gaia/hcl/HclParserImpl.kt index 130ebba7e..db9bb89fe 100644 --- a/src/main/java/io/codeka/gaia/hcl/HclParserImpl.kt +++ b/src/main/java/io/codeka/gaia/hcl/HclParserImpl.kt @@ -12,6 +12,7 @@ interface HclParser { fun parseContent(content: String): HclVisitor fun parseVariables(content: String): List fun parseOutputs(content: String): List + fun parseProvider(fileContent: String): String } @Service @@ -45,4 +46,9 @@ class HclParserImpl : HclParser { val hclVisitor = parseContent(content) return hclVisitor.outputs } + + override fun parseProvider(content: String): String { + val hclVisitor = parseContent(content) + return hclVisitor.provider + } } \ No newline at end of file diff --git a/src/main/java/io/codeka/gaia/hcl/HclVisitor.kt b/src/main/java/io/codeka/gaia/hcl/HclVisitor.kt index 532239b94..26e883847 100644 --- a/src/main/java/io/codeka/gaia/hcl/HclVisitor.kt +++ b/src/main/java/io/codeka/gaia/hcl/HclVisitor.kt @@ -10,10 +10,14 @@ class HclVisitor : hclBaseVisitor() { var variables: MutableList = LinkedList() var outputs: MutableList = LinkedList() + var provider: String = "unknown" + + private val IGNORED_PROVIDERS = setOf("null", "random", "template", "terraform") private var currentVariable: Variable = Variable(name = "") private var currentOutput: Output = Output() + override fun visitVariableDirective(ctx: hclParser.VariableDirectiveContext) { currentVariable = Variable(name = ctx.STRING().text.removeSurrounding("\"")) variables.add(currentVariable) @@ -49,4 +53,20 @@ class HclVisitor : hclBaseVisitor() { override fun visitOutputSensitive(ctx: hclParser.OutputSensitiveContext) { currentOutput.sensitive = ctx.BOOLEAN().text.removeSurrounding("\"") } + + override fun visitProviderDirective(ctx: hclParser.ProviderDirectiveContext) { + val parsedProvider = ctx.STRING().text.removeSurrounding("\"") + if (! IGNORED_PROVIDERS.contains(parsedProvider)) { + provider = parsedProvider + } + } + + override fun visitResourceDirective(ctx: hclParser.ResourceDirectiveContext) { + // provider already found ! + if (provider != "unknown") return + + // check first part of the resource type + provider = ctx.STRING(0).text.removeSurrounding("\"") + .substringBefore("_") + } } \ No newline at end of file diff --git a/src/main/java/io/codeka/gaia/modules/bo/TerraformModule.java b/src/main/java/io/codeka/gaia/modules/bo/TerraformModule.java index d0230e022..719110cb1 100644 --- a/src/main/java/io/codeka/gaia/modules/bo/TerraformModule.java +++ b/src/main/java/io/codeka/gaia/modules/bo/TerraformModule.java @@ -44,6 +44,8 @@ public class TerraformModule { private RegistryDetails registryDetails; + private String mainProvider; + public String getId() { return id; } @@ -152,4 +154,11 @@ public void setModuleMetadata(ModuleMetadata moduleMetadata) { this.moduleMetadata = moduleMetadata; } + public String getMainProvider() { + return mainProvider; + } + + public void setMainProvider(String mainProvider) { + this.mainProvider = mainProvider; + } } diff --git a/src/main/java/io/codeka/gaia/registries/service/RegistryService.kt b/src/main/java/io/codeka/gaia/registries/service/RegistryService.kt index 1ec8b0110..3bc0d7449 100644 --- a/src/main/java/io/codeka/gaia/registries/service/RegistryService.kt +++ b/src/main/java/io/codeka/gaia/registries/service/RegistryService.kt @@ -43,6 +43,10 @@ class RegistryServiceImpl( val variablesFile = registryApis[registryType]?.getFileContent(user, projectId, "variables.tf")!! module.variables = hclParser.parseVariables(variablesFile) + // find main provider + val mainFile = registryApis[registryType]?.getFileContent(user, projectId, "main.tf")!! + module.mainProvider = hclParser.parseProvider(mainFile) + // saving module ! return moduleRepository.save(module) } diff --git a/src/main/resources/static/images/aws.png b/src/main/resources/static/images/providers/aws.png similarity index 100% rename from src/main/resources/static/images/aws.png rename to src/main/resources/static/images/providers/aws.png diff --git a/src/main/resources/static/images/providers/azurerm.png b/src/main/resources/static/images/providers/azurerm.png new file mode 100644 index 000000000..fe2c7d50b Binary files /dev/null and b/src/main/resources/static/images/providers/azurerm.png differ diff --git a/src/main/resources/static/images/providers/docker.png b/src/main/resources/static/images/providers/docker.png new file mode 100644 index 000000000..d83e54a7e Binary files /dev/null and b/src/main/resources/static/images/providers/docker.png differ diff --git a/src/main/resources/static/images/providers/google.png b/src/main/resources/static/images/providers/google.png new file mode 100644 index 000000000..7e23a5cb0 Binary files /dev/null and b/src/main/resources/static/images/providers/google.png differ diff --git a/src/main/resources/templates/module_description.html b/src/main/resources/templates/module_description.html index e78c084fa..fdd3044d9 100644 --- a/src/main/resources/templates/module_description.html +++ b/src/main/resources/templates/module_description.html @@ -46,7 +46,7 @@
- +

@@ -107,9 +107,6 @@

$.get(`/api/modules/${moduleId}`) .then(data => { - // START FIXME: link with real data once implemented in the model - data.mainProvider = 'aws'; - // END FIXME new Vue({ el: "#app", data, diff --git a/src/test/java/io/codeka/gaia/hcl/HCLParserTest.java b/src/test/java/io/codeka/gaia/hcl/HCLParserTest.java deleted file mode 100644 index d9eeacffe..000000000 --- a/src/test/java/io/codeka/gaia/hcl/HCLParserTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.codeka.gaia.hcl; - -import io.codeka.gaia.modules.bo.Output; -import io.codeka.gaia.modules.bo.Variable; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.Test; -import org.springframework.core.io.ClassPathResource; - -import java.io.IOException; -import java.nio.charset.Charset; - -import static org.assertj.core.api.Assertions.assertThat; - -class HCLParserTest { - - private HclParserImpl hclParser = new HclParserImpl(); - - @Test - void parsing_variables_shouldWorkWithVisitor() throws IOException { - // given - var fileContent = IOUtils.toString(new ClassPathResource("hcl/variables.tf").getURL(), Charset.defaultCharset()); - - // when - var variables = hclParser.parseVariables(fileContent); - - // then - assertThat(variables).hasSize(3); - - var stringVar = new Variable("string_var"); - stringVar.setType("string"); - stringVar.setDescription("a test string var"); - stringVar.setDefaultValue("foo"); - - var numberVar = new Variable("number_var"); - numberVar.setType("number"); - numberVar.setDescription("a test number var"); - numberVar.setDefaultValue("42"); - - var boolVar = new Variable("bool_var"); - boolVar.setDefaultValue("false"); - - assertThat(variables).contains(stringVar, numberVar, boolVar); - } - - @Test - void parsing_variables_shouldWork_withComplexFile() throws IOException { - // given - var fileContent = IOUtils.toString(new ClassPathResource("hcl/variables_aws_eks.tf").getURL(), Charset.defaultCharset()); - - // when - var variables = hclParser.parseVariables(fileContent); - - // then - assertThat(variables).hasSize(49); - } - - @Test - void parsing_variables_shouldWork_withAnotherComplexFile() throws IOException { - // given - var fileContent = IOUtils.toString(new ClassPathResource("hcl/variables_aws_vpc.tf").getURL(), Charset.defaultCharset()); - - // when - var variables = hclParser.parseVariables(fileContent); - - // then - assertThat(variables).hasSize(282); - } - - @Test - void parsing_outputs_shouldWorkWithVisitor() throws IOException { - // given - var fileContent = IOUtils.toString(new ClassPathResource("hcl/outputs.tf").getURL(), Charset.defaultCharset()); - - // when - var outputs = hclParser.parseOutputs(fileContent); - - // then - assertThat(outputs).hasSize(2); - - var output1 = new Output("instance_ip_addr", "${aws_instance.server.private_ip}", "The private IP address of the main server instance.", "false"); - var output2 = new Output("db_password", "aws_db_instance.db[1].password", "The password for logging in to the database.", "true"); - - assertThat(outputs).contains(output1, output2); - } - - @Test - void parsing_outputs_shouldWork_withComplexFile() throws IOException { - // given - var fileContent = IOUtils.toString(new ClassPathResource("hcl/outputs_aws_eks.tf").getURL(), Charset.defaultCharset()); - - // when - var outputs = hclParser.parseOutputs(fileContent); - - // then - assertThat(outputs).hasSize(27); - } - -} \ No newline at end of file diff --git a/src/test/java/io/codeka/gaia/hcl/HCLParserTest.kt b/src/test/java/io/codeka/gaia/hcl/HCLParserTest.kt new file mode 100644 index 000000000..793defa3d --- /dev/null +++ b/src/test/java/io/codeka/gaia/hcl/HCLParserTest.kt @@ -0,0 +1,140 @@ +package io.codeka.gaia.hcl + +import io.codeka.gaia.modules.bo.Output +import io.codeka.gaia.modules.bo.Variable +import org.apache.commons.io.IOUtils +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource +import java.io.IOException +import java.nio.charset.Charset + +class HCLParserTest { + + private val hclParser = HclParserImpl() + + @Test + @Throws(IOException::class) + fun parsing_variables_shouldWorkWithVisitor() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/variables.tf").url, Charset.defaultCharset()) + + // when + val variables = hclParser.parseVariables(fileContent) + + // then + assertThat(variables).hasSize(3) + val stringVar = Variable("string_var", "string", "a test string var", "foo") + val numberVar = Variable("number_var", "number", "a test number var", "42") + val boolVar = Variable("bool_var", defaultValue = "false") + assertThat(variables).contains(stringVar, numberVar, boolVar) + } + + @Test + @Throws(IOException::class) + fun parsing_variables_shouldWork_withComplexFile() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/variables_aws_eks.tf").url, Charset.defaultCharset()) + + // when + val variables = hclParser.parseVariables(fileContent) + + // then + assertThat(variables).hasSize(49) + } + + @Test + @Throws(IOException::class) + fun parsing_variables_shouldWork_withAnotherComplexFile() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/variables_aws_vpc.tf").url, Charset.defaultCharset()) + + // when + val variables = hclParser.parseVariables(fileContent) + + // then + assertThat(variables).hasSize(282) + } + + @Test + @Throws(IOException::class) + fun parsing_outputs_shouldWorkWithVisitor() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/outputs.tf").url, Charset.defaultCharset()) + + // when + val outputs = hclParser.parseOutputs(fileContent) + + // then + assertThat(outputs).hasSize(2) + val output1 = Output("instance_ip_addr", "\${aws_instance.server.private_ip}", "The private IP address of the main server instance.", "false") + val output2 = Output("db_password", "aws_db_instance.db[1].password", "The password for logging in to the database.", "true") + assertThat(outputs).contains(output1, output2) + } + + @Test + @Throws(IOException::class) + fun parsing_outputs_shouldWork_withComplexFile() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/outputs_aws_eks.tf").url, Charset.defaultCharset()) + + // when + val outputs = hclParser.parseOutputs(fileContent) + + // then + assertThat(outputs).hasSize(27) + } + + /** + * Tests for the provider part + */ + @Nested + inner class ProviderTest { + + @Test + @Throws(IOException::class) + fun parsing_provider_shouldWork_withMainFile_includingProviderDirective() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/terraform_docker_mongo_main_with_provider.tf").url, Charset.defaultCharset()) + // when + val provider: String = hclParser.parseProvider(fileContent) + // then + assertThat(provider).isEqualTo("docker") + } + + @Test + @Throws(IOException::class) + fun parsing_provider_shouldWork_withMainFile_withoutProviderDirective() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/terraform_docker_mongo_main_without_provider.tf").url, Charset.defaultCharset()) + + // when + val provider: String = hclParser.parseProvider(fileContent) + + // then + assertThat(provider).isEqualTo("docker") + } + + @Test + @Throws(IOException::class) + fun parsing_provider_shouldReturn_unknown_ifNoProviderFound() { + // given + val fileContent = IOUtils.toString(ClassPathResource("hcl/variables.tf").url, Charset.defaultCharset()) + + // when + val provider: String = hclParser.parseProvider(fileContent) + + // then + assertThat(provider).isEqualTo("unknown") + } + + @Test + fun dummyProvidersShouldBeIgnored() { + assertThat(hclParser.parseProvider("""provider "null" {} """)).isEqualTo("unknown") + assertThat(hclParser.parseProvider("""provider "template" {} """)).isEqualTo("unknown") + assertThat(hclParser.parseProvider("""provider "random" {} """)).isEqualTo("unknown") + assertThat(hclParser.parseProvider("""provider "terraform" {} """)).isEqualTo("unknown") + } + } +} \ No newline at end of file diff --git a/src/test/java/io/codeka/gaia/registries/controller/GithubRegistryControllerIT.kt b/src/test/java/io/codeka/gaia/registries/controller/GithubRegistryControllerIT.kt index 3f8f7bf51..9467e39b3 100644 --- a/src/test/java/io/codeka/gaia/registries/controller/GithubRegistryControllerIT.kt +++ b/src/test/java/io/codeka/gaia/registries/controller/GithubRegistryControllerIT.kt @@ -109,6 +109,10 @@ class GithubRegistryControllerIT{ .andExpect(MockRestRequestMatchers.header("Authorization", "Bearer Tok'ra")) .andRespond(MockRestResponseCreators.withSuccess(ClassPathResource("/rest/github/selmak-terraform-docker-mongo-content-variables.json"), MediaType.APPLICATION_JSON)) + server.expect(requestTo("https://api.github.com/repos/selmak/terraform-docker-mongo/contents/main.tf?ref=master")) + .andExpect(MockRestRequestMatchers.header("Authorization", "Bearer Tok'ra")) + .andRespond(MockRestResponseCreators.withSuccess(ClassPathResource("/rest/github/selmak-terraform-docker-mongo-content-main.json"), MediaType.APPLICATION_JSON)) + val selmak = User("Selmak", null) selmak.oAuth2User = OAuth2User("GITHUB", "Tok'ra", null) diff --git a/src/test/java/io/codeka/gaia/registries/controller/GitlabRegistryControllerIT.kt b/src/test/java/io/codeka/gaia/registries/controller/GitlabRegistryControllerIT.kt index ccfec2ffd..edefc30c2 100644 --- a/src/test/java/io/codeka/gaia/registries/controller/GitlabRegistryControllerIT.kt +++ b/src/test/java/io/codeka/gaia/registries/controller/GitlabRegistryControllerIT.kt @@ -103,6 +103,10 @@ class GitlabRegistryControllerIT{ .andExpect(MockRestRequestMatchers.header("Authorization", "Bearer Tok'ra")) .andRespond(MockRestResponseCreators.withSuccess(ClassPathResource("/rest/gitlab/selmak-terraform-docker-mongo-content-variables.json"), MediaType.APPLICATION_JSON)) + server.expect(requestTo("https://gitlab.com/api/v4/projects/16181047/repository/files/main.tf?ref=master")) + .andExpect(MockRestRequestMatchers.header("Authorization", "Bearer Tok'ra")) + .andRespond(MockRestResponseCreators.withSuccess(ClassPathResource("/rest/gitlab/selmak-terraform-docker-mongo-content-main.json"), MediaType.APPLICATION_JSON)) + val selmak = User("Selmak", null) selmak.oAuth2User = OAuth2User("GITLAB", "Tok'ra", null) diff --git a/src/test/java/io/codeka/gaia/registries/service/RegistryServiceImplTest.kt b/src/test/java/io/codeka/gaia/registries/service/RegistryServiceImplTest.kt index 21e38b285..876eddb98 100644 --- a/src/test/java/io/codeka/gaia/registries/service/RegistryServiceImplTest.kt +++ b/src/test/java/io/codeka/gaia/registries/service/RegistryServiceImplTest.kt @@ -69,6 +69,9 @@ class RegistryServiceImplTest { whenever(gitlabRegistryApi.getFileContent(user, "15689", "variables.tf")).thenReturn(variablesFileContent) whenever(hclParser.parseVariables(variablesFileContent)).thenReturn(listOf(Variable("dummy"))) + whenever(gitlabRegistryApi.getFileContent(user, "15689", "main.tf")).thenReturn(variablesFileContent) + whenever(hclParser.parseProvider(variablesFileContent)).thenReturn("docker") + val module = registryService.importRepository("15689", RegistryType.GITLAB, user) verify(gitlabRegistryApi).getRepository(user, "15689") @@ -88,6 +91,8 @@ class RegistryServiceImplTest { assertThat(module.cliVersion).isEqualTo("1.12.8") assertThat(module.moduleMetadata.createdBy).isEqualTo(user) + assertThat(module.mainProvider).isEqualTo("docker") + assertThat(module.variables).containsExactly(Variable("dummy")) } @@ -107,6 +112,9 @@ class RegistryServiceImplTest { whenever(githubRegistryApi.getFileContent(user, "juwit/terraform-docker-mongo", "variables.tf")).thenReturn(variablesFileContent) whenever(hclParser.parseVariables(variablesFileContent)).thenReturn(listOf(Variable("dummy"))) + whenever(githubRegistryApi.getFileContent(user, "juwit/terraform-docker-mongo", "main.tf")).thenReturn(variablesFileContent) + whenever(hclParser.parseProvider(variablesFileContent)).thenReturn("docker") + val module = registryService.importRepository("juwit/terraform-docker-mongo", RegistryType.GITHUB, user) verify(githubRegistryApi).getRepository(user, "juwit/terraform-docker-mongo") @@ -126,6 +134,8 @@ class RegistryServiceImplTest { assertThat(module.cliVersion).isEqualTo("1.12.8") assertThat(module.moduleMetadata.createdBy).isEqualTo(user) + assertThat(module.mainProvider).isEqualTo("docker") + assertThat(module.variables).containsExactly(Variable("dummy")) } } \ No newline at end of file diff --git a/src/test/resources/hcl/terraform_docker_mongo_main_with_provider.tf b/src/test/resources/hcl/terraform_docker_mongo_main_with_provider.tf new file mode 100644 index 000000000..6d1804eb0 --- /dev/null +++ b/src/test/resources/hcl/terraform_docker_mongo_main_with_provider.tf @@ -0,0 +1,20 @@ +# use local docker as provider +provider "docker" { + host = "unix:///var/run/docker.sock" +} + +# get the mongo docker image +resource "docker_image" "mongo" { + name = "mongo" + keep_locally = true +} + +# start a container and expose the 27017 port +resource "docker_container" "mongo" { + name = var.mongo_container_name + image = docker_image.mongo.latest + ports = { + internal = 27017 + external = var.mongo_exposed_port + } +} \ No newline at end of file diff --git a/src/test/resources/hcl/terraform_docker_mongo_main_without_provider.tf b/src/test/resources/hcl/terraform_docker_mongo_main_without_provider.tf new file mode 100644 index 000000000..97c6fae77 --- /dev/null +++ b/src/test/resources/hcl/terraform_docker_mongo_main_without_provider.tf @@ -0,0 +1,15 @@ +# get the mongo docker image +resource "docker_image" "mongo" { + name = "mongo" + keep_locally = true +} + +# start a container and expose the 27017 port +resource "docker_container" "mongo" { + name = var.mongo_container_name + image = docker_image.mongo.latest + ports = { + internal = 27017 + external = var.mongo_exposed_port + } +} \ No newline at end of file diff --git a/src/test/resources/rest/github/selmak-terraform-docker-mongo-content-main.json b/src/test/resources/rest/github/selmak-terraform-docker-mongo-content-main.json new file mode 100644 index 000000000..7814de951 --- /dev/null +++ b/src/test/resources/rest/github/selmak-terraform-docker-mongo-content-main.json @@ -0,0 +1,18 @@ +{ + "name": "main.tf", + "path": "main.tf", + "sha": "0a5cc012309c07f639bf522dec2dab19783608c9", + "size": 360, + "url": "https://api.github.com/repos/selmak/terraform-docker-mongo/contents/main.tf?ref=master", + "html_url": "https://github.com/selmak/terraform-docker-mongo/blob/master/main.tf", + "git_url": "https://api.github.com/repos/selmak/terraform-docker-mongo/git/blobs/0a5cc012309c07f639bf522dec2dab19783608c9", + "download_url": "https://raw.githubusercontent.com/selmak/terraform-docker-mongo/master/main.tf", + "type": "file", + "content": "IyBnZXQgdGhlIG1vbmdvIGRvY2tlciBpbWFnZQpyZXNvdXJjZSAiZG9ja2Vy\nX2ltYWdlIiAibW9uZ28iIHsKICBuYW1lICAgICAgICAgPSAibW9uZ28iCiAg\na2VlcF9sb2NhbGx5ID0gdHJ1ZQp9CgojIHN0YXJ0IGEgY29udGFpbmVyIGFu\nZCBleHBvc2UgdGhlIDI3MDE3IHBvcnQKcmVzb3VyY2UgImRvY2tlcl9jb250\nYWluZXIiICJtb25nbyIgewogIG5hbWUgID0gIiR7dmFyLm1vbmdvX2NvbnRh\naW5lcl9uYW1lfSIKICBpbWFnZSA9ICIke2RvY2tlcl9pbWFnZS5tb25nby5s\nYXRlc3R9IgogIHBvcnRzID0gewogICAgaW50ZXJuYWwgPSAyNzAxNwogICAg\nZXh0ZXJuYWwgPSAiJHt2YXIubW9uZ29fZXhwb3NlZF9wb3J0fSIKICB9Cn0K\n", + "encoding": "base64", + "_links": { + "self": "https://api.github.com/repos/selmak/terraform-docker-mongo/contents/main.tf?ref=master", + "git": "https://api.github.com/repos/selmak/terraform-docker-mongo/git/blobs/0a5cc012309c07f639bf522dec2dab19783608c9", + "html": "https://github.com/selmak/terraform-docker-mongo/blob/master/main.tf" + } +} diff --git a/src/test/resources/rest/gitlab/selmak-terraform-docker-mongo-content-main.json b/src/test/resources/rest/gitlab/selmak-terraform-docker-mongo-content-main.json new file mode 100644 index 000000000..6a8736886 --- /dev/null +++ b/src/test/resources/rest/gitlab/selmak-terraform-docker-mongo-content-main.json @@ -0,0 +1,12 @@ +{ + "file_name": "main.tf", + "file_path": "main.tf", + "size": 360, + "encoding": "base64", + "content_sha256": "179816b847fa491f6a9b4d7eff54d6be640b397d8f0ec02a62ad6ed6220057b6", + "ref": "master", + "blob_id": "0a5cc012309c07f639bf522dec2dab19783608c9", + "commit_id": "91ee4feddf2d3c489f8ea1b2489027bb05839ac1", + "last_commit_id": "e01d6f909cd078253d6ef557769fc27c5df5d1e7", + "content": "IyBnZXQgdGhlIG1vbmdvIGRvY2tlciBpbWFnZQpyZXNvdXJjZSAiZG9ja2VyX2ltYWdlIiAibW9uZ28iIHsKICBuYW1lICAgICAgICAgPSAibW9uZ28iCiAga2VlcF9sb2NhbGx5ID0gdHJ1ZQp9CgojIHN0YXJ0IGEgY29udGFpbmVyIGFuZCBleHBvc2UgdGhlIDI3MDE3IHBvcnQKcmVzb3VyY2UgImRvY2tlcl9jb250YWluZXIiICJtb25nbyIgewogIG5hbWUgID0gIiR7dmFyLm1vbmdvX2NvbnRhaW5lcl9uYW1lfSIKICBpbWFnZSA9ICIke2RvY2tlcl9pbWFnZS5tb25nby5sYXRlc3R9IgogIHBvcnRzID0gewogICAgaW50ZXJuYWwgPSAyNzAxNwogICAgZXh0ZXJuYWwgPSAiJHt2YXIubW9uZ29fZXhwb3NlZF9wb3J0fSIKICB9Cn0K" +} \ No newline at end of file