├── .github └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── fixtures └── lib.go ├── go.mod ├── go.sum ├── main.go ├── main.tf ├── terraform-registry-manifest.json └── test.sh /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | environment: gpg 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: 1.22.2 25 | cache: true 26 | 27 | - name: Import GPG key 28 | id: import_gpg 29 | run: | 30 | echo "${{ secrets.GPG_PRIVATE_KEY }}" | base64 --decode | gpg --import 31 | FINGERPRINT=$(gpg --list-secret-keys --keyid-format LONG | awk '/^sec/{sub(/.*\//, "", $2); print $2; exit}') 32 | echo "::set-output name=fingerprint::${FINGERPRINT}" 33 | env: 34 | GPG_TTY: /dev/ttys000 # Set the GPG_TTY to avoid issues with pinentry 35 | 36 | - name: Run GoReleaser 37 | uses: goreleaser/goreleaser-action@v5 38 | with: 39 | args: release --clean 40 | env: 41 | # GitHub sets the GITHUB_TOKEN secret automatically. 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .terraform* 3 | terraform* 4 | !terraform-registry-manifest.json 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | mod_timestamp: '{{ .CommitTimestamp }}' 5 | flags: 6 | - -trimpath 7 | ldflags: 8 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 9 | goos: 10 | - windows 11 | - linux 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | binary: '{{ .ProjectName }}_v{{ .Version }}' 17 | archives: 18 | - format: zip 19 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 20 | checksum: 21 | extra_files: 22 | - glob: 'terraform-registry-manifest.json' 23 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 24 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 25 | algorithm: sha256 26 | signs: 27 | - artifacts: checksum 28 | args: 29 | - "--batch" 30 | - "--local-user" 31 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 32 | - "--output" 33 | - "${signature}" 34 | - "--detach-sign" 35 | - "${artifact}" 36 | release: 37 | extra_files: 38 | - glob: 'terraform-registry-manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 40 | draft: true 41 | changelog: 42 | skip: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) The OpenTofu Authors 2 | 3 | Mozilla Public License, version 2.0 4 | 5 | 1. Definitions 6 | 7 | 1.1. “Contributor” 8 | 9 | means each individual or legal entity that creates, contributes to the 10 | creation of, or owns Covered Software. 11 | 12 | 1.2. “Contributor Version” 13 | 14 | means the combination of the Contributions of others (if any) used by a 15 | Contributor and that particular Contributor’s Contribution. 16 | 17 | 1.3. “Contribution” 18 | 19 | means Covered Software of a particular Contributor. 20 | 21 | 1.4. “Covered Software” 22 | 23 | means Source Code Form to which the initial Contributor has attached the 24 | notice in Exhibit A, the Executable Form of such Source Code Form, and 25 | Modifications of such Source Code Form, in each case including portions 26 | thereof. 27 | 28 | 1.5. “Incompatible With Secondary Licenses” 29 | means 30 | 31 | a. that the initial Contributor has attached the notice described in 32 | Exhibit B to the Covered Software; or 33 | 34 | b. that the Covered Software was made available under the terms of version 35 | 1.1 or earlier of the License, but not also under the terms of a 36 | Secondary License. 37 | 38 | 1.6. “Executable Form” 39 | 40 | means any form of the work other than Source Code Form. 41 | 42 | 1.7. “Larger Work” 43 | 44 | means a work that combines Covered Software with other material, in a separate 45 | file or files, that is not Covered Software. 46 | 47 | 1.8. “License” 48 | 49 | means this document. 50 | 51 | 1.9. “Licensable” 52 | 53 | means having the right to grant, to the maximum extent possible, whether at the 54 | time of the initial grant or subsequently, any and all of the rights conveyed by 55 | this License. 56 | 57 | 1.10. “Modifications” 58 | 59 | means any of the following: 60 | 61 | a. any file in Source Code Form that results from an addition to, deletion 62 | from, or modification of the contents of Covered Software; or 63 | 64 | b. any new file in Source Code Form that contains any Covered Software. 65 | 66 | 1.11. “Patent Claims” of a Contributor 67 | 68 | means any patent claim(s), including without limitation, method, process, 69 | and apparatus claims, in any patent Licensable by such Contributor that 70 | would be infringed, but for the grant of the License, by the making, 71 | using, selling, offering for sale, having made, import, or transfer of 72 | either its Contributions or its Contributor Version. 73 | 74 | 1.12. “Secondary License” 75 | 76 | means either the GNU General Public License, Version 2.0, the GNU Lesser 77 | General Public License, Version 2.1, the GNU Affero General Public 78 | License, Version 3.0, or any later versions of those licenses. 79 | 80 | 1.13. “Source Code Form” 81 | 82 | means the form of the work preferred for making modifications. 83 | 84 | 1.14. “You” (or “Your”) 85 | 86 | means an individual or a legal entity exercising rights under this 87 | License. For legal entities, “You” includes any entity that controls, is 88 | controlled by, or is under common control with You. For purposes of this 89 | definition, “control” means (a) the power, direct or indirect, to cause 90 | the direction or management of such entity, whether by contract or 91 | otherwise, or (b) ownership of more than fifty percent (50%) of the 92 | outstanding shares or beneficial ownership of such entity. 93 | 94 | 95 | 2. License Grants and Conditions 96 | 97 | 2.1. Grants 98 | 99 | Each Contributor hereby grants You a world-wide, royalty-free, 100 | non-exclusive license: 101 | 102 | a. under intellectual property rights (other than patent or trademark) 103 | Licensable by such Contributor to use, reproduce, make available, 104 | modify, display, perform, distribute, and otherwise exploit its 105 | Contributions, either on an unmodified basis, with Modifications, or as 106 | part of a Larger Work; and 107 | 108 | b. under Patent Claims of such Contributor to make, use, sell, offer for 109 | sale, have made, import, and otherwise transfer either its Contributions 110 | or its Contributor Version. 111 | 112 | 2.2. Effective Date 113 | 114 | The licenses granted in Section 2.1 with respect to any Contribution become 115 | effective for each Contribution on the date the Contributor first distributes 116 | such Contribution. 117 | 118 | 2.3. Limitations on Grant Scope 119 | 120 | The licenses granted in this Section 2 are the only rights granted under this 121 | License. No additional rights or licenses will be implied from the distribution 122 | or licensing of Covered Software under this License. Notwithstanding Section 123 | 2.1(b) above, no patent license is granted by a Contributor: 124 | 125 | a. for any code that a Contributor has removed from Covered Software; or 126 | 127 | b. for infringements caused by: (i) Your and any other third party’s 128 | modifications of Covered Software, or (ii) the combination of its 129 | Contributions with other software (except as part of its Contributor 130 | Version); or 131 | 132 | c. under Patent Claims infringed by Covered Software in the absence of its 133 | Contributions. 134 | 135 | This License does not grant any rights in the trademarks, service marks, or 136 | logos of any Contributor (except as may be necessary to comply with the 137 | notice requirements in Section 3.4). 138 | 139 | 2.4. Subsequent Licenses 140 | 141 | No Contributor makes additional grants as a result of Your choice to 142 | distribute the Covered Software under a subsequent version of this License 143 | (see Section 10.2) or under the terms of a Secondary License (if permitted 144 | under the terms of Section 3.3). 145 | 146 | 2.5. Representation 147 | 148 | Each Contributor represents that the Contributor believes its Contributions 149 | are its original creation(s) or it has sufficient rights to grant the 150 | rights to its Contributions conveyed by this License. 151 | 152 | 2.6. Fair Use 153 | 154 | This License is not intended to limit any rights You have under applicable 155 | copyright doctrines of fair use, fair dealing, or other equivalents. 156 | 157 | 2.7. Conditions 158 | 159 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in 160 | Section 2.1. 161 | 162 | 163 | 3. Responsibilities 164 | 165 | 3.1. Distribution of Source Form 166 | 167 | All distribution of Covered Software in Source Code Form, including any 168 | Modifications that You create or to which You contribute, must be under the 169 | terms of this License. You must inform recipients that the Source Code Form 170 | of the Covered Software is governed by the terms of this License, and how 171 | they can obtain a copy of this License. You may not attempt to alter or 172 | restrict the recipients’ rights in the Source Code Form. 173 | 174 | 3.2. Distribution of Executable Form 175 | 176 | If You distribute Covered Software in Executable Form then: 177 | 178 | a. such Covered Software must also be made available in Source Code Form, 179 | as described in Section 3.1, and You must inform recipients of the 180 | Executable Form how they can obtain a copy of such Source Code Form by 181 | reasonable means in a timely manner, at a charge no more than the cost 182 | of distribution to the recipient; and 183 | 184 | b. You may distribute such Executable Form under the terms of this License, 185 | or sublicense it under different terms, provided that the license for 186 | the Executable Form does not attempt to limit or alter the recipients’ 187 | rights in the Source Code Form under this License. 188 | 189 | 3.3. Distribution of a Larger Work 190 | 191 | You may create and distribute a Larger Work under terms of Your choice, 192 | provided that You also comply with the requirements of this License for the 193 | Covered Software. If the Larger Work is a combination of Covered Software 194 | with a work governed by one or more Secondary Licenses, and the Covered 195 | Software is not Incompatible With Secondary Licenses, this License permits 196 | You to additionally distribute such Covered Software under the terms of 197 | such Secondary License(s), so that the recipient of the Larger Work may, at 198 | their option, further distribute the Covered Software under the terms of 199 | either this License or such Secondary License(s). 200 | 201 | 3.4. Notices 202 | 203 | You may not remove or alter the substance of any license notices (including 204 | copyright notices, patent notices, disclaimers of warranty, or limitations 205 | of liability) contained within the Source Code Form of the Covered 206 | Software, except that You may alter any license notices to the extent 207 | required to remedy known factual inaccuracies. 208 | 209 | 3.5. Application of Additional Terms 210 | 211 | You may choose to offer, and to charge a fee for, warranty, support, 212 | indemnity or liability obligations to one or more recipients of Covered 213 | Software. However, You may do so only on Your own behalf, and not on behalf 214 | of any Contributor. You must make it absolutely clear that any such 215 | warranty, support, indemnity, or liability obligation is offered by You 216 | alone, and You hereby agree to indemnify every Contributor for any 217 | liability incurred by such Contributor as a result of warranty, support, 218 | indemnity or liability terms You offer. You may include additional 219 | disclaimers of warranty and limitations of liability specific to any 220 | jurisdiction. 221 | 222 | 4. Inability to Comply Due to Statute or Regulation 223 | 224 | If it is impossible for You to comply with any of the terms of this License 225 | with respect to some or all of the Covered Software due to statute, judicial 226 | order, or regulation then You must: (a) comply with the terms of this License 227 | to the maximum extent possible; and (b) describe the limitations and the code 228 | they affect. Such description must be placed in a text file included with all 229 | distributions of the Covered Software under this License. Except to the 230 | extent prohibited by statute or regulation, such description must be 231 | sufficiently detailed for a recipient of ordinary skill to be able to 232 | understand it. 233 | 234 | 5. Termination 235 | 236 | 5.1. The rights granted under this License will terminate automatically if You 237 | fail to comply with any of its terms. However, if You become compliant, 238 | then the rights granted under this License from a particular Contributor 239 | are reinstated (a) provisionally, unless and until such Contributor 240 | explicitly and finally terminates Your grants, and (b) on an ongoing basis, 241 | if such Contributor fails to notify You of the non-compliance by some 242 | reasonable means prior to 60 days after You have come back into compliance. 243 | Moreover, Your grants from a particular Contributor are reinstated on an 244 | ongoing basis if such Contributor notifies You of the non-compliance by 245 | some reasonable means, this is the first time You have received notice of 246 | non-compliance with this License from such Contributor, and You become 247 | compliant prior to 30 days after Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, counter-claims, 251 | and cross-claims) alleging that a Contributor Version directly or 252 | indirectly infringes any patent, then the rights granted to You by any and 253 | all Contributors for the Covered Software under Section 2.1 of this License 254 | shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user 257 | license agreements (excluding distributors and resellers) which have been 258 | validly granted by You or Your distributors under this License prior to 259 | termination shall survive termination. 260 | 261 | 6. Disclaimer of Warranty 262 | 263 | Covered Software is provided under this License on an “as is” basis, without 264 | warranty of any kind, either expressed, implied, or statutory, including, 265 | without limitation, warranties that the Covered Software is free of defects, 266 | merchantable, fit for a particular purpose or non-infringing. The entire 267 | risk as to the quality and performance of the Covered Software is with You. 268 | Should any Covered Software prove defective in any respect, You (not any 269 | Contributor) assume the cost of any necessary servicing, repair, or 270 | correction. This disclaimer of warranty constitutes an essential part of this 271 | License. No use of any Covered Software is authorized under this License 272 | except under this disclaimer. 273 | 274 | 7. Limitation of Liability 275 | 276 | Under no circumstances and under no legal theory, whether tort (including 277 | negligence), contract, or otherwise, shall any Contributor, or anyone who 278 | distributes Covered Software as permitted above, be liable to You for any 279 | direct, indirect, special, incidental, or consequential damages of any 280 | character including, without limitation, damages for lost profits, loss of 281 | goodwill, work stoppage, computer failure or malfunction, or any and all 282 | other commercial damages or losses, even if such party shall have been 283 | informed of the possibility of such damages. This limitation of liability 284 | shall not apply to liability for death or personal injury resulting from such 285 | party’s negligence to the extent applicable law prohibits such limitation. 286 | Some jurisdictions do not allow the exclusion or limitation of incidental or 287 | consequential damages, so this exclusion and limitation may not apply to You. 288 | 289 | 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the courts of 292 | a jurisdiction where the defendant maintains its principal place of business 293 | and such litigation shall be governed by laws of that jurisdiction, without 294 | reference to its conflict-of-law provisions. Nothing in this Section shall 295 | prevent a party’s ability to bring cross-claims or counter-claims. 296 | 297 | 9. Miscellaneous 298 | 299 | This License represents the complete agreement concerning the subject matter 300 | hereof. If any provision of this License is held to be unenforceable, such 301 | provision shall be reformed only to the extent necessary to make it 302 | enforceable. Any law or regulation which provides that the language of a 303 | contract shall be construed against the drafter shall not be used to construe 304 | this License against a Contributor. 305 | 306 | 307 | 10. Versions of the License 308 | 309 | 10.1. New Versions 310 | 311 | Mozilla Foundation is the license steward. Except as provided in Section 312 | 10.3, no one other than the license steward has the right to modify or 313 | publish new versions of this License. Each version will be given a 314 | distinguishing version number. 315 | 316 | 10.2. Effect of New Versions 317 | 318 | You may distribute the Covered Software under the terms of the version of 319 | the License under which You originally received the Covered Software, or 320 | under the terms of any subsequent version published by the license 321 | steward. 322 | 323 | 10.3. Modified Versions 324 | 325 | If you create software not governed by this License, and you want to 326 | create a new license for such software, you may create and use a modified 327 | version of this License if you rename the license and remove any 328 | references to the name of the license steward (except to note that such 329 | modified license differs from this License). 330 | 331 | 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 332 | If You choose to distribute Source Code Form that is Incompatible With 333 | Secondary Licenses under the terms of this version of the License, the 334 | notice described in Exhibit B of this License must be attached. 335 | 336 | Exhibit A - Source Code Form License Notice 337 | 338 | This Source Code Form is subject to the 339 | terms of the Mozilla Public License, v. 340 | 2.0. If a copy of the MPL was not 341 | distributed with this file, You can 342 | obtain one at 343 | http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular file, then 346 | You may include the notice in a location (such as a LICENSE file in a relevant 347 | directory) where a recipient would be likely to look for such a notice. 348 | 349 | You may add additional accurate notices of copyright ownership. 350 | 351 | Exhibit B - “Incompatible With Secondary Licenses” Notice 352 | 353 | This Source Code Form is “Incompatible 354 | With Secondary Licenses”, as defined by 355 | the Mozilla Public License, v. 2.0. 356 | 357 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-provider-go 2 | 3 | This is an experimental OpenTofu function provider based on terraform-plugin-go. 4 | 5 | It allows you to write Go helper functions next to your Tofu code, so that you can use them in your Tofu configuration, in a completely type-safe way. The provider is based on [Yaegi](https://github.com/traefik/yaegi), and most of the Go standard library is available. 6 | 7 | In OpenTofu 1.7.0-beta1 and upwards you can configure the provider and pass it a Go file to load. 8 | - The package name should be `lib` 9 | - Exported functions need to start with upper-case letters. 10 | - The Tofu-facing name of the function **will be lower-cased**. 11 | - It supports simple types, like strings, integers, floats, and booleans. 12 | - It also supports complex type, like maps, slices, nullable pointers, and structures. 13 | 14 | This feature is an experimental preview and is subject to change before the OpenTofu 1.7.0 release. 15 | 16 | ## Example 17 | 18 | ```hcl 19 | // main.tf 20 | provider "go" { 21 | go = file("./lib.go") 22 | } 23 | 24 | output "test" { 25 | value = provider::go::hello("papaya") 26 | } 27 | ``` 28 | ```go 29 | // lib.go 30 | package lib 31 | 32 | func Hello(name string) string { 33 | return "Hello, " + name + "!" 34 | } 35 | ``` 36 | Output excerpt: 37 | ``` 38 | Changes to Outputs: 39 | + test = "Hello, papaya!" 40 | ``` 41 | 42 | ## More involved example 43 | 44 | ```hcl 45 | // main.tf 46 | provider "go" { 47 | go = file("./lib.go") 48 | } 49 | 50 | output "test" { 51 | value = provider::go::hello({ 52 | name = "papaya", 53 | surname = "bacon", 54 | }) 55 | } 56 | ``` 57 | ```go 58 | // lib.go 59 | package lib 60 | 61 | import ( 62 | "fmt" 63 | ) 64 | 65 | type Person struct { 66 | // We can let it default to the un-capitalized field name. 67 | Name string 68 | // Or use a struct tag to specify the object field name explicitly. 69 | Surname string `tf:"surname"` 70 | } 71 | 72 | func Hello(person Person) string { 73 | return fmt.Sprintf("Hello, %s %s!", person.Name, person.Surname) 74 | } 75 | ``` 76 | Output excerpt: 77 | ``` 78 | Changes to Outputs: 79 | + test = "Hello, papaya bacon!" 80 | ``` 81 | 82 | Moreover, all of this is type-safe and mistakes will be caught by tofu. So passing a number to the function will fail with `object required`, while forgetting e.g. the surname will fail with `attribute "surname" is required`. 83 | 84 | ## Importing 85 | Here's a snippet to require the provider in your OpenTofu configuration: 86 | ```hcl 87 | terraform { 88 | required_providers { 89 | go = { 90 | source = "registry.opentofu.org/opentofu/go" 91 | version = "0.0.1" 92 | } 93 | } 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /fixtures/lib.go: -------------------------------------------------------------------------------- 1 | package lib 2 | 3 | func Hello(name string) string { 4 | return "Hello, " + name + "!" 5 | } 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/opentofu/terraform-provider-go 2 | 3 | go 1.22.2 4 | 5 | require ( 6 | github.com/Shopify/go-lua v0.0.0-20240312125312-5d657e363856 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/hashicorp/terraform-plugin-go v0.22.1 9 | github.com/traefik/yaegi v0.16.1 10 | github.com/zclconf/go-cty v1.13.1 11 | ) 12 | 13 | require ( 14 | github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect 15 | github.com/fatih/color v1.13.0 // indirect 16 | github.com/golang/protobuf v1.5.3 // indirect 17 | github.com/hashicorp/go-hclog v1.5.0 // indirect 18 | github.com/hashicorp/go-plugin v1.6.0 // indirect 19 | github.com/hashicorp/go-uuid v1.0.3 // indirect 20 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 21 | github.com/hashicorp/terraform-registry-address v0.2.3 // indirect 22 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 23 | github.com/hashicorp/yamux v0.1.1 // indirect 24 | github.com/mattn/go-colorable v0.1.12 // indirect 25 | github.com/mattn/go-isatty v0.0.14 // indirect 26 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 27 | github.com/oklog/run v1.0.0 // indirect 28 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 29 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 30 | golang.org/x/net v0.20.0 // indirect 31 | golang.org/x/sys v0.16.0 // indirect 32 | golang.org/x/text v0.14.0 // indirect 33 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect 34 | google.golang.org/grpc v1.62.1 // indirect 35 | google.golang.org/protobuf v1.33.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Shopify/go-lua v0.0.0-20240312125312-5d657e363856 h1:N32EXb3LZ0FJwbdoMokLXysTHgWHccuYUPxmVqPowkw= 2 | github.com/Shopify/go-lua v0.0.0-20240312125312-5d657e363856/go.mod h1:M4CxjVc/1Nwka5atBv7G/sb7Ac2BDe3+FxbiT9iVNIQ= 3 | github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 4 | github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 5 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 6 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 11 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 12 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 13 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 14 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 15 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 17 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 18 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 19 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 20 | github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= 21 | github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= 22 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 23 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 24 | github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w= 25 | github.com/hashicorp/terraform-plugin-go v0.22.1/go.mod h1:qrjnqRghvQ6KnDbB12XeZ4FluclYwptntoWCr9QaXTI= 26 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 27 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 28 | github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= 29 | github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= 30 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= 31 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= 32 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 33 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 34 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 35 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 36 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 37 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 38 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 39 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 40 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 41 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 42 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 43 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 44 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 45 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 46 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 49 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 50 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 51 | github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E= 52 | github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY= 53 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 54 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 55 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 56 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 57 | github.com/zclconf/go-cty v1.13.1 h1:0a6bRwuiSHtAmqCqNOE+c2oHgepv0ctoxU4FUe43kwc= 58 | github.com/zclconf/go-cty v1.13.1/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= 59 | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= 60 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 61 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 67 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 68 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 69 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 70 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= 72 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= 73 | google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= 74 | google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= 75 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 76 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 77 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 78 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 79 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 80 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 81 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 82 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "math/big" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/hashicorp/terraform-plugin-go/tfprotov6" 12 | "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server" 13 | "github.com/hashicorp/terraform-plugin-go/tftypes" 14 | "github.com/traefik/yaegi/interp" 15 | "github.com/traefik/yaegi/stdlib" 16 | ) 17 | 18 | type Function struct { 19 | tfprotov6.Function 20 | Impl func(args []*tfprotov6.DynamicValue) (*tfprotov6.DynamicValue, *tfprotov6.FunctionError) 21 | } 22 | 23 | type FunctionProvider struct { 24 | ProviderSchema *tfprotov6.Schema 25 | StaticFunctions map[string]*Function 26 | dynamicFunctions map[string]*Function 27 | Configure func(*tfprotov6.DynamicValue) (map[string]*Function, []*tfprotov6.Diagnostic) 28 | } 29 | 30 | func (f *FunctionProvider) GetMetadata(context.Context, *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) { 31 | var functions []tfprotov6.FunctionMetadata 32 | for name := range f.StaticFunctions { 33 | functions = append(functions, tfprotov6.FunctionMetadata{Name: name}) 34 | } 35 | 36 | return &tfprotov6.GetMetadataResponse{ 37 | ServerCapabilities: &tfprotov6.ServerCapabilities{GetProviderSchemaOptional: true}, 38 | Functions: functions, 39 | }, nil 40 | } 41 | func (f *FunctionProvider) GetProviderSchema(context.Context, *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) { 42 | functions := make(map[string]*tfprotov6.Function) 43 | for name, fn := range f.StaticFunctions { 44 | functions[name] = &fn.Function 45 | } 46 | 47 | return &tfprotov6.GetProviderSchemaResponse{ 48 | ServerCapabilities: &tfprotov6.ServerCapabilities{GetProviderSchemaOptional: true}, 49 | Provider: f.ProviderSchema, 50 | Functions: functions, 51 | }, nil 52 | } 53 | func (f *FunctionProvider) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) { 54 | // Passthrough 55 | return &tfprotov6.ValidateProviderConfigResponse{PreparedConfig: req.Config}, nil 56 | } 57 | func (f *FunctionProvider) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) { 58 | funcs, diags := f.Configure(req.Config) 59 | f.dynamicFunctions = funcs 60 | return &tfprotov6.ConfigureProviderResponse{ 61 | Diagnostics: diags, 62 | }, nil 63 | } 64 | func (f *FunctionProvider) StopProvider(context.Context, *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) { 65 | return &tfprotov6.StopProviderResponse{}, nil 66 | } 67 | func (f *FunctionProvider) ValidateResourceConfig(context.Context, *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) { 68 | return nil, errors.New("not supported") 69 | } 70 | func (f *FunctionProvider) UpgradeResourceState(context.Context, *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) { 71 | return nil, errors.New("not supported") 72 | } 73 | func (f *FunctionProvider) ReadResource(context.Context, *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) { 74 | return nil, errors.New("not supported") 75 | } 76 | func (f *FunctionProvider) PlanResourceChange(context.Context, *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) { 77 | return nil, errors.New("not supported") 78 | } 79 | func (f *FunctionProvider) ApplyResourceChange(context.Context, *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) { 80 | return nil, errors.New("not supported") 81 | } 82 | func (f *FunctionProvider) ImportResourceState(context.Context, *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) { 83 | return nil, errors.New("not supported") 84 | } 85 | func (f *FunctionProvider) ValidateDataResourceConfig(context.Context, *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) { 86 | return nil, errors.New("not supported") 87 | } 88 | func (f *FunctionProvider) ReadDataSource(context.Context, *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) { 89 | return nil, errors.New("not supported") 90 | } 91 | func (f *FunctionProvider) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) { 92 | if fn, ok := f.StaticFunctions[req.Name]; ok { 93 | ret, err := fn.Impl(req.Arguments) 94 | return &tfprotov6.CallFunctionResponse{ 95 | Result: ret, 96 | Error: err, 97 | }, nil 98 | } 99 | if f.dynamicFunctions != nil { 100 | if fn, ok := f.dynamicFunctions[req.Name]; ok { 101 | ret, err := fn.Impl(req.Arguments) 102 | return &tfprotov6.CallFunctionResponse{ 103 | Result: ret, 104 | Error: err, 105 | }, nil 106 | } 107 | } 108 | return nil, errors.New("unknown function " + req.Name) 109 | } 110 | func (f *FunctionProvider) GetFunctions(context.Context, *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) { 111 | functions := make(map[string]*tfprotov6.Function) 112 | for name, fn := range f.StaticFunctions { 113 | functions[name] = &fn.Function 114 | } 115 | for name, fn := range f.dynamicFunctions { 116 | functions[name] = &fn.Function 117 | } 118 | 119 | return &tfprotov6.GetFunctionsResponse{ 120 | Functions: functions, 121 | }, nil 122 | } 123 | 124 | func main() { 125 | err := tf6server.Serve("registry.opentofu.org/opentofu/go", func() tfprotov6.ProviderServer { 126 | provider := &FunctionProvider{ 127 | ProviderSchema: &tfprotov6.Schema{ 128 | Block: &tfprotov6.SchemaBlock{ 129 | Attributes: []*tfprotov6.SchemaAttribute{ 130 | &tfprotov6.SchemaAttribute{ 131 | Name: "go", 132 | Type: tftypes.String, 133 | Required: true, 134 | }, 135 | }, 136 | }, 137 | }, 138 | Configure: func(config *tfprotov6.DynamicValue) (map[string]*Function, []*tfprotov6.Diagnostic) { 139 | res, err := config.Unmarshal(tftypes.Map{ElementType: tftypes.String}) 140 | if err != nil { 141 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 142 | Severity: tfprotov6.DiagnosticSeverityError, 143 | Summary: "Invalid configure payload", 144 | Detail: err.Error(), 145 | }} 146 | } 147 | cfg := make(map[string]tftypes.Value) 148 | err = res.As(&cfg) 149 | if err != nil { 150 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 151 | Severity: tfprotov6.DiagnosticSeverityError, 152 | Summary: "Invalid configure payload", 153 | Detail: err.Error(), 154 | }} 155 | } 156 | 157 | codeVal := cfg["go"] 158 | var code string 159 | err = codeVal.As(&code) 160 | if err != nil { 161 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 162 | Severity: tfprotov6.DiagnosticSeverityError, 163 | Summary: "Invalid configure payload", 164 | Detail: err.Error(), 165 | }} 166 | } 167 | 168 | interpreter := interp.New(interp.Options{}) 169 | if err := interpreter.Use(stdlib.Symbols); err != nil { 170 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 171 | Severity: tfprotov6.DiagnosticSeverityError, 172 | Summary: "Failed to load Go standard library", 173 | Detail: err.Error(), 174 | }} 175 | } 176 | 177 | _, err = interpreter.Eval(code) 178 | if err != nil { 179 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 180 | Severity: tfprotov6.DiagnosticSeverityError, 181 | Summary: "Failed to evaluate Go code", 182 | Detail: err.Error(), 183 | }} 184 | } 185 | 186 | exports := interpreter.Symbols("lib") 187 | libExports := exports["lib"] 188 | 189 | functions := map[string]*Function{} 190 | for name, export := range libExports { 191 | if export.Kind() != reflect.Func { 192 | continue 193 | } 194 | fn, diags := GoFunctionToTFFunction(interpreter, export) 195 | if len(diags) > 0 { 196 | return nil, diags 197 | } 198 | functions[GoNameToTFName(name)] = fn 199 | } 200 | 201 | return functions, nil 202 | }, 203 | StaticFunctions: map[string]*Function{}, 204 | } 205 | return provider 206 | }) 207 | if err != nil { 208 | panic(err) 209 | } 210 | } 211 | 212 | func GoFunctionToTFFunction(interpreter *interp.Interpreter, fn reflect.Value) (*Function, []*tfprotov6.Diagnostic) { 213 | exportType := fn.Type() 214 | var parameters []*tfprotov6.FunctionParameter 215 | for i := 0; i < exportType.NumIn(); i++ { 216 | functionParameter, err := GoTypeToTFFunctionParam(exportType.In(i)) 217 | if err != nil { 218 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 219 | Severity: tfprotov6.DiagnosticSeverityError, 220 | Summary: "Failed to convert Argument type to TF type", 221 | Detail: fmt.Errorf("argument %d: %w", i, err).Error(), 222 | }} 223 | } 224 | parameters = append(parameters, functionParameter) 225 | } 226 | if exportType.NumOut() == 0 { 227 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 228 | Severity: tfprotov6.DiagnosticSeverityError, 229 | Summary: "Function must return a value", 230 | }} 231 | } 232 | if exportType.NumOut() > 2 { 233 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 234 | Severity: tfprotov6.DiagnosticSeverityError, 235 | Summary: "Function must return at most two values", 236 | }} 237 | } 238 | if exportType.NumOut() == 2 && exportType.Out(1) != reflect.TypeFor[error]() { 239 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 240 | Severity: tfprotov6.DiagnosticSeverityError, 241 | Summary: "Second return value, if exists, must be an error", 242 | }} 243 | } 244 | output := exportType.Out(0) 245 | outputType, err := GoTypeToTFType(output) 246 | if err != nil { 247 | return nil, []*tfprotov6.Diagnostic{&tfprotov6.Diagnostic{ 248 | Severity: tfprotov6.DiagnosticSeverityError, 249 | Summary: "Failed to convert Function output type to TF type", 250 | Detail: err.Error(), 251 | }} 252 | } 253 | return &Function{ 254 | Function: tfprotov6.Function{ 255 | Parameters: parameters, 256 | Return: &tfprotov6.FunctionReturn{ 257 | Type: outputType, 258 | }, 259 | }, 260 | Impl: func(args []*tfprotov6.DynamicValue) (*tfprotov6.DynamicValue, *tfprotov6.FunctionError) { 261 | goArgs := make([]reflect.Value, len(args)) 262 | for i, arg := range args { 263 | var err error 264 | goArg, err := ProtoToGo(parameters[i].Type, exportType.In(i), arg) 265 | if err != nil { 266 | return nil, &tfprotov6.FunctionError{ 267 | Text: err.Error(), 268 | } 269 | } 270 | goArgs[i] = reflect.ValueOf(goArg) 271 | } 272 | goResult := fn.Call(goArgs) 273 | if len(goResult) > 1 && !goResult[1].IsNil() { 274 | err := goResult[1].Interface().(error) 275 | if err != nil { 276 | return nil, &tfprotov6.FunctionError{ 277 | Text: err.Error(), 278 | } 279 | } 280 | } 281 | 282 | out, err := GoToProto(outputType, goResult[0].Interface()) 283 | if err != nil { 284 | return nil, &tfprotov6.FunctionError{ 285 | Text: err.Error(), 286 | } 287 | } 288 | return out, nil 289 | }, 290 | }, nil 291 | } 292 | 293 | func TfValueToProto(tfType tftypes.Type, tfVal tftypes.Value) (*tfprotov6.DynamicValue, error) { 294 | value, err := tfprotov6.NewDynamicValue(tfType, tfVal) 295 | return &value, err 296 | } 297 | 298 | func GoTypeToTFFunctionParam(t reflect.Type) (*tfprotov6.FunctionParameter, error) { 299 | outType, err := GoTypeToTFType(t) 300 | if err != nil { 301 | return nil, err 302 | } 303 | 304 | return &tfprotov6.FunctionParameter{ 305 | AllowUnknownValues: false, 306 | AllowNullValue: t.Kind() == reflect.Ptr, 307 | Type: outType, 308 | }, nil 309 | } 310 | 311 | func GoTypeToTFType(t reflect.Type) (tftypes.Type, error) { 312 | switch t.Kind() { 313 | case reflect.String: 314 | return tftypes.String, nil 315 | case reflect.Bool: 316 | return tftypes.Bool, nil 317 | case reflect.Int, reflect.Float64: 318 | return tftypes.Number, nil 319 | case reflect.Ptr: 320 | return GoTypeToTFType(t.Elem()) 321 | case reflect.Interface: 322 | if reflect.TypeFor[interface{}]().Implements(t) { 323 | return tftypes.DynamicPseudoType, nil 324 | } else { 325 | return nil, fmt.Errorf("unsupported interface type %s, only interface{}/any interface type is supported", t.String()) 326 | } 327 | case reflect.Slice: 328 | elementType, err := GoTypeToTFType(t.Elem()) 329 | if err != nil { 330 | return nil, err 331 | } 332 | return tftypes.List{ 333 | ElementType: elementType, 334 | }, nil 335 | case reflect.Map: 336 | if t.Key().Kind() != reflect.String { 337 | return nil, fmt.Errorf("unsupported map key type %s, only string keys are supported", t.Key().String()) 338 | } 339 | valueType, err := GoTypeToTFType(t.Elem()) 340 | if err != nil { 341 | return nil, err 342 | } 343 | return tftypes.Map{ 344 | ElementType: valueType, 345 | }, nil 346 | case reflect.Struct: 347 | attributeTypes := make(map[string]tftypes.Type) 348 | for i := 0; i < t.NumField(); i++ { 349 | field := t.Field(i) 350 | fieldType, err := GoTypeToTFType(field.Type) 351 | if err != nil { 352 | return nil, err 353 | } 354 | attributeTypes[getTfObjectGoFieldName(field)] = fieldType 355 | } 356 | return tftypes.Object{ 357 | AttributeTypes: attributeTypes, 358 | }, nil 359 | default: 360 | return nil, fmt.Errorf("unsupported type %s", t.String()) 361 | } 362 | } 363 | 364 | func getTfObjectGoFieldName(field reflect.StructField) string { 365 | if tag := field.Tag.Get("tf"); tag != "" { 366 | return tag 367 | } 368 | return uncapitalize(field.Name) 369 | } 370 | 371 | func uncapitalize(s string) string { 372 | if len(s) == 1 { 373 | return strings.ToLower(s) 374 | } 375 | return strings.ToLower(s[:1]) + s[1:] 376 | } 377 | 378 | func GoNameToTFName(name string) string { 379 | return strings.ToLower(name) 380 | } 381 | 382 | func ProtoToGo(argumentTfType tftypes.Type, argumentGoType reflect.Type, arg *tfprotov6.DynamicValue) (any, error) { 383 | if len(arg.JSON) == 0 && len(arg.MsgPack) == 0 { 384 | // This is an edge-case not properly handled by arg.IsNull(). 385 | // It happens when you pass (from tf) the value `null`, to a function expecting e.g. a string pointer. 386 | 387 | // We can't just return nil here, because we need a *typed* interface{} :) 388 | // If we'd return nil here, then the later reflect call of our dynamically created functions 389 | // would fail during the dynamic type-check. 390 | return reflect.Zero(argumentGoType).Interface(), nil 391 | } 392 | argTf, err := arg.Unmarshal(argumentTfType) 393 | if err != nil { 394 | return nil, err 395 | } 396 | 397 | return TfToGoValue(argumentGoType, argTf) 398 | } 399 | 400 | func TfToGoValue(goType reflect.Type, tfValue tftypes.Value) (any, error) { 401 | if tfValue.IsNull() { 402 | return nil, nil 403 | } 404 | 405 | switch goType.Kind() { 406 | case reflect.String: 407 | var str string 408 | if err := tfValue.As(&str); err != nil { 409 | return nil, err 410 | } 411 | return str, nil 412 | case reflect.Bool: 413 | var b bool 414 | if err := tfValue.As(&b); err != nil { 415 | return nil, err 416 | } 417 | return b, nil 418 | case reflect.Int: 419 | var bigFloat big.Float 420 | if err := tfValue.As(&bigFloat); err != nil { 421 | return nil, err 422 | } 423 | 424 | f, _ := bigFloat.Int64() 425 | return int(f), nil 426 | case reflect.Float64: 427 | var bigFloat big.Float 428 | if err := tfValue.As(&bigFloat); err != nil { 429 | return nil, err 430 | } 431 | 432 | f, _ := bigFloat.Float64() 433 | return f, nil 434 | case reflect.Ptr: 435 | if tfValue.IsNull() { 436 | return nil, nil 437 | } 438 | value, err := TfToGoValue(goType.Elem(), tfValue) 439 | if err != nil { 440 | return nil, err 441 | } 442 | // If we return &value, then the type will be *interface{}. 443 | // So we construct a concrete type pointer via reflect. 444 | // This way, we get e.g. *string instead of *interface{}. 445 | out := reflect.New(reflect.TypeOf(value)) 446 | out.Elem().Set(reflect.ValueOf(value)) 447 | return out.Interface(), nil 448 | case reflect.Interface: 449 | panic("implement interface{}") 450 | case reflect.Slice: 451 | var tfValues []tftypes.Value 452 | if err := tfValue.As(&tfValues); err != nil { 453 | return nil, err 454 | } 455 | 456 | out := reflect.MakeSlice(goType, len(tfValues), len(tfValues)) 457 | for i := 0; i < len(tfValues); i++ { 458 | elem, err := TfToGoValue(goType.Elem(), tfValues[i]) 459 | if err != nil { 460 | return nil, err 461 | } 462 | out.Index(i).Set(reflect.ValueOf(elem)) 463 | } 464 | return out.Interface(), nil 465 | case reflect.Map: 466 | var tfMap map[string]tftypes.Value 467 | if err := tfValue.As(&tfMap); err != nil { 468 | return nil, err 469 | } 470 | out := reflect.MakeMap(goType) 471 | for key, tfElement := range tfMap { 472 | elem, err := TfToGoValue(goType.Elem(), tfElement) 473 | if err != nil { 474 | return nil, err 475 | } 476 | out.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(elem)) 477 | } 478 | return out.Interface(), nil 479 | case reflect.Struct: 480 | var tfMap map[string]tftypes.Value 481 | if err := tfValue.As(&tfMap); err != nil { 482 | return nil, err 483 | } 484 | // This is a fun one, you'd fine reflect.Zero(goType) should do the same, right? 485 | // Nope! If you use reflect.Zero, then the fields of it won't be addressable. 486 | // If the fields aren't addressable, they're not settable. 487 | // So, we use reflect.New and then take the pointed-to value, this way it is in fact addressable. 488 | out := reflect.New(goType).Elem() 489 | for i := 0; i < goType.NumField(); i++ { 490 | field := goType.Field(i) 491 | tfName := getTfObjectGoFieldName(field) 492 | tfElement, ok := tfMap[tfName] 493 | if !ok { 494 | return nil, fmt.Errorf("missing object field %s", tfName) 495 | } 496 | elem, err := TfToGoValue(field.Type, tfElement) 497 | if err != nil { 498 | return nil, err 499 | } 500 | out.Field(i).Set(reflect.ValueOf(elem)) 501 | } 502 | return out.Interface(), nil 503 | 504 | default: 505 | return nil, fmt.Errorf("unsupported type %s", goType.String()) 506 | } 507 | } 508 | 509 | // func CtyToGo(goType reflect.Type, ctyValue cty.Value) (any, error) { 510 | // ctyType := ctyValue.Type() 511 | // switch goType.Kind() { 512 | // case reflect.String: 513 | // if ctyType != cty.String { 514 | // return nil, fmt.Errorf("expected string, got %s", ctyType.FriendlyName()) 515 | // } 516 | // return ctyValue.AsString(), nil 517 | // case reflect.Bool: 518 | // if ctyType != cty.Bool { 519 | // return nil, fmt.Errorf("expected bool, got %s", ctyType.FriendlyName()) 520 | // } 521 | // return ctyValue.True(), nil 522 | // case reflect.Int: 523 | // if ctyType != cty.Number { 524 | // return nil, fmt.Errorf("expected number, got %s", ctyType.FriendlyName()) 525 | // } 526 | // f, _ := ctyValue.AsBigFloat().Int64() 527 | // return int(f), nil 528 | // case reflect.Float64: 529 | // if ctyType != cty.Number { 530 | // return nil, fmt.Errorf("expected number, got %s", ctyType.FriendlyName()) 531 | // } 532 | // f, _ := ctyValue.AsBigFloat().Float64() 533 | // return f, nil 534 | // case reflect.Ptr: 535 | // if ctyValue.IsNull() { 536 | // return nil, nil 537 | // } 538 | // return CtyToGo(goType.Elem(), ctyValue) 539 | // case reflect.Interface: 540 | // panic("implement interface{}") 541 | // case reflect.Slice: 542 | // if !ctyType.IsListType() { 543 | // return nil, fmt.Errorf("expected list, got %s", ctyType.FriendlyName()) 544 | // } 545 | // out := reflect.MakeSlice(goType, ctyValue.LengthInt(), ctyValue.LengthInt()) 546 | // for i := 0; i < ctyValue.LengthInt(); i++ { 547 | // elem, err := CtyToGo(goType.Elem(), ctyValue.Index(cty.NumberIntVal(int64(i)))) 548 | // if err != nil { 549 | // return nil, err 550 | // } 551 | // out.Index(i).Set(reflect.ValueOf(elem)) 552 | // } 553 | // return out.Interface(), nil 554 | // case reflect.Map: 555 | // if !ctyType.IsMapType() { 556 | // return nil, fmt.Errorf("expected map, got %s", ctyType.FriendlyName()) 557 | // } 558 | // out := reflect.MakeMap(goType) 559 | // for key, ctyElement := range ctyValue.AsValueMap() { 560 | // elem, err := CtyToGo(goType.Elem(), ctyElement) 561 | // if err != nil { 562 | // return nil, err 563 | // } 564 | // out.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(elem)) 565 | // } 566 | // return out.Interface(), nil 567 | // default: 568 | // return nil, fmt.Errorf("unsupported type %s", goType.String()) 569 | // } 570 | // } 571 | 572 | func GoToProto(tfType tftypes.Type, value any) (*tfprotov6.DynamicValue, error) { 573 | tfValue, err := GoToTfValue(tfType, value) 574 | if err != nil { 575 | return nil, err 576 | } 577 | return TfValueToProto(tfType, tfValue) 578 | } 579 | 580 | func GoToTfValue(tfType tftypes.Type, value any) (tftypes.Value, error) { 581 | if value == nil { 582 | if err := tftypes.ValidateValue(tfType, nil); err != nil { 583 | return tftypes.Value{}, err 584 | } 585 | return tftypes.NewValue(tfType, nil), nil 586 | } 587 | 588 | switch { 589 | case tfType.Is(tftypes.String): 590 | return tftypes.NewValue(tftypes.String, value), nil 591 | case tfType.Is(tftypes.Bool): 592 | return tftypes.NewValue(tftypes.Bool, value), nil 593 | case tfType.Is(tftypes.Number): 594 | switch value := value.(type) { 595 | case int: 596 | return tftypes.NewValue(tftypes.Number, value), nil 597 | case float64: 598 | return tftypes.NewValue(tftypes.Number, value), nil 599 | default: 600 | return tftypes.Value{}, fmt.Errorf("expected number, got %T", value) 601 | } 602 | case tfType.Is(tftypes.DynamicPseudoType): 603 | panic("implement interface{}") 604 | default: 605 | switch tfType := tfType.(type) { 606 | case tftypes.List: 607 | if reflect.TypeOf(value).Kind() != reflect.Slice { 608 | return tftypes.Value{}, fmt.Errorf("expected slice, got %T", value) 609 | } 610 | slice := reflect.ValueOf(value) 611 | out := make([]tftypes.Value, slice.Len()) 612 | for i := 0; i < slice.Len(); i++ { 613 | elem, err := GoToTfValue(tfType.ElementType, slice.Index(i).Interface()) 614 | if err != nil { 615 | return tftypes.Value{}, err 616 | } 617 | out[i] = elem 618 | } 619 | return tftypes.NewValue(tfType, out), nil 620 | case tftypes.Map: 621 | if reflect.TypeOf(value).Kind() != reflect.Map { 622 | return tftypes.Value{}, fmt.Errorf("expected map, got %T", value) 623 | } 624 | m := reflect.ValueOf(value) 625 | out := make(map[string]tftypes.Value, m.Len()) 626 | for _, key := range m.MapKeys() { 627 | elem, err := GoToTfValue(tfType.ElementType, m.MapIndex(key).Interface()) 628 | if err != nil { 629 | return tftypes.Value{}, err 630 | } 631 | out[key.String()] = elem 632 | } 633 | return tftypes.NewValue(tfType, out), nil 634 | case tftypes.Object: 635 | if reflect.TypeOf(value).Kind() != reflect.Struct { 636 | return tftypes.Value{}, fmt.Errorf("expected struct, got %T", value) 637 | } 638 | out := make(map[string]tftypes.Value, len(tfType.AttributeTypes)) 639 | for i := 0; i < reflect.TypeOf(value).NumField(); i++ { 640 | field := reflect.TypeOf(value).Field(i) 641 | tfName := getTfObjectGoFieldName(field) 642 | elem, err := GoToTfValue(tfType.AttributeTypes[tfName], reflect.ValueOf(value).Field(i).Interface()) 643 | if err != nil { 644 | return tftypes.Value{}, err 645 | } 646 | out[tfName] = elem 647 | } 648 | return tftypes.NewValue(tfType, out), nil 649 | default: 650 | return tftypes.Value{}, fmt.Errorf("unsupported type %s", tfType.String()) 651 | } 652 | } 653 | } 654 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | go = { 4 | source = "terraform.local/local/go" 5 | version = "0.0.1" 6 | } 7 | } 8 | } 9 | 10 | provider "go" { 11 | go = file("./fixtures/lib.go") 12 | } 13 | 14 | output "test" { 15 | value = provider::go::hello("papaya") 16 | } 17 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | go build 2 | 3 | dest=~/.terraform.d/plugins/terraform.local/local/go/0.0.1/darwin_arm64/terraform-provider-go_v0.0.1 4 | mkdir -p $(dirname $dest) 5 | 6 | cp terraform-provider-go $dest 7 | 8 | rm .terraform* -r 9 | tofu init -reconfigure 10 | tofu plan 11 | --------------------------------------------------------------------------------