├── .go-version ├── CODEOWNERS ├── .gitignore ├── Dockerfile ├── .github └── workflows │ ├── test.yaml │ └── lint.yaml ├── go.mod ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── go.sum ├── README.md ├── contrib └── hashicorp │ └── terraform │ ├── format_test.go │ └── format.go ├── tfk8s.go └── tfk8s_test.go /.go-version: -------------------------------------------------------------------------------- 1 | 1.19 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @jrhouston 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | release/ 2 | scratch/ 3 | tfk8s 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19-alpine as build 2 | WORKDIR /build 3 | COPY go.* ./ 4 | RUN go mod download 5 | COPY . . 6 | RUN apk --no-cache add make 7 | RUN CGO_ENABLED=0 make build 8 | 9 | FROM scratch 10 | COPY --from=build /build/tfk8s /bin/tfk8s 11 | ENTRYPOINT ["/bin/tfk8s"] 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: "Unit Tests" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | unit_test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-go@v3 17 | with: 18 | go-version-file: '.go-version' 19 | cache: true 20 | - name: test 21 | run: make test 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jrhouston/tfk8s 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/spf13/pflag v1.0.3 7 | github.com/stretchr/testify v1.5.1 8 | github.com/zclconf/go-cty v1.8.0 9 | sigs.k8s.io/yaml v1.1.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/google/go-cmp v0.5.2 // indirect 15 | github.com/pmezard/go-difflib v1.0.0 // indirect 16 | golang.org/x/text v0.3.8 // indirect 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 18 | gopkg.in/yaml.v2 v2.2.8 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | golangci: 16 | name: lint 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 5 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-go@v3 22 | with: 23 | go-version: '1.19' 24 | cache: true 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v3 27 | with: 28 | version: v1.48 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.1.10 2 | 3 | - Fix generation field not being stripped when using --strip 4 | 5 | # 0.1.9 6 | 7 | - Fix comma placement when formatting multi-line string values inside lists 8 | 9 | # 0.1.8 10 | 11 | - Support resources using generateName 12 | - Add option to remove quotes from object/map keys when not needed 13 | - Match nested interpolation when escaping shell 14 | 15 | # 0.1.7 16 | 17 | - Escape shell vars in HCL output 18 | 19 | # 0.1.6 20 | 21 | - Fix crash when trying to use List resources 22 | - Catch panics and print friendly error message 23 | 24 | # 0.1.5 25 | 26 | - Remove dependency on terraform in go.mod 27 | 28 | # 0.1.4 29 | 30 | - Fix empty YAML crash (#21) 31 | 32 | # 0.1.3 33 | 34 | - Ignore empty documents 35 | 36 | # 0.1.2 37 | 38 | - Add heredoc syntax for multiline strings (#14) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 John Houston 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build docker docker-push release install test clean 2 | 3 | VERSION := 0.1.10 4 | DOCKER_IMAGE_NAME := jrhouston/tfk8s 5 | 6 | build: 7 | go build -ldflags "-X main.toolVersion=${VERSION}" 8 | 9 | docker: 10 | docker build -t ${DOCKER_IMAGE_NAME}:${VERSION} . 11 | 12 | docker-push: docker 13 | docker push ${DOCKER_IMAGE_NAME}:${VERSION} 14 | 15 | release: clean 16 | mkdir -p release/ 17 | # FIXME use gox for this 18 | GOOS=linux GOOARCH=386 go build -ldflags "-X main.toolVersion=${VERSION}" -o release/tfk8s_${VERSION}_linux_386 19 | zip -j release/tfk8s_${VERSION}_linux_386.zip release/tfk8s_${VERSION}_linux_386 20 | GOOS=linux GOOARCH=amd64 go build -ldflags "-X main.toolVersion=${VERSION}" -o release/tfk8s_${VERSION}_linux_amd64 21 | zip -j release/tfk8s_${VERSION}_linux_amd64.zip release/tfk8s_${VERSION}_linux_amd64 22 | GOOS=linux GOOARCH=arm go build -ldflags "-X main.toolVersion=${VERSION}" -o release/tfk8s_${VERSION}_linux_arm 23 | zip -j release/tfk8s_${VERSION}_linux_arm.zip release/tfk8s_${VERSION}_linux_arm 24 | GOOS=darwin GOOARCH=amd64 go build -ldflags "-X main.toolVersion=${VERSION}" -o release/tfk8s_${VERSION}_darwin_amd64 25 | zip -j release/tfk8s_${VERSION}_darwin_amd64.zip release/tfk8s_${VERSION}_darwin_amd64 26 | GOOS=windows GOOARCH=amd64 go build -ldflags "-X main.toolVersion=${VERSION}" -o release/tfk8s_${VERSION}_windows_amd64 27 | zip -j release/tfk8s_${VERSION}_windows_amd64.zip release/tfk8s_${VERSION}_windows_amd64 28 | GOOS=windows GOOARCH=386 go build -ldflags "-X main.toolVersion=${VERSION}" -o release/tfk8s_${VERSION}_windows_386 29 | zip -j release/tfk8s_${VERSION}_windows_386.zip release/tfk8s_${VERSION}_windows_386 30 | 31 | install: 32 | go install -ldflags "-X main.toolVersion=${VERSION}" 33 | 34 | test: 35 | go test -v ./... 36 | 37 | clean: 38 | rm -rf release/* 39 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 6 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 7 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 8 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 9 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 11 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 12 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 13 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 18 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 21 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 22 | github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= 23 | github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 24 | github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= 25 | github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= 26 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 28 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 30 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 31 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 32 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 34 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 35 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 36 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 40 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 41 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 43 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 44 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 45 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 46 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tfk8s [![Go Report Card](https://goreportcard.com/badge/github.com/jrhouston/tfk8s)](https://goreportcard.com/report/github.com/jrhouston/tfk8s) ![tests](https://github.com/jrhouston/tfk8s/actions/workflows/test.yaml/badge.svg) 2 | 3 | --- 4 | 5 | ![](https://media.giphy.com/media/g8GfH3i5F0hby/giphy.gif) 6 | 7 | `tfk8s` is a tool that makes it easier to work with the [Terraform Kubernetes Provider](https://github.com/hashicorp/terraform-provider-kubernetes). 8 | 9 | If you want to copy examples from the Kubernetes documentation or migrate existing YAML manifests and use them with Terraform without having to convert YAML to HCL by hand, this tool is for you. 10 | 11 | - [Demo](#demo) 12 | - [Features](#features) 13 | - [Install](#install) 14 | - [Usage](#usage) 15 | - [Examples](#examples) 16 | - [Create Terraform configuration from YAML files](#create-terraform-configuration-from-yaml-files) 17 | - [Use with kubectl to output maps instead of YAML](#use-with-kubectl-to-output-maps-instead-of-yaml) 18 | - [Convert a Helm chart to Terraform](#convert-a-helm-chart-to-terraform) 19 | - [Convert a directory tree of manifests to Terraform](#convert-a-directory-tree-of-manifests-to-terraform) 20 | 21 | ## Demo 22 | 23 | [](https://asciinema.org/a/jSmyAg4Ar6EcwKCTCXN8iAJM2) 24 | 25 | ## Features 26 | 27 | - Convert a YAML file containing multiple manifests 28 | - Strip out server side fields when piping `kubectl get $R -o yaml | tfk8s --strip` 29 | 30 | ## Install 31 | 32 | ``` 33 | go install github.com/jrhouston/tfk8s@latest 34 | ``` 35 | 36 | Alternatively, clone this repo and run: 37 | 38 | ``` 39 | make install 40 | ``` 41 | 42 | If Go's bin directory is not in your `PATH` you will need to add it: 43 | 44 | ``` 45 | export PATH=$PATH:$(go env GOPATH)/bin 46 | ``` 47 | 48 | Or you can install via [brew](https://formulae.brew.sh/formula/tfk8s) for macOS/Linux: 49 | 50 | ``` 51 | brew install tfk8s 52 | ``` 53 | 54 | On macOS, you can also install via [MacPorts](https://www.macports.org): 55 | 56 | ``` 57 | sudo port install tfk8s 58 | ``` 59 | 60 | ## Usage 61 | 62 | ``` 63 | Usage of tfk8s: 64 | -f, --file string Input file containing Kubernetes YAML manifests (default "-") 65 | -M, --map-only Output only an HCL map structure 66 | -o, --output string Output file to write Terraform config (default "-") 67 | -p, --provider provider Provider alias to populate the provider attribute 68 | -s, --strip Strip out server side fields - use if you are piping from kubectl get 69 | -Q, --strip-key-quotes Strip out quotes from HCL map keys unless they are required. 70 | -V, --version Show tool version 71 | ``` 72 | 73 | ## Examples 74 | 75 | ### Create Terraform configuration from YAML files 76 | 77 | ``` 78 | tfk8s -f input.yaml -o output.tf 79 | ``` 80 | 81 | or, using pipes: 82 | ``` 83 | cat input.yaml | tfk8s > output.tf 84 | ``` 85 | 86 | **input.yaml**: 87 | ```yaml 88 | --- 89 | apiVersion: v1 90 | kind: ConfigMap 91 | metadata: 92 | name: test 93 | data: 94 | TEST: test 95 | ``` 96 | 97 | ✨✨ magically becomes ✨✨ 98 | 99 | **output.tf**: 100 | ```hcl 101 | resource "kubernetes_manifest" "configmap_test" { 102 | manifest = { 103 | "apiVersion" = "v1" 104 | "data" = { 105 | "TEST" = "test" 106 | } 107 | "kind" = "ConfigMap" 108 | "metadata" = { 109 | "name" = "test" 110 | } 111 | } 112 | } 113 | ``` 114 | 115 | ### Use with kubectl to output maps instead of YAML 116 | 117 | ``` 118 | kubectl get ns default -o yaml | tfk8s -M 119 | ``` 120 | ```hcl 121 | { 122 | "apiVersion" = "v1" 123 | "kind" = "Namespace" 124 | "metadata" = { 125 | "creationTimestamp" = "2020-05-02T15:01:32Z" 126 | "name" = "default" 127 | "resourceVersion" = "147" 128 | "selfLink" = "/api/v1/namespaces/default" 129 | "uid" = "6ac3424c-07a4-4a69-86ae-cc7a4ae72be3" 130 | } 131 | "spec" = { 132 | "finalizers" = [ 133 | "kubernetes", 134 | ] 135 | } 136 | "status" = { 137 | "phase" = "Active" 138 | } 139 | } 140 | ``` 141 | 142 | ### Convert a Helm chart to Terraform 143 | 144 | You can use `helm template` to generate a manifest from the chart, then pipe it into tfk8s: 145 | 146 | ``` 147 | helm template ./chart-path -f values.yaml | tfk8s 148 | ``` 149 | 150 | ## Convert a directory tree of manifests to Terraform 151 | 152 | You can use `tfk8s` in conjunction with `find` to convert an entire directory recursively: 153 | 154 | ```bash 155 | find dirname/ -name '*.yaml' -type f -exec sh -c 'tfk8s -f {} -o $(echo {} | sed "s/\.[^.]*$//").tf' \; 156 | ``` 157 | -------------------------------------------------------------------------------- /contrib/hashicorp/terraform/format_test.go: -------------------------------------------------------------------------------- 1 | // NOTE this file was lifted verbatim from internal/repl in the terraform project 2 | // because the FormatValue function became internal in v1.0.0 3 | 4 | package terraform 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/zclconf/go-cty/cty" 11 | ) 12 | 13 | func TestFormatValue(t *testing.T) { 14 | tests := []struct { 15 | Val cty.Value 16 | Want string 17 | }{ 18 | { 19 | cty.NullVal(cty.DynamicPseudoType), 20 | `null`, 21 | }, 22 | { 23 | cty.NullVal(cty.String), 24 | `tostring(null)`, 25 | }, 26 | { 27 | cty.NullVal(cty.Number), 28 | `tonumber(null)`, 29 | }, 30 | { 31 | cty.NullVal(cty.Bool), 32 | `tobool(null)`, 33 | }, 34 | { 35 | cty.NullVal(cty.List(cty.String)), 36 | `tolist(null) /* of string */`, 37 | }, 38 | { 39 | cty.NullVal(cty.Set(cty.Number)), 40 | `toset(null) /* of number */`, 41 | }, 42 | { 43 | cty.NullVal(cty.Map(cty.Bool)), 44 | `tomap(null) /* of bool */`, 45 | }, 46 | { 47 | cty.NullVal(cty.Object(map[string]cty.Type{"a": cty.Bool})), 48 | `null /* object */`, // Ideally this would display the full object type, including its attributes 49 | }, 50 | { 51 | cty.UnknownVal(cty.DynamicPseudoType), 52 | `(known after apply)`, 53 | }, 54 | { 55 | cty.StringVal(""), 56 | `""`, 57 | }, 58 | { 59 | cty.StringVal("hello"), 60 | `"hello"`, 61 | }, 62 | { 63 | cty.StringVal("hello\nworld"), 64 | `< 0 { 99 | operator = "<<-" 100 | } 101 | 102 | delimiter := defaultDelimiter 103 | 104 | OUTER: 105 | for { 106 | // Check if any of the lines are in conflict with the delimiter. The 107 | // parser allows leading and trailing whitespace, so we must remove it 108 | // before comparison. 109 | for _, line := range lines { 110 | // If the delimiter matches a line, extend it and start again 111 | if strings.TrimSpace(line) == delimiter { 112 | delimiter = delimiter + "_" 113 | continue OUTER 114 | } 115 | } 116 | 117 | // None of the lines match the delimiter, so we're ready 118 | break 119 | } 120 | 121 | // Write the heredoc, with indentation as appropriate. 122 | var buf strings.Builder 123 | 124 | buf.WriteString(operator) 125 | buf.WriteString(delimiter) 126 | for _, line := range lines { 127 | buf.WriteByte('\n') 128 | buf.WriteString(strings.Repeat(" ", indent)) 129 | buf.WriteString(line) 130 | } 131 | buf.WriteByte('\n') 132 | buf.WriteString(strings.Repeat(" ", indent)) 133 | buf.WriteString(delimiter) 134 | 135 | return buf.String(), true 136 | } 137 | 138 | func formatMappingValue(v cty.Value, indent int, stripKeyQuotes bool) string { 139 | var buf strings.Builder 140 | count := 0 141 | buf.WriteByte('{') 142 | indent += 2 143 | for it := v.ElementIterator(); it.Next(); { 144 | count++ 145 | k, v := it.Element() 146 | buf.WriteByte('\n') 147 | buf.WriteString(strings.Repeat(" ", indent)) 148 | key := FormatValue(k, indent, stripKeyQuotes) 149 | if stripKeyQuotes { 150 | // they can be unquoted if it starts with a letter 151 | // and only contains alphanumeric characeters, dashes, and underlines 152 | m := regexp.MustCompile(`^"[A-Za-z][0-9A-Za-z-_]+"$`) 153 | if m.MatchString(key) { 154 | key = key[1 : len(key)-1] 155 | } 156 | } 157 | buf.WriteString(key) 158 | buf.WriteString(" = ") 159 | buf.WriteString(FormatValue(v, indent, stripKeyQuotes)) 160 | } 161 | indent -= 2 162 | if count > 0 { 163 | buf.WriteByte('\n') 164 | buf.WriteString(strings.Repeat(" ", indent)) 165 | } 166 | buf.WriteByte('}') 167 | return buf.String() 168 | } 169 | 170 | func formatSequenceValue(v cty.Value, indent int, stripKeyQuotes bool) string { 171 | var buf strings.Builder 172 | count := 0 173 | buf.WriteByte('[') 174 | indent += 2 175 | for it := v.ElementIterator(); it.Next(); { 176 | count++ 177 | _, v := it.Element() 178 | buf.WriteByte('\n') 179 | buf.WriteString(strings.Repeat(" ", indent)) 180 | formattedValue := FormatValue(v, indent, stripKeyQuotes) 181 | buf.WriteString(formattedValue) 182 | if strings.HasSuffix(formattedValue, defaultDelimiter) { 183 | // write an additional newline if the value was a multiline string 184 | buf.WriteByte('\n') 185 | buf.WriteString(strings.Repeat(" ", indent)) 186 | } 187 | buf.WriteByte(',') 188 | } 189 | indent -= 2 190 | if count > 0 { 191 | buf.WriteByte('\n') 192 | buf.WriteString(strings.Repeat(" ", indent)) 193 | } 194 | buf.WriteByte(']') 195 | return buf.String() 196 | } 197 | -------------------------------------------------------------------------------- /tfk8s.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | "runtime/debug" 10 | "strings" 11 | 12 | flag "github.com/spf13/pflag" 13 | 14 | cty "github.com/zclconf/go-cty/cty" 15 | ctyjson "github.com/zclconf/go-cty/cty/json" 16 | 17 | yaml "sigs.k8s.io/yaml" 18 | 19 | "github.com/jrhouston/tfk8s/contrib/hashicorp/terraform" 20 | ) 21 | 22 | // toolVersion is the version that gets printed when you run --version 23 | var toolVersion string 24 | 25 | // resourceType is the type of Terraform resource 26 | var resourceType = "kubernetes_manifest" 27 | 28 | // ignoreMetadata is the list of metadata fields to strip 29 | // when --strip is supplied 30 | var ignoreMetadata = []string{ 31 | "creationTimestamp", 32 | "resourceVersion", 33 | "selfLink", 34 | "uid", 35 | "managedFields", 36 | "finalizers", 37 | "generation", 38 | } 39 | 40 | // ignoreAnnotations is the list of annotations to strip 41 | // when --strip is supplied 42 | var ignoreAnnotations = []string{ 43 | "kubectl.kubernetes.io/last-applied-configuration", 44 | } 45 | 46 | // stripServerSideFields removes fields that have been added on the 47 | // server side after the resource was created such as the status field 48 | func stripServerSideFields(doc cty.Value) cty.Value { 49 | m := doc.AsValueMap() 50 | 51 | // strip server-side metadata 52 | metadata := m["metadata"].AsValueMap() 53 | for _, f := range ignoreMetadata { 54 | delete(metadata, f) 55 | } 56 | if v, ok := metadata["annotations"]; ok { 57 | annotations := v.AsValueMap() 58 | for _, a := range ignoreAnnotations { 59 | delete(annotations, a) 60 | } 61 | if len(annotations) == 0 { 62 | delete(metadata, "annotations") 63 | } else { 64 | metadata["annotations"] = cty.ObjectVal(annotations) 65 | } 66 | } 67 | if ns, ok := metadata["namespace"]; ok && ns.AsString() == "default" { 68 | delete(metadata, "namespace") 69 | } 70 | m["metadata"] = cty.ObjectVal(metadata) 71 | 72 | // strip finalizer from spec 73 | if v, ok := m["spec"]; ok { 74 | mm := v.AsValueMap() 75 | delete(mm, "finalizers") 76 | m["spec"] = cty.ObjectVal(mm) 77 | } 78 | 79 | // strip status field 80 | delete(m, "status") 81 | 82 | return cty.ObjectVal(m) 83 | } 84 | 85 | // snakify converts "a-String LIKE this" to "a_string_like_this" 86 | func snakify(s string) string { 87 | re := regexp.MustCompile(`\W`) 88 | return strings.ToLower(re.ReplaceAllString(s, "_")) 89 | } 90 | 91 | // escape incidences of ${} with $${} to prevent Terraform trying to interpolate them 92 | func escapeShellVars(s string) string { 93 | r := regexp.MustCompile(`(\${.*?)`) 94 | return r.ReplaceAllString(s, `$$$1`) 95 | } 96 | 97 | // yamlToHCL converts a single YAML document Terraform HCL 98 | func yamlToHCL( 99 | doc cty.Value, providerAlias string, 100 | stripServerSide bool, mapOnly bool, stripKeyQuotes bool) (string, error) { 101 | m := doc.AsValueMap() 102 | docs := []cty.Value{doc} 103 | if strings.HasSuffix(m["kind"].AsString(), "List") { 104 | docs = m["items"].AsValueSlice() 105 | } 106 | 107 | hcl := "" 108 | for i, doc := range docs { 109 | mm := doc.AsValueMap() 110 | kind := mm["kind"].AsString() 111 | metadata := mm["metadata"].AsValueMap() 112 | var namespace string 113 | if v, ok := metadata["namespace"]; ok { 114 | namespace = v.AsString() 115 | } 116 | 117 | var name string 118 | if n, ok := metadata["name"]; ok { 119 | name = n.AsString() 120 | } else if n, ok := metadata["generateName"]; ok { 121 | name = n.AsString() 122 | if name[len(name)-1] == '-' { 123 | name = name[:len(name)-1] 124 | } 125 | } 126 | 127 | resourceName := kind 128 | if namespace != "" && namespace != "default" { 129 | resourceName = resourceName + "_" + namespace 130 | } 131 | resourceName = resourceName + "_" + name 132 | resourceName = snakify(resourceName) 133 | 134 | if stripServerSide { 135 | doc = stripServerSideFields(doc) 136 | } 137 | s := terraform.FormatValue(doc, 0, stripKeyQuotes) 138 | s = escapeShellVars(s) 139 | 140 | if mapOnly { 141 | hcl += fmt.Sprintf("%v\n", s) 142 | } else { 143 | hcl += fmt.Sprintf("resource %q %q {\n", resourceType, resourceName) 144 | if providerAlias != "" { 145 | hcl += fmt.Sprintf(" provider = %v\n\n", providerAlias) 146 | } 147 | hcl += fmt.Sprintf(" manifest = %v\n", strings.ReplaceAll(s, "\n", "\n ")) 148 | hcl += "}\n" 149 | } 150 | if i != len(docs)-1 { 151 | hcl += "\n" 152 | } 153 | } 154 | 155 | return hcl, nil 156 | } 157 | 158 | var yamlSeparator = "\n---" 159 | 160 | // YAMLToTerraformResources takes a file containing one or more Kubernetes configs 161 | // and converts it to resources that can be used by the Terraform Kubernetes Provider 162 | // 163 | // FIXME this function has too many arguments now, use functional options instead 164 | func YAMLToTerraformResources( 165 | r io.Reader, providerAlias string, stripServerSide bool, 166 | mapOnly bool, stripKeyQuotes bool) (string, error) { 167 | hcl := "" 168 | 169 | buf := bytes.Buffer{} 170 | _, err := buf.ReadFrom(r) 171 | if err != nil { 172 | return "", err 173 | } 174 | 175 | count := 0 176 | manifest := buf.String() 177 | docs := strings.Split(manifest, yamlSeparator) 178 | for _, doc := range docs { 179 | if strings.TrimSpace(doc) == "" { 180 | // some manifests have empty documents 181 | continue 182 | } 183 | 184 | var b []byte 185 | b, err = yaml.YAMLToJSON([]byte(doc)) 186 | if err != nil { 187 | return "", err 188 | } 189 | 190 | t, err := ctyjson.ImpliedType(b) 191 | if err != nil { 192 | return "", err 193 | } 194 | 195 | doc, err := ctyjson.Unmarshal(b, t) 196 | if err != nil { 197 | return "", err 198 | } 199 | 200 | if doc.IsNull() { 201 | // skip empty YAML docs 202 | continue 203 | } 204 | 205 | if !doc.Type().IsObjectType() { 206 | return "", fmt.Errorf("the manifest must be a YAML document") 207 | } 208 | 209 | formatted, err := yamlToHCL(doc, providerAlias, stripServerSide, mapOnly, stripKeyQuotes) 210 | if err != nil { 211 | return "", fmt.Errorf("error converting YAML to HCL: %s", err) 212 | } 213 | 214 | if count > 0 { 215 | hcl += "\n" 216 | } 217 | hcl += formatted 218 | count++ 219 | } 220 | 221 | return hcl, nil 222 | } 223 | 224 | func capturePanic() { 225 | if r := recover(); r != nil { 226 | fmt.Printf( 227 | "panic: %s\n\n%s\n\n"+ 228 | "⚠️ Oh no! Looks like your manifest caused tfk8s to crash.\n\n"+ 229 | "Please open a GitHub issue and include your manifest YAML with the stack trace above,\n"+ 230 | "or ping me on slack and I'll try and fix it!\n\n"+ 231 | "GitHub: https://github.com/jrhouston/tfk8s/issues\n"+ 232 | "Slack: #terraform-providers on https://kubernetes.slack.com\n\n"+ 233 | "- Thanks, @jrhouston\n\n", 234 | r, debug.Stack()) 235 | } 236 | } 237 | 238 | func main() { 239 | defer capturePanic() 240 | 241 | infile := flag.StringP("file", "f", "-", "Input file containing Kubernetes YAML manifests") 242 | outfile := flag.StringP("output", "o", "-", "Output file to write Terraform config") 243 | providerAlias := flag.StringP("provider", "p", "", "Provider alias to populate the `provider` attribute") 244 | stripServerSide := flag.BoolP("strip", "s", false, "Strip out server side fields - use if you are piping from kubectl get") 245 | version := flag.BoolP("version", "V", false, "Show tool version") 246 | mapOnly := flag.BoolP("map-only", "M", false, "Output only an HCL map structure") 247 | stripKeyQuotes := flag.BoolP("strip-key-quotes", "Q", false, "Strip out quotes from HCL map keys unless they are required.") 248 | flag.Parse() 249 | 250 | if *version { 251 | fmt.Println(toolVersion) 252 | os.Exit(0) 253 | } 254 | 255 | var file *os.File 256 | if *infile == "-" { 257 | file = os.Stdin 258 | } else { 259 | var err error 260 | file, err = os.Open(*infile) 261 | if err != nil { 262 | fmt.Fprintf(os.Stderr, "error: %s\r\n", err.Error()) 263 | os.Exit(1) 264 | } 265 | } 266 | 267 | hcl, err := YAMLToTerraformResources( 268 | file, *providerAlias, *stripServerSide, *mapOnly, *stripKeyQuotes) 269 | if err != nil { 270 | fmt.Fprintf(os.Stderr, "error: %s\r\n", err.Error()) 271 | os.Exit(1) 272 | } 273 | 274 | if *outfile == "-" { 275 | fmt.Print(hcl) 276 | } else { 277 | err := os.WriteFile(*outfile, []byte(hcl), 0644) 278 | if err != nil { 279 | fmt.Fprintf(os.Stderr, "error: %s\r\n", err.Error()) 280 | os.Exit(1) 281 | } 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /tfk8s_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestYAMLToTerraformResourcesSingle(t *testing.T) { 11 | yaml := `--- 12 | apiVersion: v1 13 | kind: ConfigMap 14 | metadata: 15 | name: test 16 | data: 17 | TEST: test` 18 | 19 | r := strings.NewReader(yaml) 20 | output, err := YAMLToTerraformResources(r, "", false, false, false) 21 | 22 | if err != nil { 23 | t.Fatal("Converting to HCL failed:", err) 24 | } 25 | 26 | expected := ` 27 | resource "kubernetes_manifest" "configmap_test" { 28 | manifest = { 29 | "apiVersion" = "v1" 30 | "data" = { 31 | "TEST" = "test" 32 | } 33 | "kind" = "ConfigMap" 34 | "metadata" = { 35 | "name" = "test" 36 | } 37 | } 38 | }` 39 | 40 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 41 | } 42 | 43 | func TestYAMLToTerraformResourcesGenerateName(t *testing.T) { 44 | yaml := `--- 45 | apiVersion: v1 46 | kind: ConfigMap 47 | metadata: 48 | generateName: test-name- 49 | data: 50 | TEST: test` 51 | 52 | r := strings.NewReader(yaml) 53 | output, err := YAMLToTerraformResources(r, "", false, false, false) 54 | 55 | if err != nil { 56 | t.Fatal("Converting to HCL failed:", err) 57 | } 58 | 59 | expected := ` 60 | resource "kubernetes_manifest" "configmap_test_name" { 61 | manifest = { 62 | "apiVersion" = "v1" 63 | "data" = { 64 | "TEST" = "test" 65 | } 66 | "kind" = "ConfigMap" 67 | "metadata" = { 68 | "generateName" = "test-name-" 69 | } 70 | } 71 | }` 72 | 73 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 74 | } 75 | 76 | func TestYAMLToTerraformResourcesEscapeShell(t *testing.T) { 77 | yaml := `--- 78 | apiVersion: v1 79 | kind: ConfigMap 80 | metadata: 81 | name: test 82 | data: 83 | SCRIPT: | 84 | echo "Hello, ${USER} your homedir is ${HOME}" 85 | echo "\${SHELL_ESCAPE${TF_ESCAPE}}"` 86 | 87 | r := strings.NewReader(yaml) 88 | output, err := YAMLToTerraformResources(r, "", false, false, false) 89 | 90 | if err != nil { 91 | t.Fatal("Converting to HCL failed:", err) 92 | } 93 | 94 | expected := ` 95 | resource "kubernetes_manifest" "configmap_test" { 96 | manifest = { 97 | "apiVersion" = "v1" 98 | "data" = { 99 | "SCRIPT" = <<-EOT 100 | echo "Hello, $${USER} your homedir is $${HOME}" 101 | echo "\$${SHELL_ESCAPE$${TF_ESCAPE}}" 102 | EOT 103 | } 104 | "kind" = "ConfigMap" 105 | "metadata" = { 106 | "name" = "test" 107 | } 108 | } 109 | }` 110 | 111 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 112 | } 113 | 114 | func TestYAMLToTerraformResourcesMultiple(t *testing.T) { 115 | yaml := `--- 116 | apiVersion: v1 117 | kind: ConfigMap 118 | metadata: 119 | name: one 120 | data: 121 | TEST: one 122 | --- 123 | # this empty 124 | # document 125 | # should be 126 | # skipped 127 | --- 128 | apiVersion: v1 129 | kind: ConfigMap 130 | metadata: 131 | name: two 132 | data: 133 | TEST: two` 134 | 135 | r := strings.NewReader(yaml) 136 | output, err := YAMLToTerraformResources(r, "", false, false, false) 137 | 138 | if err != nil { 139 | t.Fatal("Converting to HCL failed:", err) 140 | } 141 | 142 | expected := ` 143 | resource "kubernetes_manifest" "configmap_one" { 144 | manifest = { 145 | "apiVersion" = "v1" 146 | "data" = { 147 | "TEST" = "one" 148 | } 149 | "kind" = "ConfigMap" 150 | "metadata" = { 151 | "name" = "one" 152 | } 153 | } 154 | } 155 | 156 | resource "kubernetes_manifest" "configmap_two" { 157 | manifest = { 158 | "apiVersion" = "v1" 159 | "data" = { 160 | "TEST" = "two" 161 | } 162 | "kind" = "ConfigMap" 163 | "metadata" = { 164 | "name" = "two" 165 | } 166 | } 167 | }` 168 | 169 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 170 | } 171 | 172 | func TestYAMLToTerraformResourcesList(t *testing.T) { 173 | yaml := `--- 174 | apiVersion: v1 175 | kind: ConfigMapList 176 | items: 177 | - apiVersion: v1 178 | kind: ConfigMap 179 | metadata: 180 | name: one 181 | data: 182 | TEST: one 183 | - apiVersion: v1 184 | kind: ConfigMap 185 | metadata: 186 | name: two 187 | data: 188 | TEST: two 189 | - apiVersion: v1 190 | kind: ConfigMap 191 | metadata: 192 | name: two 193 | namespace: othernamespace 194 | data: 195 | TEST: two 196 | ` 197 | 198 | r := strings.NewReader(yaml) 199 | output, err := YAMLToTerraformResources(r, "", false, false, false) 200 | 201 | if err != nil { 202 | t.Fatal("Converting to HCL failed:", err) 203 | } 204 | 205 | expected := ` 206 | resource "kubernetes_manifest" "configmap_one" { 207 | manifest = { 208 | "apiVersion" = "v1" 209 | "data" = { 210 | "TEST" = "one" 211 | } 212 | "kind" = "ConfigMap" 213 | "metadata" = { 214 | "name" = "one" 215 | } 216 | } 217 | } 218 | 219 | resource "kubernetes_manifest" "configmap_two" { 220 | manifest = { 221 | "apiVersion" = "v1" 222 | "data" = { 223 | "TEST" = "two" 224 | } 225 | "kind" = "ConfigMap" 226 | "metadata" = { 227 | "name" = "two" 228 | } 229 | } 230 | } 231 | 232 | resource "kubernetes_manifest" "configmap_othernamespace_two" { 233 | manifest = { 234 | "apiVersion" = "v1" 235 | "data" = { 236 | "TEST" = "two" 237 | } 238 | "kind" = "ConfigMap" 239 | "metadata" = { 240 | "name" = "two" 241 | "namespace" = "othernamespace" 242 | } 243 | } 244 | }` 245 | 246 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 247 | } 248 | 249 | func TestYAMLToTerraformResourcesProviderAlias(t *testing.T) { 250 | yaml := `--- 251 | apiVersion: v1 252 | kind: ConfigMap 253 | metadata: 254 | name: test 255 | data: 256 | TEST: test` 257 | 258 | r := strings.NewReader(yaml) 259 | output, err := YAMLToTerraformResources(r, "kubernetes-alpha", false, false, false) 260 | 261 | if err != nil { 262 | t.Fatal("Converting to HCL failed:", err) 263 | } 264 | 265 | expected := ` 266 | resource "kubernetes_manifest" "configmap_test" { 267 | provider = kubernetes-alpha 268 | 269 | manifest = { 270 | "apiVersion" = "v1" 271 | "data" = { 272 | "TEST" = "test" 273 | } 274 | "kind" = "ConfigMap" 275 | "metadata" = { 276 | "name" = "test" 277 | } 278 | } 279 | }` 280 | 281 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 282 | } 283 | 284 | func TestYAMLToTerraformResourcesProviderServerSideStrip(t *testing.T) { 285 | yaml := `--- 286 | apiVersion: v1 287 | data: 288 | TEST: test 289 | kind: ConfigMap 290 | metadata: 291 | annotations: 292 | kubectl.kubernetes.io/last-applied-configuration: | 293 | {"apiVersion":"v1","data":{"TEST":"prod"},"test":"ConfigMap","metadata":{"annotations":{},"name":"test","namespace":"default"}} 294 | creationTimestamp: "2020-04-30T20:34:59Z" 295 | name: test 296 | namespace: default 297 | resourceVersion: "677134" 298 | selfLink: /api/v1/namespaces/default/configmaps/test 299 | uid: bea6500b-0637-4d2d-b726-e0bda0b595dd 300 | generation: 1 301 | finalizers: 302 | - test` 303 | 304 | r := strings.NewReader(yaml) 305 | output, err := YAMLToTerraformResources(r, "", true, false, false) 306 | 307 | if err != nil { 308 | t.Fatal("Converting to HCL failed:", err) 309 | } 310 | 311 | expected := ` 312 | resource "kubernetes_manifest" "configmap_test" { 313 | manifest = { 314 | "apiVersion" = "v1" 315 | "data" = { 316 | "TEST" = "test" 317 | } 318 | "kind" = "ConfigMap" 319 | "metadata" = { 320 | "name" = "test" 321 | } 322 | } 323 | }` 324 | 325 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 326 | } 327 | 328 | func TestYAMLToTerraformResourcesMapOnly(t *testing.T) { 329 | yaml := `--- 330 | apiVersion: v1 331 | data: 332 | TEST: test 333 | kind: ConfigMap 334 | metadata: 335 | name: test 336 | namespace: default 337 | resourceVersion: "677134" 338 | selfLink: /api/v1/namespaces/default/configmaps/test 339 | uid: bea6500b-0637-4d2d-b726-e0bda0b595dd` 340 | 341 | r := strings.NewReader(yaml) 342 | output, err := YAMLToTerraformResources(r, "", true, true, false) 343 | 344 | if err != nil { 345 | t.Fatal("Converting to HCL failed:", err) 346 | } 347 | 348 | expected := `{ 349 | "apiVersion" = "v1" 350 | "data" = { 351 | "TEST" = "test" 352 | } 353 | "kind" = "ConfigMap" 354 | "metadata" = { 355 | "name" = "test" 356 | } 357 | }` 358 | 359 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 360 | } 361 | 362 | func TestYAMLToTerraformResourcesEmptyDocSkip(t *testing.T) { 363 | yaml := `--- 364 | apiVersion: v1 365 | data: 366 | TEST: test 367 | kind: ConfigMap 368 | metadata: 369 | name: test 370 | namespace: default 371 | resourceVersion: "677134" 372 | selfLink: /api/v1/namespaces/default/configmaps/test 373 | uid: bea6500b-0637-4d2d-b726-e0bda0b595dd 374 | --- 375 | 376 | --- 377 | apiVersion: v1 378 | data: 379 | TEST: test 380 | kind: ConfigMap 381 | metadata: 382 | name: test2 383 | namespace: default 384 | resourceVersion: "677134" 385 | selfLink: /api/v1/namespaces/default/configmaps/test 386 | uid: bea6500b-0637-4d2d-b726-e0bda0b595dd` 387 | 388 | r := strings.NewReader(yaml) 389 | output, err := YAMLToTerraformResources(r, "", true, false, false) 390 | 391 | if err != nil { 392 | t.Fatal("Converting to HCL failed:", err) 393 | } 394 | 395 | expected := `resource "kubernetes_manifest" "configmap_test" { 396 | manifest = { 397 | "apiVersion" = "v1" 398 | "data" = { 399 | "TEST" = "test" 400 | } 401 | "kind" = "ConfigMap" 402 | "metadata" = { 403 | "name" = "test" 404 | } 405 | } 406 | } 407 | 408 | resource "kubernetes_manifest" "configmap_test2" { 409 | manifest = { 410 | "apiVersion" = "v1" 411 | "data" = { 412 | "TEST" = "test" 413 | } 414 | "kind" = "ConfigMap" 415 | "metadata" = { 416 | "name" = "test2" 417 | } 418 | } 419 | } 420 | ` 421 | 422 | assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(output)) 423 | } 424 | --------------------------------------------------------------------------------