├── .github ├── CODEOWNERS ├── mergify.yml ├── settings.yml └── workflows │ ├── acctest.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SUPPORT.md ├── docs ├── DCO.md ├── index.md └── resources │ ├── hardware.md │ ├── template.md │ └── workflow.md ├── examples └── main.tf ├── go.mod ├── go.sum ├── go.tools.mod ├── go.tools.sum ├── main.go ├── test ├── README.md ├── docker-compose.yml └── tls │ ├── .gitignore │ ├── Dockerfile │ ├── ca-config.json │ ├── ca.in.json │ ├── ca.json │ ├── entrypoint.sh │ ├── gencerts.sh │ ├── server-csr.in.json │ └── server-csr.json └── tinkerbell ├── doc.go ├── helpers.go ├── provider.go ├── provider_test.go ├── resource_hardware.go ├── resource_hardware_test.go ├── resource_template.go ├── resource_template_test.go ├── resource_workflow.go └── resource_workflow_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /.github/settings.yml @jeremytanner 2 | /.github/CODEOWNERS @jeremytanner 3 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - check-success=DCO 6 | - check-success=golangci 7 | - check-success=test 8 | 9 | pull_request_rules: 10 | - name: Automatic merge on approval 11 | conditions: 12 | - base=main 13 | - "#approved-reviews-by>=1" 14 | - "#changes-requested-reviews-by=0" 15 | - "#review-requested=0" 16 | - check-success=DCO 17 | - check-success=golangci 18 | - check-success=test 19 | - label!=do-not-merge 20 | - label=ready-to-merge 21 | actions: 22 | queue: 23 | method: merge 24 | name: default 25 | commit_message_template: | 26 | {{ title }} (#{{ number }}) 27 | 28 | {{ body }} 29 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | # Collaborators: give specific users access to this repository. 2 | # See https://docs.github.com/en/rest/reference/repos#add-a-repository-collaborator for available options 3 | collaborators: 4 | # Maintainers, should also be added to the .github/CODEOWNERS file as owners of this settings.yml file. 5 | - username: jeremytanner 6 | permission: maintain 7 | # Approvers 8 | - username: displague 9 | permission: push 10 | - username: mmlb 11 | permission: push 12 | # Reviewers 13 | - username: jacobweinstock 14 | permission: triage 15 | 16 | # Note: `permission` is only valid on organization-owned repositories. 17 | # The permission to grant the collaborator. Can be one of: 18 | # * `pull` - can pull, but not push to or administer this repository. 19 | # * `push` - can pull and push, but not administer this repository. 20 | # * `admin` - can pull, push and administer this repository. 21 | # * `maintain` - Recommended for project managers who need to manage the repository without access to sensitive or destructive actions. 22 | # * `triage` - Recommended for contributors who need to proactively manage issues and pull requests without write access. 23 | -------------------------------------------------------------------------------- /.github/workflows/acctest.yml: -------------------------------------------------------------------------------- 1 | name: Acceptance Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - "LICENSE" 6 | - "**.md" 7 | - "docs/**" 8 | push: 9 | paths-ignore: 10 | - "LICENSE" 11 | - "**.md" 12 | - "docs/**" 13 | schedule: 14 | - cron: "0 13 * * *" 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | runs-on: ubuntu-latest 20 | timeout-minutes: 10 21 | steps: 22 | - name: Set up Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: "1.17" 26 | id: go 27 | 28 | - name: Check out code into the Go module directory 29 | uses: actions/checkout@v2.3.2 30 | 31 | - name: Get dependencies 32 | run: make download 33 | 34 | - name: Build 35 | run: make build 36 | 37 | test: 38 | name: Matrix Test 39 | needs: build 40 | runs-on: ubuntu-latest 41 | timeout-minutes: 240 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | version: 46 | - stable 47 | terraform: 48 | - "0.12.29" 49 | - "0.13.4" 50 | steps: 51 | - name: Set up Go 52 | uses: actions/setup-go@v2 53 | with: 54 | go-version: "1.17" 55 | id: go 56 | 57 | - name: Check out code into the Go module directory 58 | uses: actions/checkout@v2.3.2 59 | 60 | - name: Get dependencies 61 | run: make download 62 | 63 | - name: Start Tinkerbell server 64 | run: make test-up 65 | 66 | - name: TF acceptance tests 67 | timeout-minutes: 120 68 | env: 69 | TF_ACC_TERRAFORM_VERSION: ${{ matrix.terraform }} 70 | run: make testacc 71 | 72 | golangci: 73 | name: lint 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v2 77 | - name: golangci-lint 78 | uses: golangci/golangci-lint-action@v2 79 | with: 80 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 81 | version: v1.46 82 | 83 | codespell: 84 | name: Codespell 85 | runs-on: ubuntu-latest 86 | steps: 87 | - name: Checkout Code 88 | uses: actions/checkout@v2 89 | - name: Codespell 90 | uses: codespell-project/actions-codespell@master 91 | with: 92 | check_filenames: true 93 | check_hidden: true 94 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub action can publish assets for release when a tag is created. 2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 3 | # 4 | # This uses an action (paultyng/ghaction-import-gpg) that assumes you set your 5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 6 | # secret. If you would rather own your own GPG handling, please fork this action 7 | # or use an alternative one for key handling. 8 | # 9 | # You will need to pass the `--batch` flag to `gpg` in your signing step 10 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 11 | # 12 | name: release 13 | on: 14 | push: 15 | tags: 16 | - 'v*' 17 | 18 | jobs: 19 | goreleaser: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | 25 | - name: Unshallow 26 | run: git fetch --prune --unshallow 27 | 28 | - name: Set up Go 29 | uses: actions/setup-go@v2 30 | with: 31 | go-version: 1.17 32 | 33 | - name: Import GPG key 34 | id: import_gpg 35 | uses: paultyng/ghaction-import-gpg@v2.1.0 36 | env: 37 | GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} 38 | PASSPHRASE: ${{ secrets.PASSPHRASE }} 39 | 40 | - name: Run GoReleaser 41 | uses: goreleaser/goreleaser-action@v2 42 | with: 43 | version: latest 44 | args: release --rm-dist 45 | env: 46 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.terraform 2 | /terraform-provider-tinkerbell 3 | /main.tf 4 | /terraform.tfstate* 5 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | output: 2 | sort-results: true 3 | 4 | issues: 5 | exclude-use-default: false 6 | max-same-issues: 0 7 | max-issues-per-linter: 0 8 | exclude-rules: 9 | 10 | linters-settings: 11 | exhaustive: 12 | default-signifies-exhaustive: true 13 | 14 | # List for enabled linters can be generated for updates using the following command. 15 | # golangci-lint linters | grep -E '^\S+:' | cut -d: -f1 | sort | sed 's/^/ - /g' | grep -v -E "($(grep '^ disable:' -A 100 .golangci.yml | grep -E ' - \S+$' | awk '{print $2}' | tr \\n '|' | sed 's/|$//g'))" 16 | linters: 17 | disable-all: false 18 | enable: 19 | - asciicheck 20 | - bodyclose 21 | - deadcode 22 | - depguard 23 | - dogsled 24 | - dupl 25 | - errcheck 26 | - errorlint 27 | - exhaustive 28 | - exportloopref 29 | - funlen 30 | - gochecknoglobals 31 | - gochecknoinits 32 | - gocognit 33 | - goconst 34 | - gocritic 35 | - gocyclo 36 | - godot 37 | - gofmt 38 | - gofumpt 39 | - goheader 40 | - goimports 41 | - golint 42 | - gomnd 43 | - gomodguard 44 | - goprintffuncname 45 | - ineffassign 46 | - interfacer 47 | - lll 48 | - maligned 49 | - misspell 50 | - nakedret 51 | - nestif 52 | - nlreturn 53 | - noctx 54 | - nolintlint 55 | - paralleltest 56 | - prealloc 57 | - rowserrcheck 58 | - scopelint 59 | - sqlclosecheck 60 | - structcheck 61 | - stylecheck 62 | - tparallel 63 | - typecheck 64 | - unconvert 65 | - unparam 66 | - varcheck 67 | - whitespace 68 | - wrapcheck 69 | - wsl 70 | disable: 71 | # We use gofmt for formatting and gci is not compatible with it. 72 | - gci 73 | 74 | # This code do not export any API returning errors 75 | # and internally there is no need to use typed errors. 76 | - goerr113 77 | 78 | # Terraform testing convention do not encourage to use them. 79 | - testpackage 80 | 81 | # It is OK to have TODOs in the code. 82 | - godox 83 | 84 | # As many structs in this code won't use all the fields and it's OK. 85 | - exhaustivestruct 86 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | # goreleaser does not work with CGO, it could also complicate 4 | # usage by users in CI/CD systems like Terraform Cloud where 5 | # they are unable to install libraries. 6 | - CGO_ENABLED=0 7 | mod_timestamp: '{{ .CommitTimestamp }}' 8 | flags: 9 | - -trimpath 10 | ldflags: 11 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 12 | goos: 13 | - freebsd 14 | - windows 15 | - linux 16 | - darwin 17 | goarch: 18 | - amd64 19 | - '386' 20 | - arm 21 | - arm64 22 | ignore: 23 | - goos: darwin 24 | goarch: '386' 25 | binary: '{{ .ProjectName }}_v{{ .Version }}' 26 | archives: 27 | - format: zip 28 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 29 | checksum: 30 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 31 | algorithm: sha256 32 | signs: 33 | - artifacts: checksum 34 | args: 35 | # if you are using this is a GitHub action or some other automated pipeline, you 36 | # need to pass the batch flag to indicate its not interactive. 37 | - "--batch" 38 | - "--local-user" 39 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 40 | - "--output" 41 | - "${signature}" 42 | - "--detach-sign" 43 | - "${artifact}" 44 | release: 45 | # Visit your project's GitHub Releases page to publish this release. 46 | draft: true 47 | github: 48 | owner: tinkerbell 49 | name: terraform-provider-tinkerbell 50 | changelog: 51 | skip: true 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.1 (August 13, 2021) 2 | 3 | NOTES: 4 | 5 | * No new functionality, works with current sandbox 6 | 7 | * Fix GitHub actions and update used Go version [#19](https://github.com/tinkerbell/terraform-provider-tinkerbell/pull/19) 8 | * Update Tinkerbell and linter, add linter and codespell to CI [#20](https://github.com/tinkerbell/terraform-provider-tinkerbell/pull/20) 9 | * Add/update DCO, CoC, contributing ... files [#24](https://github.com/tinkerbell/terraform-provider-tinkerbell/pull/24) 10 | * Update to newer sandbox version and remove some workarounds [#23](https://github.com/tinkerbell/terraform-provider-tinkerbell/pull/23) 11 | * Newer tink for release v0.1.1 / move from Kinvolk -> Tinkerbell repo [#25](https://github.com/tinkerbell/terraform-provider-tinkerbell/pull/25) 12 | 13 | ## 0.1.0 (October 20, 2020) 14 | 15 | NOTES: 16 | 17 | * Initial release 18 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Refer to our [Code of Conduct](https://github.com/tinkerbell/.github/blob/main/CODE_OF_CONDUCT.md) 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Hello Contributors! 2 | 3 | Thanks for your interest! 4 | We're so glad you're here. 5 | 6 | ### Important Resources 7 | 8 | #### bugs: [https://github.com/tinkerbell/terraform-provider-tinkerbell/issues](https://github.com/tinkerbell/terraform-provider-tinkerbell/issues) 9 | 10 | ### Code of Conduct 11 | 12 | Please read and understand the code of conduct found [here](https://github.com/tinkerbell/.github/blob/main/CODE_OF_CONDUCT.md). 13 | 14 | ### DCO Sign Off 15 | 16 | Please read and understand the DCO found [here](docs/DCO.md). 17 | 18 | ### Environment Details 19 | 20 | Building is handled by `make`, please see the [Makefile](Makefile) for available targets. 21 | 22 | ### How to Submit Change Requests 23 | 24 | Please submit change requests and / or features via [Issues](https://github.com/tinkerbell/terraform-provider-tinkerbell/issues). 25 | There's no guarantee it'll be changed, but you never know until you try. 26 | We'll try to add comments as soon as possible, though. 27 | 28 | ### How to Report a Bug 29 | 30 | Bugs are problems in code, in the functionality of an application or in its UI design; you can submit them through [Issues](https://github.com/tinkerbell/terraform-provider-tinkerbell/issues). 31 | 32 | ## Code Style Guides 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 Packet Host, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Build parameters. 2 | CGO_ENABLED=0 3 | LD_FLAGS="-extldflags '-static'" 4 | 5 | # Go parameters. 6 | GOCMD=go 7 | GOBUILD=$(GOCMD) build 8 | GOCLEAN=$(GOCMD) clean 9 | GOTEST=$(GOCMD) test -v 10 | GOGET=$(GOCMD) get 11 | GOMOD=$(GOCMD) mod 12 | GOBUILD=CGO_ENABLED=$(CGO_ENABLED) $(GOCMD) build -v -buildmode=exe -ldflags $(LD_FLAGS) 13 | GO_PACKAGES=./... 14 | GO_TESTS=^.*$ 15 | 16 | GOLANGCI_LINT_VERSION=v1.33.0 17 | 18 | BIN_PATH=$$HOME/bin 19 | TF_ACC= 20 | TINKERBELL_GRPC_AUTHORITY=127.0.0.1:42113 21 | TINKERBELL_CERT_URL=http://127.0.0.1:42114/cert 22 | 23 | GITHUB_TOKEN= 24 | GPG_FINGERPRINT= 25 | RELEASE_VERSION= 26 | 27 | .PHONY: all ## Build the binary, run unit tests and run linter. 28 | all: build build-test test lint 29 | 30 | .PHONY: download 31 | download: ## Download Go module dependencies required for building and testing. 32 | $(GOMOD) download 33 | 34 | .PHONY: install-golangci-lint 35 | install-golangci-lint: ## Installs golangci-lint binary into BIN_PATH. 36 | curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(BIN_PATH) $(GOLANGCI_LINT_VERSION) 37 | 38 | .PHONY: install-cc-test-reporter 39 | install-cc-test-reporter: ## Installs Code Climate test reporter binary into BIN_PATH. 40 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > $(BIN_PATH)/cc-test-reporter 41 | chmod +x $(BIN_PATH)/cc-test-reporter 42 | 43 | .PHONY: install-ci ## Installs binaries required for CI. 44 | install-ci: install-golangci-lint install-cc-test-reporter 45 | 46 | .PHONY: build 47 | build: ## Build Terraform provider binary. 48 | $(GOBUILD) 49 | 50 | .PHONY: test 51 | test: build-test ## Run unit tests matching GO_TESTS in GO_PACKAGES. 52 | TF_ACC=$(TF_ACC) TINKERBELL_GRPC_AUTHORITY=$(TINKERBELL_GRPC_AUTHORITY) TINKERBELL_CERT_URL=$(TINKERBELL_CERT_URL) $(GOTEST) -run $(GO_TESTS) $(GO_PACKAGES) 53 | 54 | .PHONY: lint 55 | lint: build build-test ## Compile code and run linter. 56 | golangci-lint run $(GO_PACKAGES) 57 | 58 | .PHONY: build-test 59 | build-test: # Compile unit tests. Useful for checking syntax errors before running unit tests. 60 | $(GOTEST) -run=nope $(GO_PACKAGES) 61 | 62 | .PHONY: update 63 | update: ## Updates all Go module dependencies. 64 | $(GOGET) -u $(GO_PACKAGES) 65 | $(GOMOD) tidy 66 | 67 | .PHONY: all-cover 68 | all-cover: build build-test test-cover lint ## Builds the binary, runs unit tests with coverage report and runs linter. 69 | 70 | .PHONY: test-cover 71 | test-cover: build-test ## Runs unit tests and writes coverage report to a PROFILEFILE for given GO_PACKAGES. 72 | $(GOTEST) -run $(GO_TESTS) -coverprofile=$(PROFILEFILE) $(GO_PACKAGES) 73 | 74 | .PHONY: cover-upload 75 | cover-upload: codecov ## Runs unit tests with coverage report and uploads it to Codecov and Code Climate. 76 | # Make codeclimate as command, as we need to run test-cover twice and make deduplicates that. 77 | # Go test results are cached anyway, so it's fine to run it multiple times. 78 | make codeclimate 79 | 80 | .PHONY: codecov 81 | codecov: PROFILEFILE=coverage.txt 82 | codecov: SHELL=/bin/bash 83 | codecov: test-cover 84 | codecov: ## Runs unit tests with coverage report and uploads it to Codecov. 85 | bash <(curl -s https://codecov.io/bash) 86 | 87 | .PHONY: testacc 88 | testacc: TF_ACC=true 89 | testacc: test ## Runs Terraform acceptance tests. 90 | 91 | .PHONY: release-env-check 92 | release-env-check: ## Checks if required environment variables are set to create a release. 93 | ifndef GITHUB_TOKEN 94 | $(error GITHUB_TOKEN is undefined) 95 | endif 96 | ifndef GPG_FINGERPRINT 97 | $(error GPG_FINGERPRINT is undefined) 98 | endif 99 | ifndef RELEASE_VERSION 100 | $(error RELEASE_VERSION is undefined) 101 | endif 102 | 103 | .PHONY: release 104 | release: release-env-check all 105 | release: ## Creates a GitHub release using goreleaser. 106 | GITHUB_TOKEN=$(GITHUB_TOKEN) GPG_FINGERPRINT=$(GPG_FINGERPRINT) bash -c 'go run -modfile=go.tools.mod github.com/goreleaser/goreleaser release --release-notes <(go run -modfile=go.tools.mod github.com/rcmachado/changelog show $(RELEASE_VERSION))' 107 | 108 | .PHONY: install-tools 109 | install-tools: ## Installs development tools required for creating a release. 110 | @echo Installing tools from tools.go 111 | @cat tools.go | grep _ | awk -F'"' '{print $$2}' | xargs -tI % go get % 112 | 113 | .PHONY: test-up 114 | test-up: ## Starts testing tink-server instance in Docker container using docker-compose. 115 | docker-compose -f test/docker-compose.yml up -d 116 | 117 | .PHONY: test-down 118 | test-down: ## Tears down testing tink-server instance created by 'test-up'. 119 | docker-compose -f test/docker-compose.yml down 120 | 121 | .PHONY: help 122 | help: ## Prints help message. 123 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinkerbell Terraform Provider 2 | 3 | ![](https://img.shields.io/badge/Stability-Experimental-red.svg) 4 | 5 | This repository is [Experimental](https://github.com/packethost/standards/blob/main/experimental-statement.md) meaning that it's based on untested ideas or techniques and not yet established or finalized or involves a radically new and innovative style! This means that support is best effort (at best!) and we strongly encourage you to NOT use this in production. 6 | 7 | The Tinkerbell provider allows to create [Tinkerbell](https://tinkerbell.org/) hardware entried, templates and workflows in a declarative way. 8 | 9 | ## Table of contents 10 | * [User documentation](#user-documentation) 11 | * [Building and testing](#building-and-testing) 12 | * [Releasing](#releasing) 13 | * [Authors](#authors) 14 | 15 | ## User documentation 16 | 17 | For user documentation, see [Terraform Registry](https://registry.terraform.io/providers/tinkerbell/tinkerbell/latest/docs). 18 | 19 | ## Building and testing 20 | 21 | For local builds, run `make` which will build the binary, run unit tests and linter. 22 | 23 | ## Releasing 24 | 25 | This project use `goreleaser` with GitHub actions for releasing. To release new version, follow the following steps: 26 | 27 | * Add a changelog for new release to CHANGELOG.md file. 28 | 29 | * Tag new release on desired git, using example command: 30 | 31 | ```sh 32 | git tag -a v0.4.7 -s -m "Release v0.4.7" 33 | ``` 34 | 35 | * Push the tag to GitHub 36 | ```sh 37 | git push origin v0.4.7 38 | ``` 39 | 40 | * Once tag is pushed, GitHub action should trigger and create a release for a given tag. 41 | 42 | * Go to newly create [GitHub release](https://github.com/tinkerbell/terraform-provider-tinkerbell/releases/tag/v0.4.7), 43 | add a changelog from CHANGELOG.md file, verify that artefacts looks correct and publish it. 44 | 45 | ## Authors 46 | 47 | * **Mateusz Gozdek** - *Initial work* - [invidian](https://github.com/invidian) 48 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | If you require support, please subscribe to the [Packet Community Slack](https://slack.packet.com/) and join the #tinkerbell channel or post an issue within this repository. 2 | -------------------------------------------------------------------------------- /docs/DCO.md: -------------------------------------------------------------------------------- 1 | # DCO Sign Off 2 | 3 | All authors to the project retain copyright to their work. However, to ensure 4 | that they are only submitting work that they have rights to, we are requiring 5 | everyone to acknowledge this by signing their work. 6 | 7 | Since this signature indicates your rights to the contribution and 8 | certifies the statements below, it must contain your real name and 9 | email address. Various forms of noreply email address must not be used. 10 | 11 | Any copyright notices in this repository should specify the authors as "The 12 | project authors". 13 | 14 | To sign your work, just add a line like this at the end of your commit message: 15 | 16 | ```text 17 | Signed-off-by: Jess Owens 18 | ``` 19 | 20 | This can easily be done with the `--signoff` option to `git commit`. 21 | 22 | By doing this you state that you can certify the following (from [https://developercertificate.org/][1]): 23 | 24 | ```text 25 | Developer Certificate of Origin 26 | Version 1.1 27 | 28 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 29 | 1 Letterman Drive 30 | Suite D4700 31 | San Francisco, CA, 94129 32 | 33 | Everyone is permitted to copy and distribute verbatim copies of this 34 | license document, but changing it is not allowed. 35 | 36 | 37 | Developer's Certificate of Origin 1.1 38 | 39 | By making a contribution to this project, I certify that: 40 | 41 | (a) The contribution was created in whole or in part by me and I 42 | have the right to submit it under the open source license 43 | indicated in the file; or 44 | 45 | (b) The contribution is based upon previous work that, to the best 46 | of my knowledge, is covered under an appropriate open source 47 | license and I have the right under that license to submit that 48 | work with modifications, whether created in whole or in part 49 | by me, under the same open source license (unless I am 50 | permitted to submit under a different license), as indicated 51 | in the file; or 52 | 53 | (c) The contribution was provided directly to me by some other 54 | person who certified (a), (b) or (c) and I have not modified 55 | it. 56 | 57 | (d) I understand and agree that this project and the contribution 58 | are public and that a record of the contribution (including all 59 | personal information I submit with it, including my sign-off) is 60 | maintained indefinitely and may be redistributed consistent with 61 | this project or the open source license(s) involved. 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Tinkerbell Provider 2 | 3 | The Tinkerbell provider allows to create [Tinkerbell](https://tinkerbell.org/) hardware entried, templates and workflows in a declarative way. 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | terraform { 9 | required_providers { 10 | tinkerbell = { 11 | source = "tinkerbell/tinkerbell" 12 | version = "0.1.0" 13 | } 14 | } 15 | } 16 | 17 | provider "tinkerbell" { 18 | grpc_authority = "127.0.0.1:42113" 19 | cert_url = "http://127.0.0.1:42114/cert" 20 | } 21 | 22 | resource "tinkerbell_hardware" "foo" { 23 | data = < 73 | actions: 74 | - name: "disk-wipe" 75 | image: disk-wipe 76 | timeout: 90 77 | - name: "disk-partition" 78 | image: disk-partition 79 | timeout: 600 80 | environment: 81 | MIRROR_HOST: 82 | volumes: 83 | - /statedir:/statedir 84 | - name: "install-root-fs" 85 | image: install-root-fs 86 | timeout: 600 87 | - name: "install-grub" 88 | image: install-grub 89 | timeout: 600 90 | volumes: 91 | - /statedir:/statedir 92 | EOF 93 | } 94 | 95 | resource "tinkerbell_workflow" "foo" { 96 | template = tinkerbell_template.foo.id 97 | hardwares = < 23 | actions: 24 | - name: "disk-wipe" 25 | image: disk-wipe 26 | timeout: 90 27 | - name: "disk-partition" 28 | image: disk-partition 29 | timeout: 600 30 | environment: 31 | MIRROR_HOST: 32 | volumes: 33 | - /statedir:/statedir 34 | - name: "install-root-fs" 35 | image: install-root-fs 36 | timeout: 600 37 | - name: "install-grub" 38 | image: install-grub 39 | timeout: 600 40 | volumes: 41 | - /statedir:/statedir 42 | EOF 43 | } 44 | ``` 45 | 46 | ## Argument Reference 47 | 48 | * `name` - (Required) Template name. 49 | * `content` - (Requires) Template content in YAML format. See Tinkerbell [documentation](https://docs.tinkerbell.org/about/templates/) for more details. 50 | -------------------------------------------------------------------------------- /docs/resources/workflow.md: -------------------------------------------------------------------------------- 1 | # Workflow Resource 2 | 3 | This resource allows to create Tinkerbell [workflows](https://docs.tinkerbell.org/about/workflows/). 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | resource "tinkerbell_hardware" "foo" { 9 | data = < 59 | actions: 60 | - name: "disk-wipe" 61 | image: disk-wipe 62 | timeout: 90 63 | - name: "disk-partition" 64 | image: disk-partition 65 | timeout: 600 66 | environment: 67 | MIRROR_HOST: 68 | volumes: 69 | - /statedir:/statedir 70 | - name: "install-root-fs" 71 | image: install-root-fs 72 | timeout: 600 73 | - name: "install-grub" 74 | image: install-grub 75 | timeout: 600 76 | volumes: 77 | - /statedir:/statedir 78 | EOF 79 | } 80 | 81 | resource "tinkerbell_workflow" "foo" { 82 | template = tinkerbell_template.foo.id 83 | hardwares = < 66 | actions: 67 | - name: "disk-wipe" 68 | image: disk-wipe 69 | timeout: 90 70 | - name: "disk-partition" 71 | image: disk-partition 72 | timeout: 600 73 | environment: 74 | MIRROR_HOST: 75 | volumes: 76 | - /statedir:/statedir 77 | - name: "install-root-fs" 78 | image: install-root-fs 79 | timeout: 600 80 | - name: "install-grub" 81 | image: install-grub 82 | timeout: 600 83 | volumes: 84 | - /statedir:/statedir 85 | EOF 86 | } 87 | 88 | resource "tinkerbell_workflow" "foo" { 89 | template = tinkerbell_template.foo.id 90 | hardwares = <bundle.pem.tmp 23 | 24 | # only "modify" the file if truly necessary since workflow will serve it with 25 | # modtime info for client caching purposes 26 | if ! cmp -s bundle.pem.tmp bundle.pem; then 27 | mv bundle.pem.tmp bundle.pem 28 | else 29 | rm bundle.pem.tmp 30 | fi 31 | -------------------------------------------------------------------------------- /test/tls/server-csr.in.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "tinkerbell", 3 | "hosts": [ 4 | "tinkerbell.registry", 5 | "tinkerbell.tinkerbell", 6 | "tinkerbell", 7 | "localhost", 8 | "127.0.0.1" 9 | ], 10 | "key": { 11 | "algo": "rsa", 12 | "size": 2048 13 | }, 14 | "names": [ 15 | { 16 | "L": "@FACILITY@" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /test/tls/server-csr.json: -------------------------------------------------------------------------------- 1 | { 2 | "CN": "tinkerbell", 3 | "hosts": [ 4 | "127.0.0.1", 5 | "192.168.1.1", 6 | "localhost", 7 | "tinkerbell", 8 | "tinkerbell.onprem.packet.net", 9 | "tinkerbell.registry", 10 | "tinkerbell.tinkerbell" 11 | ], 12 | "key": { 13 | "algo": "rsa", 14 | "size": 2048 15 | }, 16 | "names": [ 17 | { 18 | "L": "onprem" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tinkerbell/doc.go: -------------------------------------------------------------------------------- 1 | // Package tinkerbell implements Terraform provider for Tinkerbell. 2 | package tinkerbell 3 | -------------------------------------------------------------------------------- /tinkerbell/helpers.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 5 | ) 6 | 7 | func diagsFromErr(err error) diag.Diagnostics { 8 | if err != nil { 9 | return diag.FromErr(err) 10 | } 11 | 12 | return nil 13 | } 14 | -------------------------------------------------------------------------------- /tinkerbell/provider.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | 10 | "github.com/tinkerbell/tink/client" 11 | "github.com/tinkerbell/tink/protos/hardware" 12 | "github.com/tinkerbell/tink/protos/template" 13 | "github.com/tinkerbell/tink/protos/workflow" 14 | ) 15 | 16 | // Provider returns the Tinkerbell terraform provider. 17 | func Provider() *schema.Provider { 18 | return &schema.Provider{ 19 | Schema: map[string]*schema.Schema{ 20 | "grpc_authority": { 21 | Type: schema.TypeString, 22 | Optional: true, 23 | Description: "Equivalent of TINKERBELL_GRPC_AUTHORITY environment variable.", 24 | }, 25 | "cert_url": { 26 | Type: schema.TypeString, 27 | Optional: true, 28 | Description: "Equivalent of TINKERBELL_CERT_URL environment variable.", 29 | }, 30 | }, 31 | ResourcesMap: map[string]*schema.Resource{ 32 | "tinkerbell_template": resourceTemplate(), 33 | "tinkerbell_workflow": resourceWorkflow(), 34 | "tinkerbell_hardware": resourceHardware(), 35 | }, 36 | ConfigureFunc: providerConfigure, 37 | } 38 | } 39 | 40 | type tinkClientConfig struct { 41 | providerConfig *schema.ResourceData 42 | client *tinkClient 43 | clientMutex sync.Mutex 44 | } 45 | 46 | type tinkClient struct { 47 | templateClient template.TemplateServiceClient 48 | workflowClient workflow.WorkflowServiceClient 49 | hardwareClient hardware.HardwareServiceClient 50 | } 51 | 52 | func (tc *tinkClientConfig) New() (*tinkClient, error) { 53 | tc.clientMutex.Lock() 54 | defer tc.clientMutex.Unlock() 55 | 56 | if tc.client != nil { 57 | return tc.client, nil 58 | } 59 | 60 | if grpcAuthority := tc.providerConfig.Get("grpc_authority").(string); grpcAuthority != "" { 61 | if err := os.Setenv("TINKERBELL_GRPC_AUTHORITY", grpcAuthority); err != nil { 62 | return nil, fmt.Errorf("setting TINKERBELL_GRPC_AUTHORITY environment variable: %w", err) 63 | } 64 | } 65 | 66 | if certURL := tc.providerConfig.Get("cert_url").(string); certURL != "" { 67 | if err := os.Setenv("TINKERBELL_CERT_URL", certURL); err != nil { 68 | return nil, fmt.Errorf("setting TINKERBELL_CERT_URL environment variable: %w", err) 69 | } 70 | } 71 | 72 | conn, err := client.GetConnection() 73 | if err != nil { 74 | return nil, fmt.Errorf("creating tink client: %w", err) 75 | } 76 | 77 | tc.client = &tinkClient{ 78 | templateClient: template.NewTemplateServiceClient(conn), 79 | workflowClient: workflow.NewWorkflowServiceClient(conn), 80 | hardwareClient: hardware.NewHardwareServiceClient(conn), 81 | } 82 | 83 | return tc.client, nil 84 | } 85 | 86 | func providerConfigure(d *schema.ResourceData) (interface{}, error) { 87 | return &tinkClientConfig{ 88 | providerConfig: d, 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /tinkerbell/provider_test.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | ) 9 | 10 | //nolint:gochecknoglobals 11 | var testAccProviders map[string]*schema.Provider 12 | 13 | //nolint:gochecknoglobals 14 | var testAccProvider *schema.Provider 15 | 16 | //nolint:gochecknoinits 17 | func init() { 18 | testAccProvider = Provider() 19 | testAccProviders = map[string]*schema.Provider{ 20 | "tinkerbell": testAccProvider, 21 | } 22 | } 23 | 24 | func TestProvider(t *testing.T) { 25 | t.Parallel() 26 | 27 | if err := Provider().InternalValidate(); err != nil { 28 | t.Fatalf("Internal provider validation: %v", err) 29 | } 30 | } 31 | 32 | // testAccPreCheck validates the necessary test environment variables exist. 33 | func testAccPreCheck(t *testing.T) { 34 | if v := os.Getenv("TINKERBELL_GRPC_AUTHORITY"); v == "" { 35 | t.Fatal("TINKERBELL_GRPC_AUTHORITY must be set for acceptance tests") 36 | } 37 | 38 | if v := os.Getenv("TINKERBELL_CERT_URL"); v == "" { 39 | t.Fatal("TINKERBELL_CERT_URL must be set for acceptance tests") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tinkerbell/resource_hardware.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/hashicorp/go-cty/cty" 15 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 16 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" 17 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 18 | "github.com/tinkerbell/tink/pkg" 19 | "github.com/tinkerbell/tink/protos/hardware" 20 | ) 21 | 22 | const ( 23 | dataAttribute = "data" 24 | ) 25 | 26 | func resourceHardware() *schema.Resource { 27 | return &schema.Resource{ 28 | CreateContext: resourceHardwareCreate, 29 | ReadContext: resourceHardwareRead, 30 | DeleteContext: resourceHardwareDelete, 31 | UpdateContext: resourceHardwareUpdate, 32 | CustomizeDiff: customdiff.All( 33 | customdiff.ForceNewIfChange("data", func(ctx context.Context, old, new, meta interface{}) bool { 34 | oldHw := pkg.HardwareWrapper{} 35 | 36 | if err := json.Unmarshal([]byte(old.(string)), &oldHw); err != nil { 37 | log.Printf("Got malformed hardware entry from Terraform state %q: %v", old.(string), err) 38 | 39 | return true 40 | } 41 | 42 | newHw := pkg.HardwareWrapper{} 43 | 44 | if err := json.Unmarshal([]byte(new.(string)), &newHw); err != nil { 45 | log.Printf("Got malformed hardware entry from the configuration %q: %v", new.(string), err) 46 | 47 | return true 48 | } 49 | 50 | return oldHw.Hardware.Id != newHw.Hardware.Id 51 | }), 52 | ), 53 | Schema: map[string]*schema.Schema{ 54 | dataAttribute: { 55 | Type: schema.TypeString, 56 | Required: true, 57 | DiffSuppressFunc: suppressEquivalentJSONDiffs, 58 | ValidateDiagFunc: validateHardwareData, 59 | }, 60 | }, 61 | } 62 | } 63 | 64 | func suppressEquivalentJSONDiffs(k, old, new string, d *schema.ResourceData) bool { 65 | ob := bytes.NewBufferString("") 66 | if err := json.Compact(ob, []byte(old)); err != nil { 67 | return false 68 | } 69 | 70 | nb := bytes.NewBufferString("") 71 | if err := json.Compact(nb, []byte(new)); err != nil { 72 | return false 73 | } 74 | 75 | return jsonBytesEqual(ob.Bytes(), nb.Bytes()) 76 | } 77 | 78 | func jsonBytesEqual(b1, b2 []byte) bool { 79 | var o1 interface{} 80 | if err := json.Unmarshal(b1, &o1); err != nil { 81 | return false 82 | } 83 | 84 | var o2 interface{} 85 | if err := json.Unmarshal(b2, &o2); err != nil { 86 | return false 87 | } 88 | 89 | return reflect.DeepEqual(o1, o2) 90 | } 91 | 92 | func validateHardwareData(m interface{}, p cty.Path) diag.Diagnostics { 93 | hw := pkg.HardwareWrapper{} 94 | 95 | if err := json.Unmarshal([]byte(m.(string)), &hw); err != nil { 96 | return diagsFromErr(fmt.Errorf("failed decoding 'data' as JSON: %w", err)) 97 | } 98 | 99 | if hw.Hardware.Id == "" { 100 | return diagsFromErr(fmt.Errorf("ID is required in JSON data")) 101 | } 102 | 103 | return nil 104 | } 105 | 106 | const ( 107 | serializationError = "could not serialize access due to read/write dependencies among transactions" 108 | ) 109 | 110 | func retryOnTransientError(f func() error) error { 111 | err := f() 112 | if err == nil { 113 | return nil 114 | } 115 | 116 | if strings.HasSuffix(err.Error(), serializationError) { 117 | return retryOnTransientError(f) 118 | } 119 | 120 | return err 121 | } 122 | 123 | func resourceHardwareCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 124 | tc, err := m.(*tinkClientConfig).New() 125 | if err != nil { 126 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 127 | } 128 | 129 | c := tc.hardwareClient 130 | 131 | hw := pkg.HardwareWrapper{} 132 | 133 | // We can skip error checking here, validate function should already validate it. 134 | _ = json.Unmarshal([]byte(d.Get(dataAttribute).(string)), &hw) 135 | 136 | h, err := getHardware(ctx, c, hw.Hardware.Id) 137 | if err != nil { 138 | return diagsFromErr(fmt.Errorf("checking if hardware ID %q already exists: %w", hw.Hardware.Id, err)) 139 | } 140 | 141 | if h != nil { 142 | return diagsFromErr(fmt.Errorf("hardware ID %q already exists", hw.Hardware.Id)) 143 | } 144 | 145 | if _, err := c.Push(ctx, &hardware.PushRequest{Data: hw.Hardware}); err != nil { 146 | return diagsFromErr(fmt.Errorf("pushing hardware data: %w", err)) 147 | } 148 | 149 | d.SetId(hw.Hardware.Id) 150 | 151 | return nil 152 | } 153 | 154 | func resourceHardwareUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 155 | tc, err := m.(*tinkClientConfig).New() 156 | if err != nil { 157 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 158 | } 159 | 160 | c := tc.hardwareClient 161 | 162 | h, err := getHardware(ctx, c, d.Id()) 163 | if err != nil { 164 | return diagsFromErr(fmt.Errorf("checking if hardware ID %q already exists: %w", d.Id(), err)) 165 | } 166 | 167 | if h == nil { 168 | return diagsFromErr(fmt.Errorf("hardware ID %q does not exist", d.Id())) 169 | } 170 | 171 | hw := pkg.HardwareWrapper{} 172 | 173 | // We can skip error checking here, validate function should already validate it. 174 | _ = json.Unmarshal([]byte(d.Get(dataAttribute).(string)), &hw) 175 | 176 | if _, err := c.Push(ctx, &hardware.PushRequest{Data: hw.Hardware}); err != nil { 177 | return diagsFromErr(fmt.Errorf("pushing hardware data: %w", err)) 178 | } 179 | 180 | d.SetId(hw.Hardware.Id) 181 | 182 | return nil 183 | } 184 | 185 | func getHardware(ctx context.Context, c hardware.HardwareServiceClient, uuid string) (*hardware.Hardware, error) { 186 | list, err := c.All(ctx, &hardware.Empty{}) 187 | if err != nil { 188 | return nil, fmt.Errorf("getting all hardware entries: %w", err) 189 | } 190 | 191 | for { 192 | hw, err := list.Recv() 193 | if err != nil { 194 | if errors.Is(err, io.EOF) { 195 | break 196 | } 197 | 198 | return nil, fmt.Errorf("receiving hardware entry: %w", err) 199 | } 200 | 201 | if hw == nil { 202 | return nil, fmt.Errorf("received empty hardware entry: %w", err) 203 | } 204 | 205 | if hw.GetId() == uuid { 206 | return hw, nil 207 | } 208 | } 209 | 210 | return nil, nil 211 | } 212 | 213 | func resourceHardwareRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 214 | tc, err := m.(*tinkClientConfig).New() 215 | if err != nil { 216 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 217 | } 218 | 219 | c := tc.hardwareClient 220 | 221 | h, err := getHardware(ctx, c, d.Id()) 222 | if err != nil { 223 | return diagsFromErr(fmt.Errorf("checking if hardware %q exists: %w", d.Id(), err)) 224 | } 225 | 226 | if h == nil { 227 | d.SetId("") 228 | 229 | return nil 230 | } 231 | 232 | b, err := json.Marshal(pkg.HardwareWrapper{Hardware: h}) 233 | if err != nil { 234 | return diagsFromErr(fmt.Errorf("serializing received hardware entry failed: %w", err)) 235 | } 236 | 237 | if err := d.Set(dataAttribute, string(b)); err != nil { 238 | return diagsFromErr(fmt.Errorf("failed setting %q field: %w", dataAttribute, err)) 239 | } 240 | 241 | return nil 242 | } 243 | 244 | func resourceHardwareDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 245 | tc, err := m.(*tinkClientConfig).New() 246 | if err != nil { 247 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 248 | } 249 | 250 | c := tc.hardwareClient 251 | 252 | req := hardware.DeleteRequest{ 253 | Id: d.Id(), 254 | } 255 | 256 | if err := retryOnTransientError(func() error { 257 | _, err := c.Delete(ctx, &req) 258 | 259 | return err //nolint:wrapcheck 260 | }); err != nil { 261 | return diagsFromErr(fmt.Errorf("removing hardware failed: %w", err)) 262 | } 263 | 264 | return nil 265 | } 266 | -------------------------------------------------------------------------------- /tinkerbell/resource_hardware_test.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/google/uuid" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 11 | ) 12 | 13 | // From https://stackoverflow.com/a/21027407/2974814 14 | func newMAC(t *testing.T) string { 15 | buf := make([]byte, 6) 16 | if _, err := rand.Read(buf); err != nil { 17 | t.Fatalf("Generating MAC address: %v", err) 18 | } 19 | // Set the local bit 20 | buf[0] |= 2 21 | 22 | return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", buf[0], buf[1], buf[2], buf[3], buf[4], buf[5]) 23 | } 24 | 25 | func testAccHardwareConfig(uuid string, mac string) string { 26 | return fmt.Sprintf(` 27 | { 28 | "id": "%s", 29 | "metadata": { 30 | "facility": { 31 | "facility_code": "ewr1", 32 | "plan_slug": "c2.medium.x86", 33 | "plan_version_slug": "" 34 | }, 35 | "instance": {}, 36 | "state": "provisioning" 37 | }, 38 | "network": { 39 | "interfaces": [ 40 | { 41 | "dhcp": { 42 | "arch": "x86_64", 43 | "ip": { 44 | "address": "192.168.1.5", 45 | "gateway": "192.168.1.1", 46 | "netmask": "255.255.255.248" 47 | }, 48 | "mac": "%s" 49 | }, 50 | "netboot": { 51 | "allow_pxe": true, 52 | "allow_workflow": true 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | `, uuid, mac) 59 | } 60 | 61 | func testAccHardware(data, name string) string { 62 | return fmt.Sprintf(` 63 | resource "tinkerbell_hardware" "%s" { 64 | data = < 36 | actions: 37 | - name: "disk-wipe" 38 | image: disk-wipe 39 | timeout: 90 40 | - name: "disk-partition" 41 | image: disk-partition 42 | timeout: 600 43 | environment: 44 | MIRROR_HOST: 45 | volumes: 46 | - /statedir:/statedir 47 | - name: "install-root-fs" 48 | image: install-root-fs 49 | timeout: 600 50 | - name: "install-grub" 51 | image: install-grub 52 | timeout: 600 53 | volumes: 54 | - /statedir:/statedir 55 | `, timeout) 56 | } 57 | 58 | func TestAccTemplate_create(t *testing.T) { 59 | t.Parallel() 60 | 61 | name := newUUID(t) 62 | 63 | resource.Test(t, resource.TestCase{ 64 | PreCheck: func() { testAccPreCheck(t) }, 65 | Providers: testAccProviders, 66 | Steps: []resource.TestStep{ 67 | { 68 | Config: testAccTemplate(name, testAccTemplateContent(1)), 69 | }, 70 | }, 71 | }) 72 | } 73 | 74 | func TestAccTemplate_detectChanges(t *testing.T) { 75 | t.Parallel() 76 | 77 | name := newUUID(t) 78 | 79 | resource.Test(t, resource.TestCase{ 80 | PreCheck: func() { testAccPreCheck(t) }, 81 | Providers: testAccProviders, 82 | Steps: []resource.TestStep{ 83 | { 84 | Config: testAccTemplate(name, testAccTemplateContent(1)), 85 | }, 86 | { 87 | Config: testAccTemplate(name, testAccTemplateContent(2)), 88 | ExpectNonEmptyPlan: true, 89 | PlanOnly: true, 90 | }, 91 | }, 92 | }) 93 | } 94 | 95 | func TestAccTemplate_update(t *testing.T) { 96 | t.Parallel() 97 | 98 | name := newUUID(t) 99 | 100 | resource.Test(t, resource.TestCase{ 101 | PreCheck: func() { testAccPreCheck(t) }, 102 | Providers: testAccProviders, 103 | Steps: []resource.TestStep{ 104 | { 105 | Config: testAccTemplate(name, testAccTemplateContent(1)), 106 | }, 107 | { 108 | Config: testAccTemplate(name, testAccTemplateContent(2)), 109 | }, 110 | }, 111 | }) 112 | } 113 | 114 | func TestAccTemplate_validateData(t *testing.T) { 115 | t.Parallel() 116 | 117 | name := newUUID(t) 118 | 119 | resource.Test(t, resource.TestCase{ 120 | PreCheck: func() { testAccPreCheck(t) }, 121 | Providers: testAccProviders, 122 | Steps: []resource.TestStep{ 123 | { 124 | Config: testAccTemplate(name, `"foo`), 125 | ExpectError: regexp.MustCompile(`parsing template`), 126 | }, 127 | { 128 | Config: testAccTemplate(name, `foo: bar`), 129 | ExpectError: regexp.MustCompile(`parsing template`), 130 | }, 131 | }, 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /tinkerbell/resource_workflow.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | 12 | "github.com/tinkerbell/tink/protos/workflow" 13 | ) 14 | 15 | func resourceWorkflow() *schema.Resource { 16 | return &schema.Resource{ 17 | CreateContext: resourceWorkflowCreate, 18 | ReadContext: resourceWorkflowRead, 19 | DeleteContext: resourceWorkflowDelete, 20 | Schema: map[string]*schema.Schema{ 21 | "hardwares": { 22 | Type: schema.TypeString, 23 | Required: true, 24 | ForceNew: true, 25 | ValidateDiagFunc: validateNotEmpty, 26 | DiffSuppressFunc: suppressEquivalentJSONDiffs, 27 | }, 28 | "template": { 29 | Type: schema.TypeString, 30 | Required: true, 31 | ForceNew: true, 32 | ValidateDiagFunc: validateNotEmpty, 33 | }, 34 | }, 35 | } 36 | } 37 | 38 | func resourceWorkflowCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 39 | tc, err := m.(*tinkClientConfig).New() 40 | if err != nil { 41 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 42 | } 43 | 44 | c := tc.workflowClient 45 | 46 | req := workflow.CreateRequest{ 47 | Template: d.Get("template").(string), 48 | Hardware: d.Get("hardwares").(string), 49 | } 50 | 51 | res, err := c.CreateWorkflow(ctx, &req) 52 | if err != nil { 53 | return diagsFromErr(fmt.Errorf("creating workflow: %w", err)) 54 | } 55 | 56 | d.SetId(res.Id) 57 | 58 | return nil 59 | } 60 | 61 | func getWorkflow(ctx context.Context, c workflow.WorkflowServiceClient, uuid string) (*workflow.Workflow, error) { 62 | list, err := c.ListWorkflows(ctx, &workflow.Empty{}) 63 | if err != nil { 64 | return nil, fmt.Errorf("getting all workflow entries: %w", err) 65 | } 66 | 67 | for { 68 | wf, err := list.Recv() 69 | if err != nil { 70 | if errors.Is(err, io.EOF) { 71 | break 72 | } 73 | 74 | return nil, fmt.Errorf("receiving workflow entry: %w", err) 75 | } 76 | 77 | if wf == nil { 78 | return nil, fmt.Errorf("received empty workflow entry: %w", err) 79 | } 80 | 81 | if wf.GetId() == uuid { 82 | return wf, nil 83 | } 84 | } 85 | 86 | return nil, nil 87 | } 88 | 89 | func resourceWorkflowRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 90 | tc, err := m.(*tinkClientConfig).New() 91 | if err != nil { 92 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 93 | } 94 | 95 | c := tc.workflowClient 96 | 97 | wf, err := getWorkflow(ctx, c, d.Id()) 98 | if err != nil { 99 | return diagsFromErr(fmt.Errorf("getting workflow %q: %w", d.Id(), err)) 100 | } 101 | 102 | if wf == nil { 103 | d.SetId("") 104 | 105 | return nil 106 | } 107 | 108 | return nil 109 | } 110 | 111 | func resourceWorkflowDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 112 | tc, err := m.(*tinkClientConfig).New() 113 | if err != nil { 114 | return diagsFromErr(fmt.Errorf("creating Tink client: %w", err)) 115 | } 116 | 117 | c := tc.workflowClient 118 | 119 | wf, err := getWorkflow(ctx, c, d.Id()) 120 | if err != nil { 121 | return diagsFromErr(fmt.Errorf("getting workflow %q: %w", d.Id(), err)) 122 | } 123 | 124 | if wf == nil { 125 | d.SetId("") 126 | 127 | return nil 128 | } 129 | 130 | req := workflow.GetRequest{ 131 | Id: d.Id(), 132 | } 133 | 134 | if err := retryOnTransientError(func() error { 135 | _, err := c.DeleteWorkflow(ctx, &req) 136 | 137 | return err //nolint:wrapcheck 138 | }); err != nil { 139 | return diagsFromErr(fmt.Errorf("removing workflow %q: %w", d.Id(), err)) 140 | } 141 | 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /tinkerbell/resource_workflow_test.go: -------------------------------------------------------------------------------- 1 | package tinkerbell 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 8 | ) 9 | 10 | func testAccWorkflow(t *testing.T, id int) string { 11 | name := newUUID(t) 12 | rMAC := newMAC(t) 13 | resourceName := fmt.Sprintf("foo%d", id) 14 | 15 | return fmt.Sprintf(` 16 | %s 17 | 18 | %s 19 | 20 | resource "tinkerbell_workflow" "%s" { 21 | template = tinkerbell_template.a%s.id 22 | hardwares = <