├── .env.example ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ ├── stale.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── client └── client_test.go ├── cloudconnexa ├── data_source_connector.go ├── data_source_host.go ├── data_source_ip_service.go ├── data_source_network.go ├── data_source_network_routes.go ├── data_source_user.go ├── data_source_user_group.go ├── data_source_vpn_region.go ├── provider.go ├── provider_test.go ├── resource_connector.go ├── resource_connector_test.go ├── resource_dns_record.go ├── resource_dns_record_test.go ├── resource_host.go ├── resource_network.go ├── resource_route.go ├── resource_route_test.go ├── resource_service.go ├── resource_service_test.go ├── resource_user.go ├── resource_user_group.go ├── resource_user_group_test.go └── resource_user_test.go ├── docs ├── data-sources │ ├── connector.md │ ├── host.md │ ├── network.md │ ├── network_routes.md │ ├── user.md │ ├── user_group.md │ └── vpn_region.md ├── index.md └── resources │ ├── connector.md │ ├── dns_record.md │ ├── host.md │ ├── network.md │ ├── route.md │ └── user.md ├── e2e ├── integration_test.go └── setup │ ├── ec2.tf │ ├── main.tf │ └── user_data.sh.tpl ├── example ├── backend.tf ├── connectors.tf ├── hosts.tf ├── networks.tf ├── provider.tf ├── routes.tf ├── services.tf ├── user_groups.tf ├── users.tf └── variables.tf ├── go.mod ├── go.sum ├── main.go ├── templates ├── data-sources │ ├── host.md │ ├── network.md │ ├── network_routes.md │ └── user.md ├── index.md.tmpl └── resources │ ├── connector.md │ ├── dns_record.md │ ├── host.md │ ├── network.md │ ├── route.md │ └── user.md └── terraform-registry-manifest.json /.env.example: -------------------------------------------------------------------------------- 1 | OVPN_HOST= 2 | CLOUDCONNEXA_CLIENT_ID= 3 | CLOUDCONNEXA_CLIENT_SECRET= 4 | TF_ACC=0 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @OpenVPN/openvpn-cloud-terraform-project-admins 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | ignore: 10 | - dependency-name: "github.com/hashicorp/go-hclog" 11 | - dependency-name: "golang.org/x/tools" 12 | - dependency-name: "google.golang.org/grpc" 13 | schedule: 14 | interval: "daily" 15 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: golangci-lint 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: [ main ] 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | lint: 15 | name: lint 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version-file: "go.mod" 23 | cache: true 24 | 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v5 27 | with: 28 | version: latest 29 | -------------------------------------------------------------------------------- /.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 (hashicorp/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 | permissions: 19 | contents: write 20 | 21 | jobs: 22 | goreleaser: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Unshallow 29 | run: git fetch --prune --unshallow 30 | 31 | - uses: actions/setup-go@v5 32 | with: 33 | go-version-file: "go.mod" 34 | cache: true 35 | 36 | - name: Import GPG key 37 | uses: crazy-max/ghaction-import-gpg@v6 38 | id: import_gpg 39 | with: 40 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 41 | passphrase: ${{ secrets.PASSPHRASE }} 42 | 43 | - name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@v5.0.0 45 | with: 46 | version: latest 47 | args: release --clean 48 | env: 49 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Stale issues and pull requests" 2 | on: 3 | schedule: 4 | - cron: "40 17 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | days-before-stale: 720 14 | days-before-close: 30 15 | exempt-issue-labels: "needs-triage" 16 | exempt-pr-labels: "needs-triage" 17 | operations-per-run: 150 18 | stale-issue-label: "stale" 19 | stale-issue-message: | 20 | Marking this issue as stale due to inactivity. This helps our maintainers find and focus on the active issues. If this issue receives no comments in the next 30 days it will automatically be closed. Maintainers can also remove the stale label. 21 | If this issue was automatically closed and you feel this issue should be reopened, we encourage creating a new issue linking back to this one for added context. Thank you! 22 | stale-pr-label: "stale" 23 | stale-pr-message: | 24 | Marking this pull request as stale due to inactivity. This helps our maintainers find and focus on the active pull requests. If this pull request receives no comments in the next 30 days it will automatically be closed. Maintainers can also remove the stale label. 25 | If this pull request was automatically closed and you feel this pull request should be reopened, we encourage creating a new pull request linking back to this one for added context. Thank you! 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | paths-ignore: 7 | - "README.md" 8 | push: 9 | branches: 10 | - main 11 | paths-ignore: 12 | - "README.md" 13 | # We test at a regular interval to ensure we are alerted to something breaking due 14 | # to an API change, even if the code did not change. 15 | schedule: 16 | - cron: "0 0 * * *" 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | jobs: 21 | # ensure the code builds... 22 | build: 23 | name: Build 24 | runs-on: ubuntu-latest 25 | timeout-minutes: 5 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-go@v5 29 | with: 30 | go-version-file: "go.mod" 31 | cache: true 32 | - name: Get dependencies 33 | run: | 34 | go mod download 35 | - name: Build 36 | run: | 37 | go build -v . 38 | # run acceptance tests in a matrix with Terraform core versions 39 | test: 40 | name: Matrix Test 41 | needs: build 42 | runs-on: ubuntu-latest 43 | timeout-minutes: 15 44 | strategy: 45 | fail-fast: false 46 | matrix: 47 | terraform: 48 | - "1.2.*" 49 | - "1.3.*" 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: actions/setup-go@v5 53 | with: 54 | go-version-file: "go.mod" 55 | cache: true 56 | 57 | - uses: hashicorp/setup-terraform@v3 58 | with: 59 | terraform_version: ${{ matrix.terraform }} 60 | terraform_wrapper: false 61 | 62 | - name: Get dependencies 63 | run: | 64 | go mod download 65 | - name: TF acceptance tests 66 | timeout-minutes: 10 67 | env: 68 | TF_ACC: "1" 69 | CLOUDCONNEXA_TEST_ORGANIZATION: "terraform-community" 70 | CLOUDCONNEXA_CLIENT_ID: ${{ secrets.CVPN_CLIENT_ID }} 71 | CLOUDCONNEXA_CLIENT_SECRET: ${{ secrets.CVPN_CLIENT_SECRET }} 72 | run: | 73 | go test -v -cover ./cloudconnexa 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | dist/ 3 | modules-dev/ 4 | /pkg/ 5 | .vagrant/ 6 | .terraform/ 7 | .idea 8 | 9 | *.dll 10 | *.exe 11 | .DS_Store 12 | example.tf 13 | terraform.tfplan 14 | terraform.tfstate 15 | *.backup 16 | ./*.tfstate 17 | *.log 18 | *.bak 19 | *~ 20 | .*.swp 21 | *.iml 22 | *.test 23 | *.iml 24 | 25 | # Binaries 26 | terraform-provider-cloudconnexa 27 | main 28 | 29 | # Keep windows files with windows line endings 30 | *.winfile eol=crlf 31 | 32 | .terraform** 33 | terraform.tfstate** 34 | *.tfvars 35 | 36 | *.env 37 | /.vscode 38 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | - linters: 4 | - errcheck 5 | text: "Error return value of `d.Set` is not checked" 6 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | 4 | before: 5 | hooks: 6 | - go mod tidy 7 | builds: 8 | - env: 9 | # goreleaser does not work with CGO, it could also complicate 10 | # usage by users in CI/CD systems like Terraform Cloud where 11 | # they are unable to install libraries. 12 | - CGO_ENABLED=0 13 | mod_timestamp: "{{ .CommitTimestamp }}" 14 | flags: 15 | - -trimpath 16 | ldflags: 17 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}" 18 | goos: 19 | - freebsd 20 | - windows 21 | - linux 22 | - darwin 23 | goarch: 24 | - amd64 25 | - "386" 26 | - arm 27 | - arm64 28 | ignore: 29 | - goos: darwin 30 | goarch: "386" 31 | binary: "{{ .ProjectName }}_v{{ .Version }}" 32 | archives: 33 | - format: zip 34 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 35 | checksum: 36 | extra_files: 37 | - glob: "terraform-registry-manifest.json" 38 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 39 | name_template: "{{ .ProjectName }}_{{ .Version }}_SHA256SUMS" 40 | algorithm: sha256 41 | signs: 42 | - artifacts: checksum 43 | args: 44 | # if you are using this in a GitHub action or some other automated pipeline, you 45 | # need to pass the batch flag to indicate its not interactive. 46 | - "--batch" 47 | - "--local-user" 48 | - "{{ .Env.GPG_FINGERPRINT }}" 49 | - "--output" 50 | - "${signature}" 51 | - "--detach-sign" 52 | - "${artifact}" 53 | release: 54 | extra_files: 55 | - glob: "terraform-registry-manifest.json" 56 | name_template: "{{ .ProjectName }}_{{ .Version }}_manifest.json" 57 | # Manually examine the release before it goes live: 58 | draft: true 59 | changelog: 60 | skip: true 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | HOSTNAME=cloudconnexa.dev 2 | NAMESPACE=openvpn 3 | NAME=cloudconnexa 4 | VERSION=0.0.12 5 | BINARY=terraform-provider-${NAME} 6 | OS_ARCH=darwin_arm64 7 | 8 | default: install 9 | 10 | build: 11 | go build -o ${BINARY} 12 | 13 | release: 14 | goreleaser release --clean --snapshot --skip-publish --skip-sign 15 | 16 | install: build 17 | mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} 18 | mv ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} 19 | 20 | lint: 21 | golangci-lint run ./... 22 | 23 | test: 24 | go test -i $(TEST) || exit 1 25 | echo $(TEST) | xargs -t -n4 go test $(TESTARGS) -timeout=30s -parallel=4 26 | 27 | testacc: 28 | TF_ACC=1 go test $(TEST) -v $(TESTARGS) -timeout 120m 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider Cloud Connexa 2 | 3 | 4 | Terraform 5 | 6 | 7 | 8 | ANNA Money 9 | 10 | 11 | 12 | OpenVPN 13 | 14 | 15 | - [Website Cloud Connexa](https://openvpn.net/cloud-vpn/?utm_source=terraform&utm_medium=docs) 16 | - [Terraform Registry](https://registry.terraform.io/providers/OpenVPN/openvpn-cloud/latest) 17 | 18 | # Repository Name Change Notification 19 | 20 | We are excited to announce that our repository has been renamed to better align with our product's name and has moved to a new location. 21 | 22 | ## New Repository Name and URL 23 | The repository is now named **terraform-provider-cloudconnexa** and be located at [https://github.com/OpenVPN/terraform-provider-cloudconnexa](https://github.com/OpenVPN/terraform-provider-cloudconnexa). 24 | We encourage all users to update their terraform files and local configurations to reflect this change. 25 | 26 | ## Important Updates 27 | - **Resource Updates:** All resource names have been updated to align with the new repository. Please ensure to update your resources accordingly. 28 | - **Provider Updates:** The renamed provider has also been updated and is available in the [Terraform Registry](https://registry.terraform.io/providers/OpenVPN/cloudconnexa/latest). Please update your configurations to use the new provider by referencing its new address in your Terraform configurations. 29 | - **New Features and Updates:** All future features and updates will be released under the new provider name. We recommend migrating to the new provider for the latest functionalities and improvements. 30 | 31 | Thank you for your continued support and cooperation. If you have any questions or need assistance with the migration, please [open an issue](https://github.com/OpenVPN/terraform-provider-cloudconnexa/issues/new). 32 | 33 | ## Description 34 | 35 | The Terraform provider for [Cloud Connexa](https://openvpn.net/cloud-vpn/?utm_source=terraform&utm_medium=docs) allows teams to configure and update Cloud Connexa project parameters via their command line. 36 | 37 | ## Maintainers 38 | 39 | This provider plugin is maintained by: 40 | 41 | - OpenVPN team at [Cloud Connexa](https://openvpn.net/cloud-vpn/?utm_source=terraform&utm_medium=docs) 42 | - SRE Team at [ANNA Money](https://anna.money/?utm_source=terraform&utm_medium=referral&utm_campaign=docs) / [GitHub ANNA Money](http://github.com/anna-money/) 43 | - [@patoarvizu](https://github.com/patoarvizu) 44 | 45 | ## Requirements 46 | 47 | - [Terraform](https://www.terraform.io/downloads.html) 0.12.x 48 | - [Go](https://golang.org/doc/install) 1.18 (to build the provider plugin) 49 | 50 | ## Building The Provider 51 | 52 | Clone repository to: `$GOPATH/src/github.com/OpenVPN/terraform-provider-openvpn-cloud` 53 | 54 | ```sh 55 | mkdir -p $GOPATH/src/github.com/OpenVPN; cd $GOPATH/src/github.com/OpenVPN 56 | git clone git@github.com:OpenVPN/terraform-provider-openvpn-cloud.git 57 | ``` 58 | 59 | Enter the provider directory and build the provider 60 | 61 | ```sh 62 | cd $GOPATH/src/github.com/OpenVPN/terraform-provider-openvpn-cloud 63 | make build 64 | ``` 65 | 66 | ## Developing the Provider 67 | 68 | If you wish to work on the provider, you'll first need [Go](http://www.golang.org) installed on your machine (version 1.18+ is _required_). You'll also need to correctly setup a [GOPATH](http://golang.org/doc/code.html#GOPATH), as well as adding `$GOPATH/bin` to your `$PATH`. 69 | 70 | To compile the provider, run `make build`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory. 71 | 72 | ```sh 73 | make bin 74 | ... 75 | $GOPATH/bin/terraform-provider-openvpn-cloud 76 | ... 77 | ``` 78 | 79 | In order to test the provider, you can simply run `make test`. 80 | 81 | ```sh 82 | make test 83 | ``` 84 | 85 | In order to run the full suite of Acceptance tests, run `make testacc`. 86 | 87 | _Note:_ Acceptance tests create real resources, and often cost money to run. 88 | 89 | ```sh 90 | make testacc 91 | ``` 92 | 93 | _**Please note:** This provider, like Cloud Connexa API, is in beta status. Report any problems via issue in this repo._ 94 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func validateEnvVars(t *testing.T) { 14 | validateEnvVar(t, HostEnvVar) 15 | validateEnvVar(t, ClientIDEnvVar) 16 | validateEnvVar(t, ClientSecretEnvVar) 17 | } 18 | 19 | func validateEnvVar(t *testing.T, envVar string) { 20 | fmt.Println(os.Getenv(envVar)) 21 | require.NotEmptyf(t, os.Getenv(envVar), "%s must be set", envVar) 22 | } 23 | 24 | const ( 25 | HostEnvVar = "OVPN_HOST" 26 | ClientIDEnvVar = "CLOUDCONNEXA_CLIENT_ID" 27 | ClientSecretEnvVar = "CLOUDCONNEXA_CLIENT_SECRET" 28 | ) 29 | 30 | func TestNewClient(t *testing.T) { 31 | c := setUpClient(t) 32 | assert.NotEmpty(t, c.Token) 33 | } 34 | 35 | func setUpClient(t *testing.T) *cloudconnexa.Client { 36 | validateEnvVars(t) 37 | var err error 38 | client, err := cloudconnexa.NewClient(os.Getenv(HostEnvVar), os.Getenv(ClientIDEnvVar), os.Getenv(ClientSecretEnvVar)) 39 | require.NoError(t, err) 40 | return client 41 | } 42 | 43 | func TestListNetworks(t *testing.T) { 44 | c := setUpClient(t) 45 | response, err := c.Networks.GetByPage(0, 10) 46 | require.NoError(t, err) 47 | fmt.Printf("found %d networks\n", len(response.Content)) 48 | } 49 | 50 | func TestListConnectors(t *testing.T) { 51 | c := setUpClient(t) 52 | response, err := c.Connectors.GetByPage(0, 10) 53 | require.NoError(t, err) 54 | fmt.Printf("found %d connectors\n", len(response.Content)) 55 | } 56 | 57 | func TestCreateNetwork(t *testing.T) { 58 | c := setUpClient(t) 59 | connector := cloudconnexa.NetworkConnector{ 60 | Description: "test", 61 | Name: "test", 62 | VpnRegionId: "it-mxp", 63 | } 64 | route := cloudconnexa.Route{ 65 | Description: "test", 66 | Type: "IP_V4", 67 | Subnet: "10.189.253.64/30", 68 | } 69 | network := cloudconnexa.Network{ 70 | Description: "test", 71 | Egress: false, 72 | Name: "test", 73 | InternetAccess: "LOCAL", 74 | Connectors: []cloudconnexa.NetworkConnector{connector}, 75 | } 76 | response, err := c.Networks.Create(network) 77 | require.NoError(t, err) 78 | fmt.Printf("created %s network\n", response.Id) 79 | test, err := c.Routes.Create(response.Id, route) 80 | require.NoError(t, err) 81 | fmt.Printf("created %s route\n", test.Id) 82 | serviceConfig := cloudconnexa.IPServiceConfig{ 83 | ServiceTypes: []string{"ANY"}, 84 | } 85 | ipServiceRoute := cloudconnexa.IPServiceRoute{ 86 | Description: "test", 87 | Value: "10.189.253.64/30", 88 | } 89 | service := cloudconnexa.IPService{ 90 | Name: "test", 91 | Description: "test", 92 | NetworkItemId: response.Id, 93 | Type: "IP_SOURCE", 94 | NetworkItemType: "NETWORK", 95 | Config: &serviceConfig, 96 | Routes: []*cloudconnexa.IPServiceRoute{&ipServiceRoute}, 97 | } 98 | s, err := c.IPServices.Create(&service) 99 | require.NoError(t, err) 100 | fmt.Printf("created %s service\n", s.Id) 101 | err = c.Networks.Delete(response.Id) 102 | require.NoError(t, err) 103 | fmt.Printf("deleted %s network\n", response.Id) 104 | } 105 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_connector.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 11 | ) 12 | 13 | func dataSourceConnector() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use an `cloudconnexa_connector` data source to read an existing Cloud Connexa connector.", 16 | ReadContext: dataSourceConnectorRead, 17 | Schema: map[string]*schema.Schema{ 18 | "name": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | Description: "The name of the connector.", 22 | }, 23 | "network_item_id": { 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Description: "The id of the network or host with which the connector is associated.", 27 | }, 28 | "network_item_type": { 29 | Type: schema.TypeString, 30 | Computed: true, 31 | Description: "The network object type of the connector. This typically will be set to either `NETWORK` or `HOST`.", 32 | }, 33 | "vpn_region_id": { 34 | Type: schema.TypeString, 35 | Computed: true, 36 | Description: "The id of the region where the connector is deployed.", 37 | }, 38 | "ip_v4_address": { 39 | Type: schema.TypeString, 40 | Computed: true, 41 | Description: "The IPV4 address of the connector.", 42 | }, 43 | "ip_v6_address": { 44 | Type: schema.TypeString, 45 | Computed: true, 46 | Description: "The IPV6 address of the connector.", 47 | }, 48 | "profile": { 49 | Type: schema.TypeString, 50 | Computed: true, 51 | Description: "OpenVPN profile", 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | func dataSourceConnectorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 58 | c := m.(*cloudconnexa.Client) 59 | var diags diag.Diagnostics 60 | connector, err := c.Connectors.GetByName(d.Get("name").(string)) 61 | if err != nil { 62 | return append(diags, diag.FromErr(err)...) 63 | } 64 | d.Set("name", connector.Name) 65 | d.Set("network_item_id", connector.NetworkItemId) 66 | d.Set("network_item_type", connector.NetworkItemType) 67 | d.Set("vpn_region_id", connector.VpnRegionId) 68 | d.Set("ip_v4_address", connector.IPv4Address) 69 | d.Set("ip_v6_address", connector.IPv6Address) 70 | profile, err := c.Connectors.GetProfile(connector.Id) 71 | if err != nil { 72 | return append(diags, diag.FromErr(err)...) 73 | } 74 | d.Set("profile", profile) 75 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 76 | return diags 77 | } 78 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_host.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 11 | ) 12 | 13 | func dataSourceHost() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use an `cloudconnexa_host` data source to read an existing Cloud Connexa connector.", 16 | ReadContext: dataSourceHostRead, 17 | Schema: map[string]*schema.Schema{ 18 | "name": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | Description: "The name of the host.", 22 | }, 23 | "internet_access": { 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Description: "The type of internet access provided.", 27 | }, 28 | "system_subnets": { 29 | Type: schema.TypeList, 30 | Computed: true, 31 | Elem: &schema.Schema{ 32 | Type: schema.TypeString, 33 | }, 34 | Description: "The IPV4 and IPV6 subnets automatically assigned to this host.", 35 | }, 36 | "connectors": { 37 | Type: schema.TypeList, 38 | Computed: true, 39 | Description: "The list of connectors to be associated with this host.", 40 | Elem: &schema.Resource{ 41 | Schema: map[string]*schema.Schema{ 42 | "id": { 43 | Type: schema.TypeString, 44 | Computed: true, 45 | Description: "The connector id.", 46 | }, 47 | "name": { 48 | Type: schema.TypeString, 49 | Computed: true, 50 | Description: "The connector name.", 51 | }, 52 | "network_item_id": { 53 | Type: schema.TypeString, 54 | Computed: true, 55 | Description: "The id of the host with which the connector is associated.", 56 | }, 57 | "network_item_type": { 58 | Type: schema.TypeString, 59 | Computed: true, 60 | Description: "The network object type of the connector. This typically will be set to `HOST`.", 61 | }, 62 | "vpn_region_id": { 63 | Type: schema.TypeString, 64 | Computed: true, 65 | Description: "The id of the region where the connector is deployed.", 66 | }, 67 | "ip_v4_address": { 68 | Type: schema.TypeString, 69 | Computed: true, 70 | Description: "The IPV4 address of the connector.", 71 | }, 72 | "ip_v6_address": { 73 | Type: schema.TypeString, 74 | Computed: true, 75 | Description: "The IPV6 address of the connector.", 76 | }, 77 | }, 78 | }, 79 | }, 80 | }, 81 | } 82 | } 83 | 84 | func dataSourceHostRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 85 | c := m.(*cloudconnexa.Client) 86 | var diags diag.Diagnostics 87 | host, err := c.Hosts.GetByName(d.Get("name").(string)) 88 | if err != nil { 89 | return append(diags, diag.FromErr(err)...) 90 | } 91 | d.Set("name", host.Name) 92 | d.Set("internet_access", host.InternetAccess) 93 | d.Set("system_subnets", host.SystemSubnets) 94 | d.Set("connectors", getConnectorsSlice(&host.Connectors)) 95 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 96 | return diags 97 | } 98 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_ip_service.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 8 | ) 9 | 10 | func dataSourceIPService() *schema.Resource { 11 | return &schema.Resource{ 12 | ReadContext: dataSourceIPServiceRead, 13 | Schema: map[string]*schema.Schema{ 14 | "id": { 15 | Type: schema.TypeString, 16 | Required: true, 17 | }, 18 | "name": { 19 | Type: schema.TypeString, 20 | Computed: true, 21 | }, 22 | "description": { 23 | Type: schema.TypeString, 24 | Computed: true, 25 | }, 26 | "type": { 27 | Type: schema.TypeString, 28 | Computed: true, 29 | }, 30 | "routes": { 31 | Type: schema.TypeList, 32 | Computed: true, 33 | Elem: &schema.Schema{ 34 | Type: schema.TypeString, 35 | }, 36 | }, 37 | "config": { 38 | Type: schema.TypeList, 39 | Computed: true, 40 | Elem: resourceServiceConfig(), 41 | }, 42 | "network_item_type": { 43 | Type: schema.TypeString, 44 | Computed: true, 45 | }, 46 | "network_item_id": { 47 | Type: schema.TypeString, 48 | Computed: true, 49 | }, 50 | }, 51 | } 52 | } 53 | 54 | func dataSourceIPServiceRead(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 55 | c := i.(*cloudconnexa.Client) 56 | service, err := c.IPServices.Get( 57 | data.Id(), 58 | ) 59 | 60 | if err != nil { 61 | return diag.FromErr(err) 62 | } 63 | setResourceData(data, service) 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_network.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | func dataSourceNetwork() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use a `cloudconnexa_network` data source to read an Cloud Connexa network.", 16 | ReadContext: dataSourceNetworkRead, 17 | Schema: map[string]*schema.Schema{ 18 | "network_id": { 19 | Type: schema.TypeString, 20 | Computed: true, 21 | Description: "The network ID.", 22 | }, 23 | "name": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | Description: "The network name.", 27 | }, 28 | "egress": { 29 | Type: schema.TypeBool, 30 | Computed: true, 31 | Description: "Boolean to indicate whether this network provides an egress or not.", 32 | }, 33 | "internet_access": { 34 | Type: schema.TypeString, 35 | Computed: true, 36 | Description: "The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`.", 37 | }, 38 | "system_subnets": { 39 | Type: schema.TypeList, 40 | Computed: true, 41 | Elem: &schema.Schema{ 42 | Type: schema.TypeString, 43 | }, 44 | Description: "The IPV4 and IPV6 subnets automatically assigned to this network.", 45 | }, 46 | "routes": { 47 | Type: schema.TypeList, 48 | Computed: true, 49 | Description: "The routes associated with this network.", 50 | Elem: &schema.Resource{ 51 | Schema: map[string]*schema.Schema{ 52 | "id": { 53 | Type: schema.TypeString, 54 | Computed: true, 55 | Description: "The route id.", 56 | }, 57 | "type": { 58 | Type: schema.TypeString, 59 | Computed: true, 60 | Description: "The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`.", 61 | }, 62 | "subnet": { 63 | Type: schema.TypeString, 64 | Computed: true, 65 | Description: "The value of the route, either an IPV4 address, an IPV6 address, or a DNS hostname.", 66 | }, 67 | }, 68 | }, 69 | }, 70 | "connectors": { 71 | Type: schema.TypeList, 72 | Computed: true, 73 | Description: "The list of connectors associated with this network.", 74 | Elem: &schema.Resource{ 75 | Schema: map[string]*schema.Schema{ 76 | "id": { 77 | Type: schema.TypeString, 78 | Computed: true, 79 | Description: "The connector id.", 80 | }, 81 | "name": { 82 | Type: schema.TypeString, 83 | Computed: true, 84 | Description: "The connector name.", 85 | }, 86 | "network_item_id": { 87 | Type: schema.TypeString, 88 | Computed: true, 89 | Description: "The id of the network with which the connector is associated.", 90 | }, 91 | "network_item_type": { 92 | Type: schema.TypeString, 93 | Computed: true, 94 | Description: "The network object type of the connector. This typically will be set to `NETWORK`.", 95 | }, 96 | "vpn_region_id": { 97 | Type: schema.TypeString, 98 | Computed: true, 99 | Description: "The id of the region where the connector is deployed.", 100 | }, 101 | "ip_v4_address": { 102 | Type: schema.TypeString, 103 | Computed: true, 104 | Description: "The IPV4 address of the connector.", 105 | }, 106 | "ip_v6_address": { 107 | Type: schema.TypeString, 108 | Computed: true, 109 | Description: "The IPV6 address of the connector.", 110 | }, 111 | }, 112 | }, 113 | }, 114 | }, 115 | } 116 | } 117 | 118 | func dataSourceNetworkRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 119 | c := m.(*cloudconnexa.Client) 120 | var diags diag.Diagnostics 121 | networkName := d.Get("name").(string) 122 | network, err := c.Networks.GetByName(networkName) 123 | if err != nil { 124 | return append(diags, diag.FromErr(err)...) 125 | } 126 | if network == nil { 127 | return append(diags, diag.Errorf("Network with name %s was not found", networkName)...) 128 | } 129 | d.Set("network_id", network.Id) 130 | d.Set("name", network.Name) 131 | d.Set("description", network.Description) 132 | d.Set("egress", network.Egress) 133 | d.Set("internet_access", network.InternetAccess) 134 | d.Set("system_subnets", network.SystemSubnets) 135 | d.Set("routes", getRoutesSlice(&network.Routes)) 136 | //d.Set("connectors", getConnectorsSlice(&network.Connectors)) 137 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 138 | return diags 139 | } 140 | 141 | func getRoutesSlice(networkRoutes *[]cloudconnexa.Route) []interface{} { 142 | routes := make([]interface{}, len(*networkRoutes)) 143 | for i, r := range *networkRoutes { 144 | route := make(map[string]interface{}) 145 | route["id"] = r.Id 146 | route["subnet"] = r.Subnet 147 | route["type"] = r.Type 148 | routes[i] = route 149 | } 150 | return routes 151 | } 152 | 153 | func getConnectorsSlice(connectors *[]cloudconnexa.Connector) []interface{} { 154 | conns := make([]interface{}, len(*connectors)) 155 | for i, c := range *connectors { 156 | connector := make(map[string]interface{}) 157 | connector["id"] = c.Id 158 | connector["name"] = c.Name 159 | connector["network_item_id"] = c.NetworkItemId 160 | connector["network_item_type"] = c.NetworkItemType 161 | connector["vpn_region_id"] = c.VpnRegionId 162 | connector["ip_v4_address"] = c.IPv4Address 163 | connector["ip_v6_address"] = c.IPv6Address 164 | conns[i] = connector 165 | } 166 | return conns 167 | } 168 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_network_routes.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | func dataSourceNetworkRoutes() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use an `cloudconnexa_network_routes` data source to read all the routes associated with an Cloud Connexa network.", 16 | ReadContext: dataSourceNetworkRoutesRead, 17 | Schema: map[string]*schema.Schema{ 18 | "network_item_id": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | Description: "The id of the Cloud Connexa network of the routes to be discovered.", 22 | }, 23 | "routes": { 24 | Type: schema.TypeList, 25 | Computed: true, 26 | Description: "The list of routes.", 27 | Elem: &schema.Resource{ 28 | Schema: map[string]*schema.Schema{ 29 | "id": { 30 | Type: schema.TypeString, 31 | Computed: true, 32 | Description: "The unique identifier of the route.", 33 | }, 34 | "type": { 35 | Type: schema.TypeString, 36 | Computed: true, 37 | Description: "The type of route. Valid values are `IP_V4`, `IP_V6`, and others.", 38 | }, 39 | "subnet": { 40 | Type: schema.TypeString, 41 | Computed: true, 42 | Description: "The subnet of the route.", 43 | }, 44 | "description": { 45 | Type: schema.TypeString, 46 | Computed: true, 47 | Description: "A description of the route.", 48 | }, 49 | }, 50 | }, 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | func dataSourceNetworkRoutesRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 57 | c := m.(*cloudconnexa.Client) 58 | var diags diag.Diagnostics 59 | 60 | networkId := d.Get("network_item_id").(string) 61 | routes, err := c.Routes.List(networkId) 62 | if err != nil { 63 | return append(diags, diag.FromErr(err)...) 64 | } 65 | 66 | configRoutes := make([]map[string]interface{}, len(routes)) 67 | for i, r := range routes { 68 | route := make(map[string]interface{}) 69 | route["id"] = r.Id 70 | route["type"] = r.Type 71 | route["subnet"] = r.Subnet 72 | route["description"] = r.Description 73 | configRoutes[i] = route 74 | } 75 | 76 | if err := d.Set("routes", configRoutes); err != nil { 77 | return append(diags, diag.FromErr(err)...) 78 | } 79 | 80 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 81 | return diags 82 | } 83 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_user.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | func dataSourceUser() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use a `cloudconnexa_user` data source to read a specific Cloud Connexa user.", 16 | ReadContext: dataSourceUserRead, 17 | Schema: map[string]*schema.Schema{ 18 | "user_id": { 19 | Type: schema.TypeString, 20 | Computed: true, 21 | Description: "The ID of this resource.", 22 | }, 23 | "username": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | Description: "The username of the user.", 27 | }, 28 | "role": { 29 | Type: schema.TypeString, 30 | Required: true, 31 | Description: "The type of user role. Valid values are `ADMIN`, `MEMBER`, or `OWNER`.", 32 | }, 33 | "email": { 34 | Type: schema.TypeString, 35 | Computed: true, 36 | Description: "The email address of the user.", 37 | }, 38 | "auth_type": { 39 | Type: schema.TypeString, 40 | Computed: true, 41 | Description: "The authentication type of the user.", 42 | }, 43 | "first_name": { 44 | Type: schema.TypeString, 45 | Computed: true, 46 | Description: "The user's first name.", 47 | }, 48 | "last_name": { 49 | Type: schema.TypeString, 50 | Computed: true, 51 | Description: "The user's last name.", 52 | }, 53 | "group_id": { 54 | Type: schema.TypeString, 55 | Computed: true, 56 | Description: "The user's group id.", 57 | }, 58 | "status": { 59 | Type: schema.TypeString, 60 | Computed: true, 61 | Description: "The user's status.", 62 | }, 63 | "devices": { 64 | Type: schema.TypeList, 65 | Computed: true, 66 | Description: "The list of user devices.", 67 | Elem: &schema.Resource{ 68 | Schema: map[string]*schema.Schema{ 69 | "id": { 70 | Type: schema.TypeString, 71 | Computed: true, 72 | Description: "The device's id.", 73 | }, 74 | "name": { 75 | Type: schema.TypeString, 76 | Computed: true, 77 | Description: "The device's name.", 78 | }, 79 | "description": { 80 | Type: schema.TypeString, 81 | Computed: true, 82 | Description: "The device's description.", 83 | }, 84 | "ip_v4_address": { 85 | Type: schema.TypeString, 86 | Computed: true, 87 | Description: "The device's IPV4 address.", 88 | }, 89 | "ip_v6_address": { 90 | Type: schema.TypeString, 91 | Computed: true, 92 | Description: "The device's IPV6 address.", 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | } 99 | } 100 | 101 | func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 102 | c := m.(*cloudconnexa.Client) 103 | var diags diag.Diagnostics 104 | userName := d.Get("username").(string) 105 | user, err := c.Users.Get(userName) 106 | if err != nil { 107 | return append(diags, diag.FromErr(err)...) 108 | } 109 | if user == nil { 110 | return append(diags, diag.Errorf("User with name %s was not found", userName)...) 111 | } 112 | 113 | d.Set("user_id", user.Id) 114 | d.Set("username", user.Username) 115 | d.Set("role", user.Role) 116 | d.Set("email", user.Email) 117 | d.Set("auth_type", user.AuthType) 118 | d.Set("first_name", user.FirstName) 119 | d.Set("last_name", user.LastName) 120 | d.Set("group_id", user.GroupId) 121 | d.Set("status", user.Status) 122 | d.Set("devices", getUserDevicesSlice(&user.Devices)) 123 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 124 | return diags 125 | } 126 | 127 | func getUserDevicesSlice(userDevices *[]cloudconnexa.Device) []interface{} { 128 | devices := make([]interface{}, len(*userDevices)) 129 | for i, d := range *userDevices { 130 | device := make(map[string]interface{}) 131 | device["id"] = d.Id 132 | device["name"] = d.Name 133 | device["description"] = d.Description 134 | device["ip_v4_address"] = d.IPv4Address 135 | device["ip_v6_address"] = d.IPv6Address 136 | devices[i] = device 137 | } 138 | return devices 139 | } 140 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_user_group.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | func dataSourceUserGroup() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use an `cloudconnexa_user_group` data source to read an Cloud Connexa user group.", 16 | ReadContext: dataSourceUserGroupRead, 17 | Schema: map[string]*schema.Schema{ 18 | "user_group_id": { 19 | Type: schema.TypeString, 20 | Computed: true, 21 | Description: "The user group ID.", 22 | }, 23 | "name": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | Description: "The user group name.", 27 | }, 28 | "vpn_region_ids": { 29 | Type: schema.TypeList, 30 | Computed: true, 31 | Elem: &schema.Schema{ 32 | Type: schema.TypeString, 33 | }, 34 | Description: "The list of VPN region IDs this user group is associated with.", 35 | }, 36 | "internet_access": { 37 | Type: schema.TypeString, 38 | Computed: true, 39 | Description: "The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`.", 40 | }, 41 | "max_device": { 42 | Type: schema.TypeInt, 43 | Computed: true, 44 | Description: "The maximum number of devices per user.", 45 | }, 46 | "system_subnets": { 47 | Type: schema.TypeList, 48 | Computed: true, 49 | Elem: &schema.Schema{ 50 | Type: schema.TypeString, 51 | }, 52 | Description: "The IPV4 and IPV6 addresses of the subnets associated with this user group.", 53 | }, 54 | }, 55 | } 56 | } 57 | 58 | func dataSourceUserGroupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 59 | c := m.(*cloudconnexa.Client) 60 | var diags diag.Diagnostics 61 | userGroupName := d.Get("name").(string) 62 | userGroup, err := c.UserGroups.GetByName(userGroupName) 63 | if err != nil { 64 | return append(diags, diag.FromErr(err)...) 65 | } 66 | if userGroup == nil { 67 | return append(diags, diag.Errorf("User group with name %s was not found", userGroupName)...) 68 | } 69 | d.Set("user_group_id", userGroup.ID) 70 | d.Set("name", userGroup.Name) 71 | d.Set("vpn_region_ids", userGroup.VpnRegionIds) 72 | d.Set("internet_access", userGroup.InternetAccess) 73 | d.Set("max_device", userGroup.MaxDevice) 74 | d.Set("system_subnets", userGroup.SystemSubnets) 75 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 76 | return diags 77 | } 78 | -------------------------------------------------------------------------------- /cloudconnexa/data_source_vpn_region.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | func dataSourceVpnRegion() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use a `cloudconnexa_vpn_region` data source to read an Cloud Connexa VPN region.", 16 | ReadContext: dataSourceVpnRegionRead, 17 | Schema: map[string]*schema.Schema{ 18 | "region_id": { 19 | Type: schema.TypeString, 20 | Required: true, 21 | Description: "The id of the region.", 22 | }, 23 | "continent": { 24 | Type: schema.TypeString, 25 | Computed: true, 26 | Description: "The continent of the region.", 27 | }, 28 | "country": { 29 | Type: schema.TypeString, 30 | Computed: true, 31 | Description: "The country of the region.", 32 | }, 33 | "country_iso": { 34 | Type: schema.TypeString, 35 | Computed: true, 36 | Description: "The ISO code of the country of the region.", 37 | }, 38 | "region_name": { 39 | Type: schema.TypeString, 40 | Computed: true, 41 | Description: "The name of the region.", 42 | }, 43 | }, 44 | } 45 | } 46 | 47 | func dataSourceVpnRegionRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 48 | c := m.(*cloudconnexa.Client) 49 | var diags diag.Diagnostics 50 | vpnRegionId := d.Get("region_id").(string) 51 | vpnRegion, err := c.VPNRegions.GetVpnRegion(vpnRegionId) 52 | if err != nil { 53 | return append(diags, diag.FromErr(err)...) 54 | } 55 | if vpnRegion == nil { 56 | return append(diags, diag.Errorf("VPN region with id %s was not found", vpnRegionId)...) 57 | } 58 | d.Set("region_id", vpnRegion.Id) 59 | d.Set("continent", vpnRegion.Continent) 60 | d.Set("country", vpnRegion.Country) 61 | d.Set("country_iso", vpnRegion.CountryISO) 62 | d.Set("region_name", vpnRegion.RegionName) 63 | d.SetId(strconv.FormatInt(time.Now().Unix(), 10)) 64 | return diags 65 | } 66 | -------------------------------------------------------------------------------- /cloudconnexa/provider.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | ) 11 | 12 | const ( 13 | ClientIDEnvVar = "CLOUDCONNEXA_CLIENT_ID" 14 | ClientSecretEnvVar = "CLOUDCONNEXA_CLIENT_SECRET" 15 | ) 16 | 17 | type Token struct { 18 | AccessToken string `json:"access_token"` 19 | } 20 | 21 | func Provider() *schema.Provider { 22 | return &schema.Provider{ 23 | Schema: map[string]*schema.Schema{ 24 | "client_id": { 25 | Description: "The authentication client_id used to connect to Cloud Connexa API. The value can be sourced from " + 26 | "the `CLOUDCONNEXA_CLIENT_ID` environment variable.", 27 | Type: schema.TypeString, 28 | Optional: true, 29 | Sensitive: true, 30 | DefaultFunc: schema.EnvDefaultFunc(ClientIDEnvVar, nil), 31 | }, 32 | "client_secret": { 33 | Description: "The authentication client_secret used to connect to Cloud Connexa API. The value can be sourced from " + 34 | "the `CLOUDCONNEXA_CLIENT_SECRET` environment variable.", 35 | Type: schema.TypeString, 36 | Optional: true, 37 | Sensitive: true, 38 | DefaultFunc: schema.EnvDefaultFunc(ClientSecretEnvVar, nil), 39 | }, 40 | "base_url": { 41 | Description: "The target Cloud Connexa Base API URL in the format `https://[companyName].api.openvpn.com`", 42 | Type: schema.TypeString, 43 | Required: true, 44 | }, 45 | }, 46 | ResourcesMap: map[string]*schema.Resource{ 47 | "cloudconnexa_network": resourceNetwork(), 48 | "cloudconnexa_connector": resourceConnector(), 49 | "cloudconnexa_route": resourceRoute(), 50 | "cloudconnexa_dns_record": resourceDnsRecord(), 51 | "cloudconnexa_user": resourceUser(), 52 | "cloudconnexa_host": resourceHost(), 53 | "cloudconnexa_user_group": resourceUserGroup(), 54 | "cloudconnexa_ip_service": resourceIPService(), 55 | }, 56 | 57 | DataSourcesMap: map[string]*schema.Resource{ 58 | "cloudconnexa_network": dataSourceNetwork(), 59 | "cloudconnexa_connector": dataSourceConnector(), 60 | "cloudconnexa_user": dataSourceUser(), 61 | "cloudconnexa_user_group": dataSourceUserGroup(), 62 | "cloudconnexa_vpn_region": dataSourceVpnRegion(), 63 | "cloudconnexa_network_routes": dataSourceNetworkRoutes(), 64 | "cloudconnexa_host": dataSourceHost(), 65 | "cloudconnexa_ip_service": dataSourceIPService(), 66 | }, 67 | ConfigureContextFunc: providerConfigure, 68 | } 69 | } 70 | 71 | func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { 72 | clientId := d.Get("client_id").(string) 73 | clientSecret := d.Get("client_secret").(string) 74 | baseUrl := d.Get("base_url").(string) 75 | cloudConnexaClient, err := cloudconnexa.NewClient(baseUrl, clientId, clientSecret) 76 | var diags diag.Diagnostics 77 | if err != nil { 78 | diags = append(diags, diag.Diagnostic{ 79 | Severity: diag.Error, 80 | Summary: "Unable to create client", 81 | Detail: fmt.Sprintf("Error: %v", err), 82 | }) 83 | return nil, diags 84 | } 85 | return cloudConnexaClient, nil 86 | } 87 | -------------------------------------------------------------------------------- /cloudconnexa/provider_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 11 | 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | const alphabet = "abcdefghigklmnopqrstuvwxyz" 18 | 19 | var testCloudID = os.Getenv("CLOUDCONNEXA_TEST_ORGANIZATION") 20 | var testAccProvider *schema.Provider 21 | var testAccProviderFactories map[string]func() (*schema.Provider, error) 22 | 23 | func init() { 24 | testAccProvider = Provider() 25 | testAccProviderFactories = map[string]func() (*schema.Provider, error){ 26 | "cloudconnexa": func() (*schema.Provider, error) { 27 | return testAccProvider, nil 28 | }, 29 | } 30 | } 31 | 32 | func TestProvider(t *testing.T) { 33 | err := Provider().InternalValidate() 34 | require.NoError(t, err) 35 | 36 | // must have the required error when the credentials are not set 37 | t.Setenv(ClientIDEnvVar, "") 38 | t.Setenv(ClientSecretEnvVar, "") 39 | rc := terraform.ResourceConfig{} 40 | diags := Provider().Configure(context.Background(), &rc) 41 | assert.True(t, diags.HasError()) 42 | 43 | for _, d := range diags { 44 | assert.Truef(t, strings.Contains(d.Detail, cloudconnexa.ErrCredentialsRequired.Error()), 45 | "error message does not contain the expected error: %s", d.Detail) 46 | } 47 | } 48 | 49 | func testAccPreCheck(t *testing.T) { 50 | if v := os.Getenv(ClientIDEnvVar); v == "" { 51 | t.Fatalf("%s must be set for acceptance tests", ClientIDEnvVar) 52 | } 53 | if v := os.Getenv(ClientSecretEnvVar); v == "" { 54 | t.Fatalf("%s must be set for acceptance tests", ClientSecretEnvVar) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cloudconnexa/resource_connector.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 11 | ) 12 | 13 | func resourceConnector() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use `cloudconnexa_connector` to create an Cloud Connexa connector.\n\n~> NOTE: This only creates the Cloud Connexa connector object. Additional manual steps are required to associate a host in your infrastructure with the connector. Go to https://openvpn.net/cloud-docs/connector/ for more information.", 16 | CreateContext: resourceConnectorCreate, 17 | ReadContext: resourceConnectorRead, 18 | DeleteContext: resourceConnectorDelete, 19 | Importer: &schema.ResourceImporter{ 20 | StateContext: schema.ImportStatePassthroughContext, 21 | }, 22 | Schema: map[string]*schema.Schema{ 23 | "name": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | ForceNew: true, 27 | Description: "The connector display name.", 28 | }, 29 | "vpn_region_id": { 30 | Type: schema.TypeString, 31 | Required: true, 32 | ForceNew: true, 33 | Description: "The id of the region where the connector will be deployed.", 34 | }, 35 | "network_item_type": { 36 | Type: schema.TypeString, 37 | Required: true, 38 | ForceNew: true, 39 | ValidateFunc: validation.StringInSlice([]string{"HOST", "NETWORK"}, false), 40 | Description: "The type of network item of the connector. Supported values are `HOST` and `NETWORK`.", 41 | }, 42 | "network_item_id": { 43 | Type: schema.TypeString, 44 | Required: true, 45 | ForceNew: true, 46 | Description: "The id of the network with which this connector is associated.", 47 | }, 48 | "ip_v4_address": { 49 | Type: schema.TypeString, 50 | Computed: true, 51 | Description: "The IPV4 address of the connector.", 52 | }, 53 | "ip_v6_address": { 54 | Type: schema.TypeString, 55 | Computed: true, 56 | Description: "The IPV6 address of the connector.", 57 | }, 58 | "profile": { 59 | Type: schema.TypeString, 60 | Computed: true, 61 | Description: "OpenVPN profile of the connector.", 62 | }, 63 | }, 64 | } 65 | } 66 | 67 | func resourceConnectorCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 68 | c := m.(*cloudconnexa.Client) 69 | var diags diag.Diagnostics 70 | name := d.Get("name").(string) 71 | networkItemId := d.Get("network_item_id").(string) 72 | networkItemType := d.Get("network_item_type").(string) 73 | vpnRegionId := d.Get("vpn_region_id").(string) 74 | connector := cloudconnexa.Connector{ 75 | Name: name, 76 | NetworkItemId: networkItemId, 77 | NetworkItemType: networkItemType, 78 | VpnRegionId: vpnRegionId, 79 | } 80 | conn, err := c.Connectors.Create(connector, networkItemId) 81 | if err != nil { 82 | return diag.FromErr(err) 83 | } 84 | d.SetId(conn.Id) 85 | profile, err := c.Connectors.GetProfile(conn.Id) 86 | if err != nil { 87 | return append(diags, diag.FromErr(err)...) 88 | } 89 | d.Set("profile", profile) 90 | return append(diags, diag.Diagnostic{ 91 | Severity: diag.Warning, 92 | Summary: "Connector needs to be set up manually", 93 | Detail: "Terraform only creates the Cloud Connexa connector object, but additional manual steps are required to associate a host in your infrastructure with this connector. Go to https://openvpn.net/cloud-docs/connector/ for more information.", 94 | }) 95 | } 96 | 97 | func resourceConnectorRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 98 | c := m.(*cloudconnexa.Client) 99 | var diags diag.Diagnostics 100 | connector, err := c.Connectors.GetByID(d.Id()) 101 | if err != nil { 102 | return append(diags, diag.FromErr(err)...) 103 | } 104 | if connector == nil { 105 | d.SetId("") 106 | } else { 107 | d.SetId(connector.Id) 108 | d.Set("name", connector.Name) 109 | d.Set("vpn_region_id", connector.VpnRegionId) 110 | d.Set("network_item_type", connector.NetworkItemType) 111 | d.Set("network_item_id", connector.NetworkItemId) 112 | d.Set("ip_v4_address", connector.IPv4Address) 113 | d.Set("ip_v6_address", connector.IPv6Address) 114 | profile, err := c.Connectors.GetProfile(connector.Id) 115 | if err != nil { 116 | return append(diags, diag.FromErr(err)...) 117 | } 118 | d.Set("profile", profile) 119 | } 120 | return diags 121 | } 122 | 123 | func resourceConnectorDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 124 | c := m.(*cloudconnexa.Client) 125 | var diags diag.Diagnostics 126 | err := c.Connectors.Delete(d.Id(), d.Get("network_item_id").(string), d.Get("network_item_type").(string)) 127 | if err != nil { 128 | return append(diags, diag.FromErr(err)...) 129 | } 130 | return diags 131 | } 132 | 133 | func getConnectorSlice(connectors []cloudconnexa.Connector, networkItemId string, connectorName string, m interface{}) ([]interface{}, error) { 134 | if len(connectors) == 0 { 135 | return nil, nil 136 | } 137 | connectorsList := make([]interface{}, 1) 138 | for _, c := range connectors { 139 | if c.NetworkItemId == networkItemId && c.Name == connectorName { 140 | connector := make(map[string]interface{}) 141 | connector["id"] = c.Id 142 | connector["name"] = c.Name 143 | connector["network_item_id"] = c.NetworkItemId 144 | connector["network_item_type"] = c.NetworkItemType 145 | connector["vpn_region_id"] = c.VpnRegionId 146 | connector["ip_v4_address"] = c.IPv4Address 147 | connector["ip_v6_address"] = c.IPv6Address 148 | client := m.(*cloudconnexa.Client) 149 | profile, err := client.Connectors.GetProfile(c.Id) 150 | if err != nil { 151 | return nil, err 152 | } 153 | connector["profile"] = profile 154 | connectorsList[0] = connector 155 | break 156 | } 157 | } 158 | return connectorsList, nil 159 | } 160 | -------------------------------------------------------------------------------- /cloudconnexa/resource_connector_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "fmt" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 11 | ) 12 | 13 | func TestAccCloudConnexaConnector_basic(t *testing.T) { 14 | rName := acctest.RandomWithPrefix("test-connector") 15 | resourceName := "cloudconnexa_connector.test" 16 | 17 | resource.Test(t, resource.TestCase{ 18 | PreCheck: func() { testAccPreCheck(t) }, 19 | ProviderFactories: testAccProviderFactories, 20 | CheckDestroy: testAccCheckCloudConnexaConnectorDestroy, 21 | Steps: []resource.TestStep{ 22 | { 23 | Config: testAccCloudConnexaConnectorConfigBasic(rName), 24 | Check: resource.ComposeTestCheckFunc( 25 | testAccCheckCloudConnexaConnectorExists(resourceName), 26 | resource.TestCheckResourceAttr(resourceName, "name", rName), 27 | resource.TestCheckResourceAttrSet(resourceName, "vpn_region_id"), 28 | resource.TestCheckResourceAttrSet(resourceName, "network_item_type"), 29 | resource.TestCheckResourceAttrSet(resourceName, "network_item_id"), 30 | resource.TestCheckResourceAttrSet(resourceName, "ip_v4_address"), 31 | resource.TestCheckResourceAttrSet(resourceName, "ip_v6_address"), 32 | ), 33 | }, 34 | }, 35 | }) 36 | } 37 | 38 | func testAccCheckCloudConnexaConnectorExists(n string) resource.TestCheckFunc { 39 | return func(s *terraform.State) error { 40 | rs, ok := s.RootModule().Resources[n] 41 | if !ok { 42 | return fmt.Errorf("Not found: %s", n) 43 | } 44 | if rs.Primary.ID == "" { 45 | return fmt.Errorf("No connector ID is set") 46 | } 47 | return nil 48 | } 49 | } 50 | 51 | func testAccCheckCloudConnexaConnectorDestroy(s *terraform.State) error { 52 | client := testAccProvider.Meta().(*cloudconnexa.Client) 53 | 54 | for _, rs := range s.RootModule().Resources { 55 | if rs.Type != "cloudconnexa_connector" { 56 | continue 57 | } 58 | 59 | connectorId := rs.Primary.ID 60 | connector, err := client.Connectors.GetByID(connectorId) 61 | 62 | if err != nil { 63 | return err 64 | } 65 | 66 | if connector != nil { 67 | return fmt.Errorf("connector with ID '%s' still exists", connectorId) 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func testAccCloudConnexaConnectorConfigBasic(rName string) string { 75 | return fmt.Sprintf(` 76 | provider "cloudconnexa" { 77 | base_url = "https://%[1]s.api.openvpn.com" 78 | } 79 | 80 | resource "cloudconnexa_connector" "test" { 81 | name = "%s" 82 | vpn_region_id = "us-west-1" 83 | network_item_type = "HOST" 84 | network_item_id = "example_network_item_id" 85 | } 86 | `, testCloudID, rName) 87 | } 88 | -------------------------------------------------------------------------------- /cloudconnexa/resource_dns_record.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 10 | ) 11 | 12 | func resourceDnsRecord() *schema.Resource { 13 | return &schema.Resource{ 14 | Description: "Use `cloudconnexa_dns_record` to create a DNS record on your VPN.", 15 | CreateContext: resourceDnsRecordCreate, 16 | ReadContext: resourceDnsRecordRead, 17 | DeleteContext: resourceDnsRecordDelete, 18 | UpdateContext: resourceDnsRecordUpdate, 19 | Importer: &schema.ResourceImporter{ 20 | StateContext: schema.ImportStatePassthroughContext, 21 | }, 22 | Schema: map[string]*schema.Schema{ 23 | "domain": { 24 | Type: schema.TypeString, 25 | Required: true, 26 | ForceNew: true, 27 | Description: "The DNS record name.", 28 | }, 29 | "description": { 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Default: "Managed by Terraform", 33 | ValidateFunc: validation.StringLenBetween(1, 120), 34 | Description: "The description for the UI. Defaults to `Managed by Terraform`.", 35 | }, 36 | "ip_v4_addresses": { 37 | Type: schema.TypeList, 38 | Optional: true, 39 | Elem: &schema.Schema{ 40 | Type: schema.TypeString, 41 | ValidateFunc: validation.IsIPv4Address, 42 | }, 43 | Description: "The list of IPV4 addresses to which this record will resolve.", 44 | }, 45 | "ip_v6_addresses": { 46 | Type: schema.TypeList, 47 | Optional: true, 48 | Elem: &schema.Schema{ 49 | Type: schema.TypeString, 50 | ValidateFunc: validation.IsIPv6Address, 51 | }, 52 | Description: "The list of IPV6 addresses to which this record will resolve.", 53 | }, 54 | }, 55 | } 56 | } 57 | 58 | func resourceDnsRecordCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 59 | c := m.(*cloudconnexa.Client) 60 | var diags diag.Diagnostics 61 | domain := d.Get("domain").(string) 62 | description := d.Get("description").(string) 63 | ipV4Addresses := d.Get("ip_v4_addresses").([]interface{}) 64 | ipV4AddressesSlice := make([]string, 0) 65 | for _, a := range ipV4Addresses { 66 | ipV4AddressesSlice = append(ipV4AddressesSlice, a.(string)) 67 | } 68 | ipV6Addresses := d.Get("ip_v6_addresses").([]interface{}) 69 | ipV6AddressesSlice := make([]string, 0) 70 | for _, a := range ipV6Addresses { 71 | ipV6AddressesSlice = append(ipV6AddressesSlice, a.(string)) 72 | } 73 | dr := cloudconnexa.DnsRecord{ 74 | Domain: domain, 75 | Description: description, 76 | IPV4Addresses: ipV4AddressesSlice, 77 | IPV6Addresses: ipV6AddressesSlice, 78 | } 79 | dnsRecord, err := c.DnsRecords.Create(dr) 80 | if err != nil { 81 | return append(diags, diag.FromErr(err)...) 82 | } 83 | d.SetId(dnsRecord.Id) 84 | return diags 85 | } 86 | 87 | func resourceDnsRecordRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 88 | c := m.(*cloudconnexa.Client) 89 | var diags diag.Diagnostics 90 | recordId := d.Id() 91 | r, err := c.DnsRecords.GetDnsRecord(recordId) 92 | if err != nil { 93 | return append(diags, diag.FromErr(err)...) 94 | } 95 | if r == nil { 96 | d.SetId("") 97 | } else { 98 | d.Set("domain", r.Domain) 99 | d.Set("description", r.Description) 100 | d.Set("ip_v4_addresses", r.IPV4Addresses) 101 | d.Set("ip_v6_addresses", r.IPV6Addresses) 102 | } 103 | return diags 104 | } 105 | 106 | func resourceDnsRecordUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 107 | c := m.(*cloudconnexa.Client) 108 | var diags diag.Diagnostics 109 | _, domain := d.GetChange("domain") 110 | _, description := d.GetChange("description") 111 | _, ipV4Addresses := d.GetChange("ip_v4_addresses") 112 | ipV4AddressesSlice := getAddressesSlice(ipV4Addresses.([]interface{})) 113 | _, ipV6Addresses := d.GetChange("ip_v6_addresses") 114 | ipV6AddressesSlice := getAddressesSlice(ipV6Addresses.([]interface{})) 115 | dr := cloudconnexa.DnsRecord{ 116 | Id: d.Id(), 117 | Domain: domain.(string), 118 | Description: description.(string), 119 | IPV4Addresses: ipV4AddressesSlice, 120 | IPV6Addresses: ipV6AddressesSlice, 121 | } 122 | err := c.DnsRecords.Update(dr) 123 | if err != nil { 124 | return append(diags, diag.FromErr(err)...) 125 | } 126 | return diags 127 | } 128 | 129 | func resourceDnsRecordDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 130 | c := m.(*cloudconnexa.Client) 131 | var diags diag.Diagnostics 132 | routeId := d.Id() 133 | err := c.DnsRecords.Delete(routeId) 134 | if err != nil { 135 | return append(diags, diag.FromErr(err)...) 136 | } 137 | return diags 138 | } 139 | 140 | func getAddressesSlice(addresses []interface{}) []string { 141 | addressesSlice := make([]string, 0) 142 | for _, a := range addresses { 143 | addressesSlice = append(addressesSlice, a.(string)) 144 | } 145 | return addressesSlice 146 | } 147 | -------------------------------------------------------------------------------- /cloudconnexa/resource_dns_record_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "fmt" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestAccCloudConnexaDnsRecord_basic(t *testing.T) { 13 | resourceName := "cloudconnexa_dns_record.test" 14 | domainName := "test.cloudconnexa.com" 15 | resource.Test(t, resource.TestCase{ 16 | PreCheck: func() { testAccPreCheck(t) }, 17 | ProviderFactories: testAccProviderFactories, 18 | CheckDestroy: testAccCheckCloudConnexaDnsRecordDestroy, 19 | Steps: []resource.TestStep{ 20 | { 21 | Config: testAccCloudConnexaDnsRecordConfig(domainName), 22 | Check: resource.ComposeTestCheckFunc( 23 | resource.TestCheckResourceAttr(resourceName, "domain", domainName), 24 | resource.TestCheckResourceAttr(resourceName, "description", "test description"), 25 | resource.TestCheckResourceAttr(resourceName, "ip_v4_addresses.0", "192.168.1.1"), 26 | resource.TestCheckResourceAttr(resourceName, "ip_v4_addresses.1", "192.168.1.2"), 27 | resource.TestCheckResourceAttr(resourceName, "ip_v6_addresses.0", "2001:db8:85a3:0:0:8a2e:370:7334"), 28 | resource.TestCheckResourceAttr(resourceName, "ip_v6_addresses.1", "2001:db8:85a3:0:0:8a2e:370:7335"), 29 | ), 30 | }, 31 | }, 32 | }) 33 | } 34 | 35 | func testAccCheckCloudConnexaDnsRecordDestroy(s *terraform.State) error { 36 | client := testAccProvider.Meta().(*cloudconnexa.Client) 37 | 38 | for _, rs := range s.RootModule().Resources { 39 | if rs.Type != "cloudconnexa_dns_record" { 40 | continue 41 | } 42 | 43 | recordId := rs.Primary.ID 44 | r, err := client.DnsRecords.GetDnsRecord(recordId) 45 | 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if r != nil { 51 | return fmt.Errorf("DNS record with ID '%s' still exists", recordId) 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | 58 | func testAccCloudConnexaDnsRecordConfig(domainName string) string { 59 | return fmt.Sprintf(` 60 | provider "cloudconnexa" { 61 | base_url = "https://%[1]s.api.openvpn.com" 62 | } 63 | 64 | resource "cloudconnexa_dns_record" "test" { 65 | domain = "%[2]s" 66 | description = "test description" 67 | ip_v4_addresses = ["192.168.1.1", "192.168.1.2"] 68 | ip_v6_addresses = ["2001:db8:85a3:0:0:8a2e:370:7334", "2001:db8:85a3:0:0:8a2e:370:7335"] 69 | } 70 | `, testCloudID, domainName) 71 | } 72 | -------------------------------------------------------------------------------- /cloudconnexa/resource_host.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 6 | "hash/fnv" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 11 | ) 12 | 13 | func resourceHost() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use `cloudconnexa_host` to create an Cloud Connexa host.", 16 | CreateContext: resourceHostCreate, 17 | ReadContext: resourceHostRead, 18 | UpdateContext: resourceHostUpdate, 19 | DeleteContext: resourceHostDelete, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: schema.ImportStatePassthroughContext, 22 | }, 23 | Schema: map[string]*schema.Schema{ 24 | "name": { 25 | Type: schema.TypeString, 26 | Required: true, 27 | Description: "The display name of the host.", 28 | }, 29 | "description": { 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Default: "Managed by Terraform", 33 | ValidateFunc: validation.StringLenBetween(1, 120), 34 | Description: "The description for the UI. Defaults to `Managed by Terraform`.", 35 | }, 36 | "internet_access": { 37 | Type: schema.TypeString, 38 | Optional: true, 39 | Default: "LOCAL", 40 | ValidateFunc: validation.StringInSlice([]string{"BLOCKED", "GLOBAL_INTERNET", "LOCAL"}, false), 41 | Description: "The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`.", 42 | }, 43 | "system_subnets": { 44 | Type: schema.TypeSet, 45 | Computed: true, 46 | Elem: &schema.Schema{ 47 | Type: schema.TypeString, 48 | }, 49 | Description: "The IPV4 and IPV6 subnets automatically assigned to this host.", 50 | }, 51 | "connector": { 52 | Type: schema.TypeSet, 53 | Required: true, 54 | Set: func(i interface{}) int { 55 | n := i.(map[string]interface{})["name"] 56 | h := fnv.New32a() 57 | h.Write([]byte(n.(string))) 58 | return int(h.Sum32()) 59 | }, 60 | Description: "The set of connectors to be associated with this host. Can be defined more than once.", 61 | Elem: &schema.Resource{ 62 | Schema: map[string]*schema.Schema{ 63 | "id": { 64 | Type: schema.TypeString, 65 | Computed: true, 66 | }, 67 | "name": { 68 | Type: schema.TypeString, 69 | Required: true, 70 | Description: "Name of the connector associated with this host.", 71 | }, 72 | "vpn_region_id": { 73 | Type: schema.TypeString, 74 | Required: true, 75 | Description: "The id of the region where the connector will be deployed.", 76 | }, 77 | "network_item_type": { 78 | Type: schema.TypeString, 79 | Computed: true, 80 | Description: "The network object type. This typically will be set to `HOST`.", 81 | }, 82 | "network_item_id": { 83 | Type: schema.TypeString, 84 | Computed: true, 85 | Description: "The host id.", 86 | }, 87 | "ip_v4_address": { 88 | Type: schema.TypeString, 89 | Computed: true, 90 | Description: "The IPV4 address of the connector.", 91 | }, 92 | "ip_v6_address": { 93 | Type: schema.TypeString, 94 | Computed: true, 95 | Description: "The IPV6 address of the connector.", 96 | }, 97 | "profile": { 98 | Type: schema.TypeString, 99 | Computed: true, 100 | }, 101 | }, 102 | }, 103 | }, 104 | }, 105 | } 106 | } 107 | 108 | func resourceHostCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 109 | c := m.(*cloudconnexa.Client) 110 | var diags diag.Diagnostics 111 | var connectors []cloudconnexa.Connector 112 | configConnectors := d.Get("connector").(*schema.Set) 113 | for _, c := range configConnectors.List() { 114 | connectors = append(connectors, cloudconnexa.Connector{ 115 | Name: c.(map[string]interface{})["name"].(string), 116 | VpnRegionId: c.(map[string]interface{})["vpn_region_id"].(string), 117 | }) 118 | } 119 | h := cloudconnexa.Host{ 120 | Name: d.Get("name").(string), 121 | Description: d.Get("description").(string), 122 | InternetAccess: d.Get("internet_access").(string), 123 | Connectors: connectors, 124 | } 125 | host, err := c.Hosts.Create(h) 126 | if err != nil { 127 | return append(diags, diag.FromErr(err)...) 128 | } 129 | d.SetId(host.Id) 130 | diagnostics := setConnectorsList(d, c, host.Connectors) 131 | if diagnostics != nil { 132 | return diagnostics 133 | } 134 | 135 | return append(diags, diag.Diagnostic{ 136 | Severity: diag.Warning, 137 | Summary: "The connector for this host needs to be set up manually", 138 | Detail: "Terraform only creates the Cloud Connexa connector object for this host, but additional manual steps are required to associate a host in your infrastructure with this connector. Go to https://openvpn.net/cloud-docs/connector/ for more information.", 139 | }) 140 | } 141 | 142 | func resourceHostRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 143 | c := m.(*cloudconnexa.Client) 144 | var diags diag.Diagnostics 145 | host, err := c.Hosts.Get(d.Id()) 146 | if err != nil { 147 | return append(diags, diag.FromErr(err)...) 148 | } 149 | if host == nil { 150 | d.SetId("") 151 | return diags 152 | } 153 | d.Set("name", host.Name) 154 | d.Set("description", host.Description) 155 | d.Set("internet_access", host.InternetAccess) 156 | d.Set("system_subnets", host.SystemSubnets) 157 | 158 | diagnostics := setConnectorsList(d, c, host.Connectors) 159 | if diagnostics != nil { 160 | return diagnostics 161 | } 162 | return diags 163 | } 164 | 165 | func resourceHostUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 166 | c := m.(*cloudconnexa.Client) 167 | var diags diag.Diagnostics 168 | if d.HasChange("connector") { 169 | old, new := d.GetChange("connector") 170 | oldSet := old.(*schema.Set) 171 | newSet := new.(*schema.Set) 172 | if oldSet.Len() == 0 && newSet.Len() > 0 { 173 | // This happens when importing the resource 174 | newConnector := cloudconnexa.Connector{ 175 | Name: newSet.List()[0].(map[string]interface{})["name"].(string), 176 | VpnRegionId: newSet.List()[0].(map[string]interface{})["vpn_region_id"].(string), 177 | NetworkItemType: "HOST", 178 | } 179 | _, err := c.Connectors.Create(newConnector, d.Id()) 180 | if err != nil { 181 | return append(diags, diag.FromErr(err)...) 182 | } 183 | } else { 184 | for _, o := range oldSet.List() { 185 | if !newSet.Contains(o) { 186 | err := c.Connectors.Delete(o.(map[string]interface{})["id"].(string), d.Id(), "HOST") 187 | if err != nil { 188 | diags = append(diags, diag.FromErr(err)...) 189 | } 190 | } 191 | } 192 | for _, n := range newSet.List() { 193 | if !oldSet.Contains(n) { 194 | newConnector := cloudconnexa.Connector{ 195 | Name: n.(map[string]interface{})["name"].(string), 196 | VpnRegionId: n.(map[string]interface{})["vpn_region_id"].(string), 197 | NetworkItemType: "HOST", 198 | } 199 | _, err := c.Connectors.Create(newConnector, d.Id()) 200 | if err != nil { 201 | diags = append(diags, diag.FromErr(err)...) 202 | } 203 | } 204 | } 205 | } 206 | } 207 | if d.HasChanges("name", "description", "internet_access") { 208 | _, newName := d.GetChange("name") 209 | _, newDescription := d.GetChange("description") 210 | _, newAccess := d.GetChange("internet_access") 211 | err := c.Hosts.Update(cloudconnexa.Host{ 212 | Id: d.Id(), 213 | Name: newName.(string), 214 | Description: newDescription.(string), 215 | InternetAccess: newAccess.(string), 216 | }) 217 | if err != nil { 218 | return append(diags, diag.FromErr(err)...) 219 | } 220 | } 221 | return append(diags, resourceHostRead(ctx, d, m)...) 222 | } 223 | 224 | func resourceHostDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 225 | c := m.(*cloudconnexa.Client) 226 | var diags diag.Diagnostics 227 | hostId := d.Id() 228 | err := c.Hosts.Delete(hostId) 229 | if err != nil { 230 | return append(diags, diag.FromErr(err)...) 231 | } 232 | return diags 233 | } 234 | 235 | func setConnectorsList(data *schema.ResourceData, c *cloudconnexa.Client, connectors []cloudconnexa.Connector) diag.Diagnostics { 236 | connectorsList := make([]interface{}, len(connectors)) 237 | for i, connector := range connectors { 238 | connectorsData, err := getConnectorsListItem(c, connector) 239 | if err != nil { 240 | return diag.FromErr(err) 241 | } 242 | connectorsList[i] = connectorsData 243 | } 244 | err := data.Set("connector", connectorsList) 245 | if err != nil { 246 | return diag.FromErr(err) 247 | } 248 | return nil 249 | } 250 | 251 | func getConnectorsListItem(c *cloudconnexa.Client, connector cloudconnexa.Connector) (map[string]interface{}, error) { 252 | connectorsData := map[string]interface{}{ 253 | "id": connector.Id, 254 | "name": connector.Name, 255 | "vpn_region_id": connector.VpnRegionId, 256 | "ip_v4_address": connector.IPv4Address, 257 | "ip_v6_address": connector.IPv6Address, 258 | "network_item_id": connector.NetworkItemId, 259 | "network_item_type": connector.NetworkItemType, 260 | } 261 | 262 | connectorProfile, err := c.Connectors.GetProfile(connector.Id) 263 | if err != nil { 264 | return nil, err 265 | } 266 | connectorsData["profile"] = connectorProfile 267 | return connectorsData, nil 268 | } 269 | -------------------------------------------------------------------------------- /cloudconnexa/resource_network.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 11 | ) 12 | 13 | func resourceNetwork() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use `cloudconnexa_network` to create an Cloud Connexa Network.", 16 | CreateContext: resourceNetworkCreate, 17 | ReadContext: resourceNetworkRead, 18 | UpdateContext: resourceNetworkUpdate, 19 | DeleteContext: resourceNetworkDelete, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: schema.ImportStatePassthroughContext, 22 | }, 23 | Schema: map[string]*schema.Schema{ 24 | "name": { 25 | Type: schema.TypeString, 26 | Required: true, 27 | Description: "The display name of the network.", 28 | }, 29 | "description": { 30 | Type: schema.TypeString, 31 | Optional: true, 32 | Default: "Managed by Terraform", 33 | ValidateFunc: validation.StringLenBetween(1, 120), 34 | Description: "The display description for this resource. Defaults to `Managed by Terraform`.", 35 | }, 36 | "egress": { 37 | Type: schema.TypeBool, 38 | Optional: true, 39 | Default: true, 40 | Description: "Boolean to control whether this network provides an egress or not.", 41 | }, 42 | "internet_access": { 43 | Type: schema.TypeString, 44 | Optional: true, 45 | Default: "LOCAL", 46 | ValidateFunc: validation.StringInSlice([]string{"BLOCKED", "GLOBAL_INTERNET", "LOCAL"}, false), 47 | Description: "The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`.", 48 | }, 49 | "system_subnets": { 50 | Type: schema.TypeSet, 51 | Computed: true, 52 | Elem: &schema.Schema{ 53 | Type: schema.TypeString, 54 | }, 55 | Description: "The IPV4 and IPV6 subnets automatically assigned to this network.", 56 | }, 57 | "default_route": { 58 | Type: schema.TypeList, 59 | Required: true, 60 | MaxItems: 1, 61 | Description: "The default route of this network.", 62 | Elem: &schema.Resource{ 63 | Schema: map[string]*schema.Schema{ 64 | "type": { 65 | Type: schema.TypeString, 66 | Optional: true, 67 | Default: "IP_V4", 68 | ValidateFunc: validation.StringInSlice([]string{"IP_V4", "IP_V6"}, false), 69 | Description: "The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`.", 70 | }, 71 | "description": { 72 | Type: schema.TypeString, 73 | Optional: true, 74 | Default: "Managed by Terraform.", 75 | Description: "The default route description.", 76 | }, 77 | "subnet": { 78 | Type: schema.TypeString, 79 | Required: true, 80 | Description: "The target value of the default route.", 81 | }, 82 | "id": { 83 | Type: schema.TypeString, 84 | Computed: true, 85 | Description: "The ID of this resource.", 86 | }, 87 | }, 88 | }, 89 | }, 90 | "default_connector": { 91 | Type: schema.TypeList, 92 | Required: true, 93 | MaxItems: 1, 94 | Description: "The default connector of this network.", 95 | Elem: &schema.Resource{ 96 | Schema: map[string]*schema.Schema{ 97 | "id": { 98 | Type: schema.TypeString, 99 | Computed: true, 100 | Description: "The ID of this resource.", 101 | }, 102 | "description": { 103 | Type: schema.TypeString, 104 | Optional: true, 105 | Default: "Managed by Terraform.", 106 | Description: "The default connection description.", 107 | }, 108 | "name": { 109 | Type: schema.TypeString, 110 | Required: true, 111 | Description: "Name of the connector automatically created and attached to this network.", 112 | }, 113 | "vpn_region_id": { 114 | Type: schema.TypeString, 115 | Required: true, 116 | Description: "The id of the region where the default connector will be deployed.", 117 | }, 118 | "network_item_type": { 119 | Type: schema.TypeString, 120 | Computed: true, 121 | Description: "The network object type. This typically will be set to `NETWORK`.", 122 | }, 123 | "network_item_id": { 124 | Type: schema.TypeString, 125 | Computed: true, 126 | Description: "The parent network id.", 127 | }, 128 | "ip_v4_address": { 129 | Type: schema.TypeString, 130 | Computed: true, 131 | Description: "The IPV4 address of the default connector.", 132 | }, 133 | "ip_v6_address": { 134 | Type: schema.TypeString, 135 | Computed: true, 136 | Description: "The IPV6 address of the default connector.", 137 | }, 138 | "profile": { 139 | Type: schema.TypeString, 140 | Computed: true, 141 | Description: "OpenVPN profile of the connector.", 142 | }, 143 | }, 144 | }, 145 | }, 146 | }, 147 | } 148 | } 149 | 150 | func resourceNetworkCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 151 | c := m.(*cloudconnexa.Client) 152 | var diags diag.Diagnostics 153 | configConnector := d.Get("default_connector").([]interface{})[0].(map[string]interface{}) 154 | connectors := []cloudconnexa.NetworkConnector{ 155 | { 156 | Name: configConnector["name"].(string), 157 | VpnRegionId: configConnector["vpn_region_id"].(string), 158 | Description: configConnector["description"].(string), 159 | }, 160 | } 161 | n := cloudconnexa.Network{ 162 | Name: d.Get("name").(string), 163 | Description: d.Get("description").(string), 164 | Egress: d.Get("egress").(bool), 165 | InternetAccess: d.Get("internet_access").(string), 166 | Connectors: connectors, 167 | } 168 | network, err := c.Networks.Create(n) 169 | if err != nil { 170 | return append(diags, diag.FromErr(err)...) 171 | } 172 | d.SetId(network.Id) 173 | configRoute := d.Get("default_route").([]interface{})[0].(map[string]interface{}) 174 | defaultRoute, err := c.Routes.Create(network.Id, cloudconnexa.Route{ 175 | Type: configRoute["type"].(string), 176 | Description: configRoute["description"].(string), 177 | Subnet: configRoute["subnet"].(string), 178 | }) 179 | if err != nil { 180 | return append(diags, diag.FromErr(err)...) 181 | } 182 | defaultRouteWithIdSlice := make([]map[string]interface{}, 1) 183 | defaultRouteWithIdSlice[0] = map[string]interface{}{ 184 | "id": defaultRoute.Id, 185 | "description": defaultRoute.Description, 186 | "type": defaultRoute.Type, 187 | "subnet": defaultRoute.Subnet, 188 | } 189 | d.Set("default_route", defaultRouteWithIdSlice) 190 | connectorsList := make([]interface{}, 1) 191 | connector := make(map[string]interface{}) 192 | connector["id"] = network.Connectors[0].Id 193 | connector["name"] = network.Connectors[0].Name 194 | connector["network_item_id"] = network.Connectors[0].NetworkItemId 195 | connector["network_item_type"] = network.Connectors[0].NetworkItemType 196 | connector["vpn_region_id"] = network.Connectors[0].VpnRegionId 197 | connector["ip_v4_address"] = network.Connectors[0].IPv4Address 198 | connector["ip_v6_address"] = network.Connectors[0].IPv6Address 199 | client := m.(*cloudconnexa.Client) 200 | profile, err := client.Connectors.GetProfile(network.Connectors[0].Id) 201 | if err != nil { 202 | return append(diags, diag.FromErr(err)...) 203 | } 204 | connector["profile"] = profile 205 | connectorsList[0] = connector 206 | err = d.Set("default_connector", connectorsList) 207 | if err != nil { 208 | return append(diags, diag.FromErr(err)...) 209 | } 210 | return append(diags, diag.Diagnostic{ 211 | Severity: diag.Warning, 212 | Summary: "The default connector for this network needs to be set up manually", 213 | Detail: "Terraform only creates the Cloud Connexa default connector object for this network, but additional manual steps are required to associate a host in your infrastructure with this connector. Go to https://openvpn.net/cloud-docs/connector/ for more information.", 214 | }) 215 | } 216 | 217 | func resourceNetworkRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 218 | c := m.(*cloudconnexa.Client) 219 | var diags diag.Diagnostics 220 | network, err := c.Networks.Get(d.Id()) 221 | if err != nil { 222 | return append(diags, diag.FromErr(err)...) 223 | } 224 | if network == nil { 225 | d.SetId("") 226 | return diags 227 | } 228 | d.Set("name", network.Name) 229 | d.Set("description", network.Description) 230 | d.Set("egress", network.Egress) 231 | d.Set("internet_access", network.InternetAccess) 232 | d.Set("system_subnets", network.SystemSubnets) 233 | if len(d.Get("default_connector").([]interface{})) > 0 { 234 | configConnector := d.Get("default_connector").([]interface{})[0].(map[string]interface{}) 235 | connectorName := configConnector["name"].(string) 236 | networkConnectors, err := c.Connectors.GetByNetworkID(network.Id) 237 | if err != nil { 238 | return append(diags, diag.FromErr(err)...) 239 | } 240 | retrievedConnector, err := getConnectorSlice(networkConnectors, network.Id, connectorName, m) 241 | if err != nil { 242 | return append(diags, diag.FromErr(err)...) 243 | } 244 | err = d.Set("default_connector", retrievedConnector) 245 | if err != nil { 246 | return append(diags, diag.FromErr(err)...) 247 | } 248 | } 249 | if len(d.Get("default_route").([]interface{})) > 0 { 250 | configRoute := d.Get("default_route").([]interface{})[0].(map[string]interface{}) 251 | route, err := c.Routes.GetNetworkRoute(d.Id(), configRoute["id"].(string)) 252 | if err != nil { 253 | return append(diags, diag.FromErr(err)...) 254 | } 255 | if route == nil { 256 | d.Set("default_route", []map[string]interface{}{}) 257 | } else { 258 | defaultRoute := []map[string]interface{}{ 259 | { 260 | "id": configRoute["id"].(string), 261 | "type": route.Type, 262 | "description": route.Description, 263 | }, 264 | } 265 | if route.Type == "IP_V4" || route.Type == "IP_V6" { 266 | defaultRoute[0]["subnet"] = route.Subnet 267 | } 268 | d.Set("default_route", defaultRoute) 269 | } 270 | } 271 | return diags 272 | } 273 | 274 | func resourceNetworkUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 275 | c := m.(*cloudconnexa.Client) 276 | var diags diag.Diagnostics 277 | if d.HasChange("default_connector") { 278 | old, new := d.GetChange("default_connector") 279 | oldSlice := old.([]interface{}) 280 | newSlice := new.([]interface{}) 281 | if len(oldSlice) == 0 && len(newSlice) == 1 { 282 | // This happens when importing the resource 283 | newConnector := cloudconnexa.Connector{ 284 | Name: newSlice[0].(map[string]interface{})["name"].(string), 285 | VpnRegionId: newSlice[0].(map[string]interface{})["vpn_region_id"].(string), 286 | NetworkItemType: "NETWORK", 287 | } 288 | _, err := c.Connectors.Create(newConnector, d.Id()) 289 | if err != nil { 290 | return append(diags, diag.FromErr(err)...) 291 | } 292 | } else { 293 | oldMap := oldSlice[0].(map[string]interface{}) 294 | newMap := newSlice[0].(map[string]interface{}) 295 | if oldMap["name"].(string) != newMap["name"].(string) || oldMap["vpn_region_id"].(string) != newMap["vpn_region_id"].(string) { 296 | newConnector := cloudconnexa.Connector{ 297 | Name: newMap["name"].(string), 298 | VpnRegionId: newMap["vpn_region_id"].(string), 299 | NetworkItemType: "NETWORK", 300 | } 301 | _, err := c.Connectors.Create(newConnector, d.Id()) 302 | if err != nil { 303 | return append(diags, diag.FromErr(err)...) 304 | } 305 | if len(oldMap["id"].(string)) > 0 { 306 | // This can sometimes happen when importing the resource 307 | err = c.Connectors.Delete(oldMap["id"].(string), d.Id(), oldMap["network_item_type"].(string)) 308 | if err != nil { 309 | return append(diags, diag.FromErr(err)...) 310 | } 311 | } 312 | } 313 | } 314 | } 315 | if d.HasChange("default_route") { 316 | old, new := d.GetChange("default_route") 317 | oldSlice := old.([]interface{}) 318 | newSlice := new.([]interface{}) 319 | if len(oldSlice) == 0 && len(newSlice) == 1 { 320 | // This happens when importing the resource 321 | newMap := newSlice[0].(map[string]interface{}) 322 | routeType := newMap["type"] 323 | routeDesc := newMap["description"] 324 | routeSubnet := newMap["subnet"] 325 | route := cloudconnexa.Route{ 326 | Type: routeType.(string), 327 | Description: routeDesc.(string), 328 | Subnet: routeSubnet.(string), 329 | } 330 | defaultRoute, err := c.Routes.Create(d.Id(), route) 331 | if err != nil { 332 | return append(diags, diag.FromErr(err)...) 333 | } 334 | defaultRouteWithIdSlice := make([]map[string]interface{}, 1) 335 | defaultRouteWithIdSlice[0] = map[string]interface{}{ 336 | "id": defaultRoute.Id, 337 | "description": defaultRoute.Description, 338 | } 339 | err = d.Set("default_route", defaultRouteWithIdSlice) 340 | if err != nil { 341 | diags = append(diags, diag.FromErr(err)...) 342 | } 343 | } else { 344 | newMap := newSlice[0].(map[string]interface{}) 345 | routeId := newMap["id"] 346 | routeType := newMap["type"] 347 | routeDesc := newMap["description"] 348 | routeSubnet := newMap["subnet"] 349 | route := cloudconnexa.Route{ 350 | Id: routeId.(string), 351 | Type: routeType.(string), 352 | Description: routeDesc.(string), 353 | Subnet: routeSubnet.(string), 354 | } 355 | err := c.Routes.Update(d.Id(), route) 356 | if err != nil { 357 | diags = append(diags, diag.FromErr(err)...) 358 | } 359 | } 360 | } 361 | if d.HasChanges("name", "description", "internet_access", "egress") { 362 | _, newName := d.GetChange("name") 363 | _, newDescription := d.GetChange("description") 364 | _, newEgress := d.GetChange("egress") 365 | _, newAccess := d.GetChange("internet_access") 366 | err := c.Networks.Update(cloudconnexa.Network{ 367 | Id: d.Id(), 368 | Name: newName.(string), 369 | Description: newDescription.(string), 370 | Egress: newEgress.(bool), 371 | InternetAccess: newAccess.(string), 372 | }) 373 | if err != nil { 374 | return append(diags, diag.FromErr(err)...) 375 | } 376 | } 377 | return append(diags, resourceNetworkRead(ctx, d, m)...) 378 | } 379 | 380 | func resourceNetworkDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 381 | c := m.(*cloudconnexa.Client) 382 | var diags diag.Diagnostics 383 | networkId := d.Id() 384 | err := c.Networks.Delete(networkId) 385 | if err != nil { 386 | return append(diags, diag.FromErr(err)...) 387 | } 388 | return diags 389 | } 390 | -------------------------------------------------------------------------------- /cloudconnexa/resource_route.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 11 | ) 12 | 13 | func resourceRoute() *schema.Resource { 14 | return &schema.Resource{ 15 | Description: "Use `cloudconnexa_route` to create a route on an Cloud Connexa network.", 16 | CreateContext: resourceRouteCreate, 17 | UpdateContext: resourceRouteUpdate, 18 | ReadContext: resourceRouteRead, 19 | DeleteContext: resourceRouteDelete, 20 | Importer: &schema.ResourceImporter{ 21 | StateContext: schema.ImportStatePassthroughContext, 22 | }, 23 | Schema: map[string]*schema.Schema{ 24 | "type": { 25 | Type: schema.TypeString, 26 | Required: true, 27 | ForceNew: true, 28 | ValidateFunc: validation.StringInSlice([]string{"IP_V4", "IP_V6"}, false), 29 | Description: "The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`.", 30 | }, 31 | "subnet": { 32 | Type: schema.TypeString, 33 | Required: true, 34 | ForceNew: true, 35 | Description: "The target value of the default route.", 36 | }, 37 | "network_item_id": { 38 | Type: schema.TypeString, 39 | Required: true, 40 | ForceNew: true, 41 | Description: "The id of the network on which to create the route.", 42 | }, 43 | "description": { 44 | Type: schema.TypeString, 45 | Optional: true, 46 | Default: "Managed by Terraform", 47 | }, 48 | }, 49 | } 50 | } 51 | 52 | func resourceRouteCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 53 | c := m.(*cloudconnexa.Client) 54 | var diags diag.Diagnostics 55 | networkItemId := d.Get("network_item_id").(string) 56 | routeType := d.Get("type").(string) 57 | routeSubnet := d.Get("subnet").(string) 58 | descriptionValue := d.Get("description").(string) 59 | r := cloudconnexa.Route{ 60 | Type: routeType, 61 | Subnet: routeSubnet, 62 | Description: descriptionValue, 63 | } 64 | route, err := c.Routes.Create(networkItemId, r) 65 | if err != nil { 66 | return append(diags, diag.FromErr(err)...) 67 | } 68 | d.SetId(route.Id) 69 | if routeType == "IP_V4" || routeType == "IP_V6" { 70 | d.Set("subnet", route.Subnet) 71 | } 72 | return diags 73 | } 74 | 75 | func resourceRouteRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 76 | c := m.(*cloudconnexa.Client) 77 | var diags diag.Diagnostics 78 | routeId := d.Id() 79 | r, err := c.Routes.Get(routeId) 80 | if err != nil { 81 | return append(diags, diag.FromErr(err)...) 82 | } 83 | if r == nil { 84 | d.SetId("") 85 | } else { 86 | d.Set("type", r.Type) 87 | if r.Type == "IP_V4" || r.Type == "IP_V6" { 88 | d.Set("subnet", r.Subnet) 89 | } 90 | d.Set("description", r.Description) 91 | d.Set("network_item_id", r.NetworkItemId) 92 | } 93 | return diags 94 | } 95 | 96 | func resourceRouteUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 97 | c := m.(*cloudconnexa.Client) 98 | var diags diag.Diagnostics 99 | if !d.HasChanges("description", "subnet") { 100 | return diags 101 | } 102 | 103 | networkItemId := d.Get("network_item_id").(string) 104 | _, description := d.GetChange("description") 105 | _, subnet := d.GetChange("subnet") 106 | r := cloudconnexa.Route{ 107 | Id: d.Id(), 108 | Description: description.(string), 109 | Subnet: subnet.(string), 110 | } 111 | 112 | err := c.Routes.Update(networkItemId, r) 113 | if err != nil { 114 | return append(diags, diag.FromErr(err)...) 115 | } 116 | return diags 117 | } 118 | 119 | func resourceRouteDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 120 | c := m.(*cloudconnexa.Client) 121 | var diags diag.Diagnostics 122 | routeId := d.Id() 123 | networkItemId := d.Get("network_item_id").(string) 124 | err := c.Routes.Delete(networkItemId, routeId) 125 | if err != nil { 126 | return append(diags, diag.FromErr(err)...) 127 | } 128 | return diags 129 | } 130 | -------------------------------------------------------------------------------- /cloudconnexa/resource_route_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestAccCloudConnexaRoute_basic(t *testing.T) { 17 | rn := "cloudconnexa_route.test" 18 | ip, err := acctest.RandIpAddress("10.0.0.0/8") 19 | require.NoError(t, err) 20 | route := cloudconnexa.Route{ 21 | Description: "test" + acctest.RandString(10), 22 | Type: "IP_V4", 23 | Subnet: ip + "/32", 24 | } 25 | routeChanged := route 26 | routeChanged.Description = acctest.RandStringFromCharSet(10, alphabet) 27 | networkRandString := "test" + acctest.RandString(10) 28 | var routeId string 29 | 30 | check := func(r cloudconnexa.Route) resource.TestCheckFunc { 31 | return resource.ComposeTestCheckFunc( 32 | testAccCheckCloudConnexaRouteExists(rn, &routeId), 33 | resource.TestCheckResourceAttr(rn, "description", r.Description), 34 | resource.TestCheckResourceAttr(rn, "type", r.Type), 35 | resource.TestCheckResourceAttr(rn, "subnet", r.Subnet), 36 | ) 37 | } 38 | 39 | resource.Test(t, resource.TestCase{ 40 | PreCheck: func() { testAccPreCheck(t) }, 41 | ProviderFactories: testAccProviderFactories, 42 | CheckDestroy: testAccCheckCloudConnexaRouteDestroy, 43 | Steps: []resource.TestStep{ 44 | { 45 | Config: testAccCloudConnexaRouteConfig(route, networkRandString), 46 | Check: check(route), 47 | }, 48 | { 49 | Config: testAccCloudConnexaRouteConfig(routeChanged, networkRandString), 50 | Check: check(routeChanged), 51 | }, 52 | { 53 | ResourceName: rn, 54 | ImportState: true, 55 | ImportStateIdFunc: testAccCloudConnexaRouteImportStateIdFunc(rn), 56 | ImportStateVerify: true, 57 | }, 58 | }, 59 | }) 60 | } 61 | 62 | func testAccCheckCloudConnexaRouteDestroy(s *terraform.State) error { 63 | client := testAccProvider.Meta().(*cloudconnexa.Client) 64 | for _, rs := range s.RootModule().Resources { 65 | if rs.Type != "cloudconnexa_route" { 66 | continue 67 | } 68 | routeId := rs.Primary.ID 69 | r, err := client.Routes.Get(routeId) 70 | if err == nil { 71 | return err 72 | } 73 | if r != nil { 74 | return errors.New("route still exists") 75 | } 76 | } 77 | return nil 78 | } 79 | 80 | func testAccCheckCloudConnexaRouteExists(n string, routeID *string) resource.TestCheckFunc { 81 | return func(s *terraform.State) error { 82 | rs, ok := s.RootModule().Resources[n] 83 | if !ok { 84 | return fmt.Errorf("not found: %s", n) 85 | } 86 | 87 | if rs.Primary.ID == "" { 88 | return errors.New("no ID is set") 89 | } 90 | 91 | client := testAccProvider.Meta().(*cloudconnexa.Client) 92 | _, err := client.Routes.Get(rs.Primary.ID) 93 | if err != nil { 94 | return err 95 | } 96 | return nil 97 | } 98 | } 99 | 100 | func testAccCloudConnexaRouteImportStateIdFunc(n string) resource.ImportStateIdFunc { 101 | return func(s *terraform.State) (string, error) { 102 | rs, ok := s.RootModule().Resources[n] 103 | if !ok { 104 | return "", fmt.Errorf("not found: %s", n) 105 | } 106 | return rs.Primary.ID, nil 107 | } 108 | } 109 | 110 | func testAccCloudConnexaRouteConfig(r cloudconnexa.Route, networkRandStr string) string { 111 | return fmt.Sprintf(` 112 | provider "cloudconnexa" { 113 | base_url = "https://%[1]s.api.openvpn.com" 114 | } 115 | resource "cloudconnexa_network" "test" { 116 | name = "%[5]s" 117 | default_connector { 118 | name = "%[5]s" 119 | vpn_region_id = "fi-hel" 120 | } 121 | default_route { 122 | subnet = "10.1.2.0/24" 123 | type = "IP_V4" 124 | } 125 | } 126 | resource "cloudconnexa_route" "test" { 127 | network_item_id = cloudconnexa_network.test.id 128 | description = "%[2]s" 129 | subnet = "%[3]s" 130 | type = "%[4]s" 131 | } 132 | `, testCloudID, r.Description, r.Subnet, r.Type, networkRandStr) 133 | } 134 | -------------------------------------------------------------------------------- /cloudconnexa/resource_service.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/go-cty/cty" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 10 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 11 | ) 12 | 13 | var ( 14 | validValues = []string{"ANY", "BGP", "CUSTOM", "DHCP", "DNS", "FTP", "HTTP", "HTTPS", "IMAP", "IMAPS", "NTP", "POP3", "POP3S", "SMTP", "SMTPS", "SNMP", "SSH", "TELNET", "TFTP"} 15 | ) 16 | 17 | func resourceIPService() *schema.Resource { 18 | return &schema.Resource{ 19 | CreateContext: resourceIPServiceCreate, 20 | ReadContext: resourceServiceRead, 21 | DeleteContext: resourceServiceDelete, 22 | UpdateContext: resourceServiceUpdate, 23 | Schema: map[string]*schema.Schema{ 24 | "id": { 25 | Type: schema.TypeString, 26 | Computed: true, 27 | }, 28 | "name": { 29 | Type: schema.TypeString, 30 | Required: true, 31 | }, 32 | "description": { 33 | Type: schema.TypeString, 34 | Default: "Created by Terraform Cloud Connexa Provider", 35 | ValidateFunc: validation.StringLenBetween(1, 255), 36 | Optional: true, 37 | }, 38 | "type": { 39 | Type: schema.TypeString, 40 | Required: true, 41 | ValidateFunc: validation.StringInSlice([]string{"IP_SOURCE", "SERVICE_DESTINATION"}, false), 42 | }, 43 | "routes": { 44 | Type: schema.TypeList, 45 | Required: true, 46 | MinItems: 1, 47 | Elem: &schema.Schema{ 48 | Type: schema.TypeString, 49 | }, 50 | }, 51 | "config": { 52 | Type: schema.TypeList, 53 | MaxItems: 1, 54 | Optional: true, 55 | Elem: resourceServiceConfig(), 56 | }, 57 | "network_item_type": { 58 | Type: schema.TypeString, 59 | Required: true, 60 | ValidateFunc: validation.StringInSlice([]string{"NETWORK", "HOST"}, false), 61 | }, 62 | "network_item_id": { 63 | Type: schema.TypeString, 64 | Required: true, 65 | }, 66 | }, 67 | } 68 | } 69 | 70 | func resourceServiceUpdate(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 71 | c := i.(*cloudconnexa.Client) 72 | 73 | s, err := c.IPServices.Update(data.Id(), resourceDataToService(data)) 74 | if err != nil { 75 | return diag.FromErr(err) 76 | } 77 | setResourceData(data, s) 78 | return nil 79 | } 80 | 81 | func resourceServiceConfig() *schema.Resource { 82 | return &schema.Resource{ 83 | Schema: map[string]*schema.Schema{ 84 | "custom_service_types": { 85 | Type: schema.TypeList, 86 | Optional: true, 87 | Elem: &schema.Resource{ 88 | Schema: map[string]*schema.Schema{ 89 | "icmp_type": { 90 | Type: schema.TypeList, 91 | Required: true, 92 | Elem: &schema.Resource{ 93 | Schema: map[string]*schema.Schema{ 94 | "lower_value": { 95 | Type: schema.TypeInt, 96 | Required: true, 97 | }, 98 | "upper_value": { 99 | Type: schema.TypeInt, 100 | Required: true, 101 | }, 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | }, 108 | "service_types": { 109 | Type: schema.TypeList, 110 | Optional: true, 111 | Elem: &schema.Schema{ 112 | Type: schema.TypeString, 113 | ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { 114 | 115 | val := i.(string) 116 | for _, validValue := range validValues { 117 | if val == validValue { 118 | return nil 119 | } 120 | } 121 | return diag.Errorf("service type must be one of %s", validValues) 122 | }, 123 | }, 124 | }, 125 | }, 126 | } 127 | } 128 | 129 | func resourceServiceRead(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 130 | c := i.(*cloudconnexa.Client) 131 | var diags diag.Diagnostics 132 | service, err := c.IPServices.Get(data.Id()) 133 | if err != nil { 134 | return append(diags, diag.FromErr(err)...) 135 | } 136 | if service == nil { 137 | data.SetId("") 138 | return diags 139 | } 140 | setResourceData(data, service) 141 | return diags 142 | } 143 | 144 | func setResourceData(data *schema.ResourceData, service *cloudconnexa.IPServiceResponse) { 145 | data.SetId(service.Id) 146 | _ = data.Set("name", service.Name) 147 | _ = data.Set("description", service.Description) 148 | _ = data.Set("type", service.Type) 149 | _ = data.Set("routes", flattenRoutes(service.Routes)) 150 | _ = data.Set("config", flattenServiceConfig(service.Config)) 151 | _ = data.Set("network_item_type", service.NetworkItemType) 152 | _ = data.Set("network_item_id", service.NetworkItemId) 153 | } 154 | 155 | func resourceServiceDelete(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 156 | c := i.(*cloudconnexa.Client) 157 | var diags diag.Diagnostics 158 | err := c.IPServices.Delete(data.Id()) 159 | if err != nil { 160 | return append(diags, diag.FromErr(err)...) 161 | } 162 | return diags 163 | } 164 | 165 | func flattenServiceConfig(config *cloudconnexa.IPServiceConfig) interface{} { 166 | var data = map[string]interface{}{ 167 | "custom_service_types": flattenCustomServiceTypes(config.CustomServiceTypes), 168 | "service_types": config.ServiceTypes, 169 | } 170 | return []interface{}{data} 171 | } 172 | 173 | func flattenCustomServiceTypes(types []*cloudconnexa.CustomIPServiceType) interface{} { 174 | var data []interface{} 175 | for _, t := range types { 176 | data = append( 177 | data, 178 | map[string]interface{}{ 179 | "icmp_type": flattenIcmpType(t.IcmpType), 180 | }, 181 | ) 182 | } 183 | return data 184 | } 185 | 186 | func flattenIcmpType(icmpType []cloudconnexa.Range) interface{} { 187 | var data []interface{} 188 | for _, t := range icmpType { 189 | data = append( 190 | data, 191 | map[string]interface{}{ 192 | "lower_value": t.LowerValue, 193 | "upper_value": t.UpperValue, 194 | }, 195 | ) 196 | } 197 | return data 198 | } 199 | 200 | func flattenRoutes(routes []*cloudconnexa.Route) []string { 201 | var data []string 202 | for _, route := range routes { 203 | data = append( 204 | data, 205 | route.Subnet, 206 | ) 207 | } 208 | return data 209 | } 210 | 211 | func resourceIPServiceCreate(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { 212 | client := m.(*cloudconnexa.Client) 213 | 214 | service := resourceDataToService(data) 215 | createdService, err := client.IPServices.Create(service) 216 | if err != nil { 217 | return diag.FromErr(err) 218 | } 219 | setResourceData(data, createdService) 220 | return nil 221 | } 222 | 223 | func resourceDataToService(data *schema.ResourceData) *cloudconnexa.IPService { 224 | routes := data.Get("routes").([]interface{}) 225 | var configRoutes []*cloudconnexa.IPServiceRoute 226 | for _, r := range routes { 227 | configRoutes = append( 228 | configRoutes, 229 | &cloudconnexa.IPServiceRoute{ 230 | Value: r.(string), 231 | Description: "Managed by Terraform", 232 | }, 233 | ) 234 | } 235 | 236 | config := cloudconnexa.IPServiceConfig{} 237 | configList := data.Get("config").([]interface{}) 238 | if len(configList) > 0 && configList[0] != nil { 239 | 240 | config.CustomServiceTypes = []*cloudconnexa.CustomIPServiceType{} 241 | config.ServiceTypes = []string{} 242 | 243 | mainConfig := configList[0].(map[string]interface{}) 244 | for _, r := range mainConfig["custom_service_types"].([]interface{}) { 245 | cst := r.(map[string]interface{}) 246 | var icmpTypes []cloudconnexa.Range 247 | for _, r := range cst["icmp_type"].([]interface{}) { 248 | icmpType := r.(map[string]interface{}) 249 | icmpTypes = append( 250 | icmpTypes, 251 | cloudconnexa.Range{ 252 | LowerValue: icmpType["lower_value"].(int), 253 | UpperValue: icmpType["upper_value"].(int), 254 | }, 255 | ) 256 | } 257 | config.CustomServiceTypes = append( 258 | config.CustomServiceTypes, 259 | &cloudconnexa.CustomIPServiceType{ 260 | IcmpType: icmpTypes, 261 | }, 262 | ) 263 | } 264 | 265 | for _, r := range mainConfig["service_types"].([]interface{}) { 266 | config.ServiceTypes = append(config.ServiceTypes, r.(string)) 267 | } 268 | } 269 | 270 | s := &cloudconnexa.IPService{ 271 | Name: data.Get("name").(string), 272 | Description: data.Get("description").(string), 273 | NetworkItemId: data.Get("network_item_id").(string), 274 | NetworkItemType: data.Get("network_item_type").(string), 275 | Type: data.Get("type").(string), 276 | Routes: configRoutes, 277 | Config: &config, 278 | } 279 | return s 280 | } 281 | -------------------------------------------------------------------------------- /cloudconnexa/resource_service_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 7 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 12 | ) 13 | 14 | func TestAccCloudConnexaService_basic(t *testing.T) { 15 | rn := "cloudconnexa_service.test" 16 | networkName := acctest.RandStringFromCharSet(10, alphabet) 17 | service := cloudconnexa.IPService{ 18 | Name: acctest.RandStringFromCharSet(10, alphabet), 19 | } 20 | serviceChanged := service 21 | serviceChanged.Name = fmt.Sprintf("changed-%s", acctest.RandStringFromCharSet(10, alphabet)) 22 | 23 | check := func(service cloudconnexa.IPService) resource.TestCheckFunc { 24 | return resource.ComposeTestCheckFunc( 25 | testAccCheckCloudConnexaServiceExists(rn, networkName), 26 | resource.TestCheckResourceAttr(rn, "name", service.Name), 27 | ) 28 | } 29 | 30 | resource.Test(t, resource.TestCase{ 31 | PreCheck: func() { testAccPreCheck(t) }, 32 | ProviderFactories: testAccProviderFactories, 33 | CheckDestroy: testAccCheckCloudConnexaServiceDestroy, 34 | Steps: []resource.TestStep{ 35 | { 36 | Config: testAccCloudConnexaServiceConfig(service, networkName), 37 | Check: check(service), 38 | }, 39 | { 40 | Config: testAccCloudConnexaServiceConfig(serviceChanged, networkName), 41 | Check: check(serviceChanged), 42 | }, 43 | }, 44 | }) 45 | } 46 | 47 | func testAccCheckCloudConnexaServiceExists(rn, networkId string) resource.TestCheckFunc { 48 | return func(s *terraform.State) error { 49 | rs, ok := s.RootModule().Resources[rn] 50 | if !ok { 51 | return fmt.Errorf("not found: %s", rn) 52 | } 53 | 54 | if rs.Primary.ID == "" { 55 | return errors.New("no ID is set") 56 | } 57 | 58 | c := testAccProvider.Meta().(*cloudconnexa.Client) 59 | _, err := c.IPServices.Get(rs.Primary.ID) 60 | if err != nil { 61 | return err 62 | } 63 | return nil 64 | } 65 | } 66 | 67 | func testAccCheckCloudConnexaServiceDestroy(state *terraform.State) error { 68 | c := testAccProvider.Meta().(*cloudconnexa.Client) 69 | for _, rs := range state.RootModule().Resources { 70 | if rs.Type != "cloudconnexa_service" { 71 | continue 72 | } 73 | id := rs.Primary.Attributes["id"] 74 | s, err := c.IPServices.Get(id) 75 | if err == nil || s != nil { 76 | return fmt.Errorf("service still exists") 77 | } 78 | } 79 | return nil 80 | } 81 | func testAccCloudConnexaServiceConfig(service cloudconnexa.IPService, networkName string) string { 82 | return fmt.Sprintf(` 83 | provider "cloudconnexa" { 84 | base_url = "https://%s.api.openvpn.com" 85 | } 86 | 87 | resource "cloudconnexa_network" "test" { 88 | name = "%s" 89 | description = "test" 90 | 91 | default_connector { 92 | name = "%s" 93 | vpn_region_id = "fi-hel" 94 | } 95 | default_route { 96 | value = "10.1.2.0/24" 97 | type = "IP_V4" 98 | } 99 | } 100 | 101 | resource "cloudconnexa_ip_service" "test" { 102 | name = "%s" 103 | type = "SERVICE_DESTINATION" 104 | description = "test" 105 | network_item_type = "NETWORK" 106 | network_item_id = cloudconnexa_network.test.id 107 | routes = ["test.ua" ] 108 | config { 109 | service_types = ["ANY"] 110 | } 111 | } 112 | `, testCloudID, networkName, fmt.Sprintf("connector_%s", networkName), service.Name) 113 | } 114 | -------------------------------------------------------------------------------- /cloudconnexa/resource_user.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 8 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 9 | ) 10 | 11 | func resourceUser() *schema.Resource { 12 | return &schema.Resource{ 13 | Description: "Use `cloudconnexa_user` to create an Cloud Connexa user.", 14 | CreateContext: resourceUserCreate, 15 | ReadContext: resourceUserRead, 16 | UpdateContext: resourceUserUpdate, 17 | DeleteContext: resourceUserDelete, 18 | Importer: &schema.ResourceImporter{ 19 | StateContext: schema.ImportStatePassthroughContext, 20 | }, 21 | Schema: map[string]*schema.Schema{ 22 | "username": { 23 | Type: schema.TypeString, 24 | Required: true, 25 | ForceNew: true, 26 | ValidateFunc: validation.StringLenBetween(1, 120), 27 | Description: "A username for the user.", 28 | }, 29 | "email": { 30 | Type: schema.TypeString, 31 | Required: true, 32 | ValidateFunc: validation.StringLenBetween(1, 120), 33 | Description: "An invitation to Cloud Connexa account will be sent to this email. It will include an initial password and a VPN setup guide.", 34 | }, 35 | "first_name": { 36 | Type: schema.TypeString, 37 | Required: true, 38 | ValidateFunc: validation.StringLenBetween(1, 20), 39 | Description: "User's first name.", 40 | }, 41 | "last_name": { 42 | Type: schema.TypeString, 43 | Required: true, 44 | ValidateFunc: validation.StringLenBetween(1, 20), 45 | Description: "User's last name.", 46 | }, 47 | "group_id": { 48 | Type: schema.TypeString, 49 | Optional: true, 50 | Description: "The UUID of a user's group.", 51 | }, 52 | "role": { 53 | Type: schema.TypeString, 54 | Optional: true, 55 | ForceNew: true, 56 | Default: "MEMBER", 57 | Description: "The type of user role. Valid values are `ADMIN`, `MEMBER`, or `OWNER`.", 58 | }, 59 | "devices": { 60 | Type: schema.TypeList, 61 | Optional: true, 62 | ForceNew: true, 63 | MaxItems: 1, 64 | Description: "When a user signs in, the device that they use will be added to their account. You can read more at [Cloud Connexa Device](https://openvpn.net/cloud-docs/device/).", 65 | Elem: &schema.Resource{ 66 | Schema: map[string]*schema.Schema{ 67 | "name": { 68 | Type: schema.TypeString, 69 | Required: true, 70 | ValidateFunc: validation.StringLenBetween(1, 32), 71 | Description: "A device name.", 72 | }, 73 | "description": { 74 | Type: schema.TypeString, 75 | Required: true, 76 | ValidateFunc: validation.StringLenBetween(1, 120), 77 | Description: "A device description.", 78 | }, 79 | "ipv4_address": { 80 | Type: schema.TypeString, 81 | Optional: true, 82 | Description: "An IPv4 address of the device.", 83 | }, 84 | "ipv6_address": { 85 | Type: schema.TypeString, 86 | Optional: true, 87 | Description: "An IPv6 address of the device.", 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | } 94 | } 95 | 96 | func resourceUserCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 97 | c := m.(*cloudconnexa.Client) 98 | var diags diag.Diagnostics 99 | username := d.Get("username").(string) 100 | email := d.Get("email").(string) 101 | firstName := d.Get("first_name").(string) 102 | lastName := d.Get("last_name").(string) 103 | groupId := d.Get("group_id").(string) 104 | role := d.Get("role").(string) 105 | configDevices := d.Get("devices").([]interface{}) 106 | var devices []cloudconnexa.Device 107 | for _, d := range configDevices { 108 | device := d.(map[string]interface{}) 109 | devices = append( 110 | devices, 111 | cloudconnexa.Device{ 112 | Name: device["name"].(string), 113 | Description: device["description"].(string), 114 | IPv4Address: device["ipv4_address"].(string), 115 | IPv6Address: device["ipv6_address"].(string), 116 | }, 117 | ) 118 | 119 | } 120 | u := cloudconnexa.User{ 121 | Username: username, 122 | Email: email, 123 | FirstName: firstName, 124 | LastName: lastName, 125 | GroupId: groupId, 126 | Devices: devices, 127 | Role: role, 128 | } 129 | user, err := c.Users.Create(u) 130 | if err != nil { 131 | return append(diags, diag.FromErr(err)...) 132 | } 133 | d.SetId(user.Id) 134 | return append(diags, diag.Diagnostic{ 135 | Severity: diag.Warning, 136 | Summary: "The user's role cannot be changed using the code.", 137 | Detail: "There is a bug in Cloud Connexa API that prevents setting the user's role during the creation. All users are created as Members by default. Once it's fixed, the provider will be updated accordingly.", 138 | }) 139 | } 140 | 141 | func resourceUserRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 142 | c := m.(*cloudconnexa.Client) 143 | var diags diag.Diagnostics 144 | userId := d.Id() 145 | u, err := c.Users.Get(userId) 146 | 147 | // If group_id is not set, Cloud Connexa sets it to the default group. 148 | var groupId string 149 | if d.Get("group_id") == "" { 150 | // The group has not been explicitly set. 151 | // Set it to an empty string to keep the default group. 152 | groupId = "" 153 | } else { 154 | groupId = u.GroupId 155 | } 156 | 157 | if err != nil { 158 | return append(diags, diag.FromErr(err)...) 159 | } 160 | if u == nil { 161 | d.SetId("") 162 | } else { 163 | d.Set("username", u.Username) 164 | d.Set("email", u.Email) 165 | d.Set("first_name", u.FirstName) 166 | d.Set("last_name", u.LastName) 167 | d.Set("group_id", groupId) 168 | d.Set("devices", u.Devices) 169 | d.Set("role", u.Role) 170 | } 171 | return diags 172 | } 173 | 174 | func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 175 | c := m.(*cloudconnexa.Client) 176 | var diags diag.Diagnostics 177 | if !d.HasChanges("first_name", "last_name", "group_id", "email") { 178 | return diags 179 | } 180 | 181 | u, err := c.Users.Get(d.Id()) 182 | if err != nil { 183 | return append(diags, diag.FromErr(err)...) 184 | } 185 | 186 | _, email := d.GetChange("email") 187 | _, firstName := d.GetChange("first_name") 188 | _, lastName := d.GetChange("last_name") 189 | _, role := d.GetChange("role") 190 | status := u.Status 191 | oldGroupId, newGroupId := d.GetChange("group_id") 192 | 193 | groupId := newGroupId.(string) 194 | // If both are empty strings, then the group has not been set explicitly. 195 | // The update endpoint requires group_id to be set, so we should set it to the default group. 196 | if oldGroupId.(string) == "" && groupId == "" { 197 | g, err := c.UserGroups.GetByName("Default") 198 | if err != nil { 199 | return append(diags, diag.FromErr(err)...) 200 | } 201 | groupId = g.ID 202 | } 203 | 204 | err = c.Users.Update(cloudconnexa.User{ 205 | Id: d.Id(), 206 | Email: email.(string), 207 | FirstName: firstName.(string), 208 | LastName: lastName.(string), 209 | GroupId: groupId, 210 | Role: role.(string), 211 | Status: status, 212 | }) 213 | 214 | return append(diags, diag.FromErr(err)...) 215 | } 216 | 217 | func resourceUserDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 218 | c := m.(*cloudconnexa.Client) 219 | var diags diag.Diagnostics 220 | userId := d.Id() 221 | err := c.Users.Delete(userId) 222 | if err != nil { 223 | return append(diags, diag.FromErr(err)...) 224 | } 225 | return diags 226 | } 227 | -------------------------------------------------------------------------------- /cloudconnexa/resource_user_group.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" 8 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 9 | ) 10 | 11 | func resourceUserGroup() *schema.Resource { 12 | return &schema.Resource{ 13 | Description: "Use `cloudconnexa_user_group` to create an Cloud Connexa user group.", 14 | CreateContext: resourceUserGroupCreate, 15 | ReadContext: resourceUserGroupRead, 16 | UpdateContext: resourceUserGroupUpdate, 17 | DeleteContext: resourceUserGroupDelete, 18 | Importer: &schema.ResourceImporter{ 19 | StateContext: schema.ImportStatePassthroughContext, 20 | }, 21 | Schema: map[string]*schema.Schema{ 22 | "id": { 23 | Type: schema.TypeString, 24 | Computed: true, 25 | Description: "The ID of the user group.", 26 | }, 27 | "connect_auth": { 28 | Type: schema.TypeString, 29 | Optional: true, 30 | Default: "AUTO", 31 | ValidateFunc: validation.StringInSlice([]string{"AUTH", "AUTO", "STRICT_AUTH"}, false), 32 | }, 33 | "internet_access": { 34 | Type: schema.TypeString, 35 | Optional: true, 36 | Default: "LOCAL", 37 | ValidateFunc: validation.StringInSlice([]string{"LOCAL", "BLOCKED", "GLOBAL_INTERNET"}, false), 38 | }, 39 | "max_device": { 40 | Type: schema.TypeInt, 41 | Optional: true, 42 | Default: 3, 43 | Description: "The maximum number of devices that can be connected to the user group.", 44 | }, 45 | "name": { 46 | Type: schema.TypeString, 47 | Required: true, 48 | ValidateFunc: validation.StringLenBetween(1, 40), 49 | Description: "The name of the user group.", 50 | }, 51 | "system_subnets": { 52 | Type: schema.TypeList, 53 | Optional: true, 54 | Computed: true, 55 | Default: nil, 56 | Description: "A list of subnets that are accessible to the user group.", 57 | Elem: &schema.Schema{ 58 | Type: schema.TypeString, 59 | }, 60 | }, 61 | "vpn_region_ids": { 62 | Type: schema.TypeList, 63 | Required: true, 64 | MinItems: 1, 65 | Description: "A list of VPN regions that are accessible to the user group.", 66 | Elem: &schema.Schema{ 67 | Type: schema.TypeString, 68 | }, 69 | }, 70 | }, 71 | } 72 | } 73 | 74 | func resourceUserGroupUpdate(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 75 | c := i.(*cloudconnexa.Client) 76 | var diags diag.Diagnostics 77 | ug := resourceDataToUserGroup(data) 78 | 79 | userGroup, err := c.UserGroups.Update(data.Id(), ug) 80 | if err != nil { 81 | return append(diags, diag.FromErr(err)...) 82 | } 83 | 84 | if userGroup == nil { 85 | data.SetId("") 86 | } else { 87 | updateUserGroupData(data, userGroup) 88 | } 89 | 90 | return diags 91 | } 92 | 93 | func resourceDataToUserGroup(data *schema.ResourceData) *cloudconnexa.UserGroup { 94 | name := data.Get("name").(string) 95 | connectAuth := data.Get("connect_auth").(string) 96 | maxDevice := data.Get("max_device").(int) 97 | internetAccess := data.Get("internet_access").(string) 98 | configSystemSubnets := data.Get("system_subnets").([]interface{}) 99 | var systemSubnets []string 100 | for _, s := range configSystemSubnets { 101 | systemSubnets = append(systemSubnets, s.(string)) 102 | } 103 | configVpnRegionIds := data.Get("vpn_region_ids").([]interface{}) 104 | var vpnRegionIds []string 105 | for _, r := range configVpnRegionIds { 106 | vpnRegionIds = append(vpnRegionIds, r.(string)) 107 | } 108 | 109 | ug := &cloudconnexa.UserGroup{ 110 | Name: name, 111 | ConnectAuth: connectAuth, 112 | MaxDevice: maxDevice, 113 | SystemSubnets: systemSubnets, 114 | VpnRegionIds: vpnRegionIds, 115 | InternetAccess: internetAccess, 116 | } 117 | return ug 118 | } 119 | 120 | func updateUserGroupData(data *schema.ResourceData, userGroup *cloudconnexa.UserGroup) { 121 | data.SetId(userGroup.ID) 122 | _ = data.Set("connect_auth", userGroup.ConnectAuth) 123 | _ = data.Set("max_device", userGroup.MaxDevice) 124 | _ = data.Set("name", userGroup.Name) 125 | _ = data.Set("system_subnets", userGroup.SystemSubnets) 126 | _ = data.Set("vpn_region_ids", userGroup.VpnRegionIds) 127 | _ = data.Set("internet_access", userGroup.InternetAccess) 128 | } 129 | 130 | func resourceUserGroupDelete(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 131 | c := i.(*cloudconnexa.Client) 132 | var diags diag.Diagnostics 133 | err := c.UserGroups.Delete(data.Id()) 134 | if err != nil { 135 | return append(diags, diag.FromErr(err)...) 136 | } 137 | data.SetId("") 138 | return diags 139 | } 140 | 141 | func resourceUserGroupRead(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { 142 | c := i.(*cloudconnexa.Client) 143 | var diags diag.Diagnostics 144 | userGroup, err := c.UserGroups.Get(data.Id()) 145 | if err != nil { 146 | return append(diags, diag.FromErr(err)...) 147 | } 148 | 149 | if userGroup == nil { 150 | data.SetId("") 151 | } else { 152 | updateUserGroupData(data, userGroup) 153 | } 154 | return diags 155 | } 156 | 157 | func resourceUserGroupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { 158 | c := m.(*cloudconnexa.Client) 159 | var diags diag.Diagnostics 160 | ug := resourceDataToUserGroup(d) 161 | 162 | userGroup, err := c.UserGroups.Create(ug) 163 | if err != nil { 164 | return append(diags, diag.FromErr(err)...) 165 | } 166 | updateUserGroupData(d, userGroup) 167 | return diags 168 | } 169 | -------------------------------------------------------------------------------- /cloudconnexa/resource_user_group_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 8 | "testing" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 12 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 13 | ) 14 | 15 | func TestAccCloudConnexaUserGroup_basic(t *testing.T) { 16 | rn := "cloudconnexa_user_group.test" 17 | userGroup := cloudconnexa.UserGroup{ 18 | Name: acctest.RandStringFromCharSet(10, alphabet), 19 | VpnRegionIds: []string{ 20 | "us-east-1", 21 | }, 22 | } 23 | userGroupChanged := userGroup 24 | userGroupChanged.Name = fmt.Sprintf("changed-%s", acctest.RandStringFromCharSet(10, alphabet)) 25 | 26 | check := func(userGroup cloudconnexa.UserGroup) resource.TestCheckFunc { 27 | return resource.ComposeTestCheckFunc( 28 | testAccCheckCloudConnexaUserGroupExists(rn), 29 | resource.TestCheckResourceAttr(rn, "name", userGroup.Name), 30 | resource.TestCheckResourceAttr(rn, "vpn_region_ids.0", userGroup.VpnRegionIds[0]), 31 | ) 32 | } 33 | 34 | resource.Test(t, resource.TestCase{ 35 | PreCheck: func() { testAccPreCheck(t) }, 36 | ProviderFactories: testAccProviderFactories, 37 | CheckDestroy: testAccCheckCloudConnexaUserGroupDestroy, 38 | Steps: []resource.TestStep{ 39 | { 40 | Config: testAccCloudConnexaUserGroupConfig(userGroup), 41 | Check: check(userGroup), 42 | }, 43 | { 44 | Config: testAccCloudConnexaUserGroupConfig(userGroupChanged), 45 | Check: check(userGroupChanged), 46 | }, 47 | { 48 | ResourceName: rn, 49 | ImportState: true, 50 | ImportStateIdFunc: testAccCloudConnexaUserImportStateIdFunc(rn), 51 | ImportStateVerify: true, 52 | }, 53 | }, 54 | }) 55 | } 56 | 57 | func testAccCheckCloudConnexaUserGroupDestroy(s *terraform.State) error { 58 | c := testAccProvider.Meta().(*cloudconnexa.Client) 59 | for _, rs := range s.RootModule().Resources { 60 | if rs.Type != "cloudconnexa_user_group" { 61 | continue 62 | } 63 | username := rs.Primary.Attributes["username"] 64 | u, err := c.UserGroups.GetByName(username) 65 | if err == nil { 66 | if u != nil { 67 | return errors.New("user still exists") 68 | } 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | func testAccCheckCloudConnexaUserGroupExists(rn string) resource.TestCheckFunc { 75 | return func(s *terraform.State) error { 76 | rs, ok := s.RootModule().Resources[rn] 77 | if !ok { 78 | return fmt.Errorf("not found: %s", rn) 79 | } 80 | 81 | if rs.Primary.ID == "" { 82 | return errors.New("no ID is set") 83 | } 84 | 85 | c := testAccProvider.Meta().(*cloudconnexa.Client) 86 | _, err := c.UserGroups.Get(rs.Primary.ID) 87 | if err != nil { 88 | return err 89 | } 90 | return nil 91 | } 92 | } 93 | 94 | func testAccCloudConnexaUserGroupConfig(userGroup cloudconnexa.UserGroup) string { 95 | idsStr, _ := json.Marshal(userGroup.VpnRegionIds) 96 | 97 | return fmt.Sprintf(` 98 | provider "cloudconnexa" { 99 | base_url = "https://%s.api.openvpn.com" 100 | } 101 | resource "cloudconnexa_user_group" "test" { 102 | name = "%s" 103 | vpn_region_ids = %s 104 | 105 | } 106 | `, testCloudID, userGroup.Name, idsStr) 107 | } 108 | -------------------------------------------------------------------------------- /cloudconnexa/resource_user_test.go: -------------------------------------------------------------------------------- 1 | package cloudconnexa 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 7 | "testing" 8 | 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 11 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 12 | ) 13 | 14 | func TestAccCloudConnexaUser_basic(t *testing.T) { 15 | rn := "cloudconnexa_user.test" 16 | user := cloudconnexa.User{ 17 | Username: acctest.RandStringFromCharSet(10, alphabet), 18 | FirstName: acctest.RandStringFromCharSet(10, alphabet), 19 | LastName: acctest.RandStringFromCharSet(10, alphabet), 20 | Email: fmt.Sprintf("terraform-tests+%s@devopenvpn.in", acctest.RandString(10)), 21 | } 22 | userChanged := user 23 | userChanged.Email = fmt.Sprintf("terraform-tests+changed%s@devopenvpn.in", acctest.RandString(10)) 24 | var userID string 25 | 26 | check := func(user cloudconnexa.User) resource.TestCheckFunc { 27 | return resource.ComposeTestCheckFunc( 28 | testAccCheckCloudConnexaUserExists(rn, &userID), 29 | resource.TestCheckResourceAttr(rn, "username", user.Username), 30 | resource.TestCheckResourceAttr(rn, "email", user.Email), 31 | resource.TestCheckResourceAttr(rn, "first_name", user.FirstName), 32 | resource.TestCheckResourceAttr(rn, "last_name", user.LastName), 33 | ) 34 | } 35 | 36 | resource.Test(t, resource.TestCase{ 37 | PreCheck: func() { testAccPreCheck(t) }, 38 | ProviderFactories: testAccProviderFactories, 39 | CheckDestroy: testAccCheckCloudConnexaUserDestroy, 40 | Steps: []resource.TestStep{ 41 | { 42 | Config: testAccCloudConnexaUserConfig(user), 43 | Check: check(user), 44 | }, 45 | { 46 | Config: testAccCloudConnexaUserConfig(userChanged), 47 | Check: check(userChanged), 48 | }, 49 | { 50 | ResourceName: rn, 51 | ImportState: true, 52 | ImportStateIdFunc: testAccCloudConnexaUserImportStateIdFunc(rn), 53 | ImportStateVerify: true, 54 | }, 55 | }, 56 | }) 57 | } 58 | 59 | func testAccCheckCloudConnexaUserDestroy(s *terraform.State) error { 60 | client := testAccProvider.Meta().(*cloudconnexa.Client) 61 | for _, rs := range s.RootModule().Resources { 62 | if rs.Type != "cloudconnexa_user" { 63 | continue 64 | } 65 | username := rs.Primary.Attributes["username"] 66 | u, err := client.Users.Get(username) 67 | if err == nil { 68 | if u != nil { 69 | return errors.New("user still exists") 70 | } 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | func testAccCheckCloudConnexaUserExists(n string, teamID *string) resource.TestCheckFunc { 77 | return func(s *terraform.State) error { 78 | rs, ok := s.RootModule().Resources[n] 79 | if !ok { 80 | return fmt.Errorf("not found: %s", n) 81 | } 82 | 83 | if rs.Primary.ID == "" { 84 | return errors.New("no ID is set") 85 | } 86 | 87 | client := testAccProvider.Meta().(*cloudconnexa.Client) 88 | _, err := client.Users.Get(rs.Primary.ID) 89 | if err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | } 95 | 96 | func testAccCloudConnexaUserImportStateIdFunc(n string) resource.ImportStateIdFunc { 97 | return func(s *terraform.State) (string, error) { 98 | rs, ok := s.RootModule().Resources[n] 99 | if !ok { 100 | return "", fmt.Errorf("not found: %s", n) 101 | } 102 | return rs.Primary.ID, nil 103 | } 104 | } 105 | 106 | func testAccCloudConnexaUserConfig(user cloudconnexa.User) string { 107 | return fmt.Sprintf(` 108 | provider "cloudconnexa" { 109 | base_url = "https://%s.api.openvpn.com" 110 | } 111 | resource "cloudconnexa_user" "test" { 112 | username = "%s" 113 | email = "%s" 114 | first_name = "%s" 115 | last_name = "%s" 116 | } 117 | `, testCloudID, user.Username, user.Email, user.FirstName, user.LastName) 118 | } 119 | -------------------------------------------------------------------------------- /docs/data-sources/connector.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_connector Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use an cloudconnexa_connector data source to read an existing Cloud Connexa connector. 7 | --- 8 | 9 | # cloudconnexa_connector (Data Source) 10 | 11 | Use an `cloudconnexa_connector` data source to read an existing Cloud Connexa connector. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The name of the connector. 21 | 22 | ### Read-Only 23 | 24 | - `id` (String) The ID of this resource. 25 | - `ip_v4_address` (String) The IPV4 address of the connector. 26 | - `ip_v6_address` (String) The IPV6 address of the connector. 27 | - `network_item_id` (String) The id of the network or host with which the connector is associated. 28 | - `network_item_type` (String) The network object type of the connector. This typically will be set to either `NETWORK` or `HOST`. 29 | - `vpn_region_id` (String) The id of the region where the connector is deployed. 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/data-sources/host.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_host Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use an cloudconnexa_host data source to read an existing Cloud Connexa connector. 7 | --- 8 | 9 | # cloudconnexa_host (Data Source) 10 | 11 | Use an `cloudconnexa_host` data source to read an existing Cloud Connexa connector. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The name of the host. 21 | 22 | ### Read-Only 23 | 24 | - `connectors` (List of Object) The list of connectors to be associated with this host. (see [below for nested schema](#nestedatt--connectors)) 25 | - `id` (String) The ID of this resource. 26 | - `internet_access` (String) The type of internet access provided. 27 | - `system_subnets` (List of String) The IPV4 and IPV6 subnets automatically assigned to this host. 28 | 29 | 30 | ### Nested Schema for `connectors` 31 | 32 | Read-Only: 33 | 34 | - `id` (String) The connector id. 35 | - `ip_v4_address` (String) The IPV4 address of the connector. 36 | - `ip_v6_address` (String) The IPV6 address of the connector. 37 | - `name` (String) The connector name. 38 | - `network_item_id` (String) The id of the host with which the connector is associated. 39 | - `network_item_type` (String) The network object type of the connector. This typically will be set to `HOST`. 40 | - `vpn_region_id` (String) The id of the region where the connector is deployed. 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/data-sources/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_network Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use a cloudconnexa_network data source to read an Cloud Connexa network. 7 | --- 8 | 9 | # cloudconnexa_network (Data Source) 10 | 11 | Use a `cloudconnexa_network` data source to read an Cloud Connexa network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The network name. 21 | 22 | ### Read-Only 23 | 24 | - `connectors` (List of Object) The list of connectors associated with this network. (see [below for nested schema](#nestedatt--connectors)) 25 | - `egress` (Boolean) Boolean to indicate whether this network provides an egress or not. 26 | - `id` (String) The ID of this resource. 27 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 28 | - `network_id` (String) The network ID. 29 | - `routes` (List of Object) The routes associated with this network. (see [below for nested schema](#nestedatt--routes)) 30 | - `system_subnets` (List of String) The IPV4 and IPV6 subnets automatically assigned to this network. 31 | 32 | 33 | ### Nested Schema for `connectors` 34 | 35 | Read-Only: 36 | 37 | - `id` (String) The connector id. 38 | - `ip_v4_address` (String) The IPV4 address of the connector. 39 | - `ip_v6_address` (String) The IPV6 address of the connector. 40 | - `name` (String) The connector name. 41 | - `network_item_id` (String) The id of the network with which the connector is associated. 42 | - `network_item_type` (String) The network object type of the connector. This typically will be set to `NETWORK`. 43 | - `vpn_region_id` (String) The id of the region where the connector is deployed. 44 | 45 | 46 | 47 | ### Nested Schema for `routes` 48 | 49 | Read-Only: 50 | 51 | - `id` (String) The route id. 52 | - `subnet` (String) The value of the route, either an IPV4 address, an IPV6 address, or a DNS hostname. 53 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/data-sources/network_routes.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_network_routes Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use an cloudconnexa_network_routes data source to read all the routes associated with an Cloud Connexa network. 7 | --- 8 | 9 | # cloudconnexa_network_routes (Data Source) 10 | 11 | Use an `cloudconnexa_network_routes` data source to read all the routes associated with an Cloud Connexa network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `network_item_id` (String) The id of the Cloud Connexa network of the routes to be discovered. 21 | 22 | ### Read-Only 23 | 24 | - `id` (String) The ID of this resource. 25 | - `routes` (List of Object) The list of routes. (see [below for nested schema](#nestedatt--routes)) 26 | 27 | 28 | ### Nested Schema for `routes` 29 | 30 | Read-Only: 31 | 32 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 33 | - `value` (String) The value of the route, either an IPV4 address, an IPV6 address, or a DNS hostname. 34 | 35 | 36 | -------------------------------------------------------------------------------- /docs/data-sources/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_user Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use a cloudconnexa_user data source to read a specific Cloud Connexa user. 7 | --- 8 | 9 | # cloudconnexa_user (Data Source) 10 | 11 | Use a `cloudconnexa_user` data source to read a specific Cloud Connexa user. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `role` (String) The type of user role. Valid values are `ADMIN`, `MEMBER`, or `OWNER`. 21 | - `username` (String) The username of the user. 22 | 23 | ### Read-Only 24 | 25 | - `auth_type` (String) The authentication type of the user. 26 | - `devices` (List of Object) The list of user devices. (see [below for nested schema](#nestedatt--devices)) 27 | - `email` (String) The email address of the user. 28 | - `first_name` (String) The user's first name. 29 | - `group_id` (String) The user's group id. 30 | - `id` (String) The ID of this resource. 31 | - `last_name` (String) The user's last name. 32 | - `status` (String) The user's status. 33 | - `user_id` (String) The ID of this resource. 34 | 35 | 36 | ### Nested Schema for `devices` 37 | 38 | Read-Only: 39 | 40 | - `description` (String) The device's description. 41 | - `id` (String) The device's id. 42 | - `ip_v4_address` (String) The device's IPV4 address. 43 | - `ip_v6_address` (String) The device's IPV6 address. 44 | - `name` (String) The device's name. 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/data-sources/user_group.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_user_group Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use an cloudconnexa_user_group data source to read an Cloud Connexa user group. 7 | --- 8 | 9 | # cloudconnexa_user_group (Data Source) 10 | 11 | Use an `cloudconnexa_user_group` data source to read an Cloud Connexa user group. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The user group name. 21 | 22 | ### Read-Only 23 | 24 | - `id` (String) The ID of this resource. 25 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 26 | - `max_device` (Number) The maximum number of devices per user. 27 | - `system_subnets` (List of String) The IPV4 and IPV6 addresses of the subnets associated with this user group. 28 | - `user_group_id` (String) The user group ID. 29 | - `vpn_region_ids` (List of String) The list of VPN region IDs this user group is associated with. 30 | 31 | 32 | -------------------------------------------------------------------------------- /docs/data-sources/vpn_region.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_vpn_region Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use a cloudconnexa_vpn_region data source to read an Cloud Connexa VPN region. 7 | --- 8 | 9 | # cloudconnexa_vpn_region (Data Source) 10 | 11 | Use a `cloudconnexa_vpn_region` data source to read an Cloud Connexa VPN region. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `region_id` (String) The id of the region. 21 | 22 | ### Read-Only 23 | 24 | - `continent` (String) The continent of the region. 25 | - `country` (String) The country of the region. 26 | - `country_iso` (String) The ISO code of the country of the region. 27 | - `id` (String) The ID of this resource. 28 | - `region_name` (String) The name of the region. 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa Provider" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # CloudConnexa Provider 10 | 11 | !> **WARNING:** This provider is experimental and support for it is on a best-effort basis. Additionally, the underlying API for Cloud Connexa is on Beta, which means that future versions of the provider may introduce breaking changes. Should that happen, migration documentation and support will also be provided on a best-effort basis. 12 | 13 | Use this provider to interact with the [Cloud Connexa API](https://openvpn.net/cloud-docs/developer/index.html). 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - **base_url** (String) The base url of your Cloud Connexa account. 21 | 22 | ### Optional 23 | 24 | - **client_id** (String, Sensitive) If not provided, it will default to the value of the `CLOUDCONNEXA_CLIENT_ID` environment variable. 25 | - **client_secret** (String, Sensitive) If not provided, it will default to the value of the `CLOUDCONNEXA_CLIENT_SECRET` environment variable. 26 | 27 | ### Credentials 28 | 29 | To authenticate with the Cloud Connexa API, you'll need the client_id and client_secret. 30 | These credentials can be found in the Cloud Connexa Portal. 31 | Go to the Settings page and click on the API tab. 32 | From there, you can enable the API and generate new authentication credentials. 33 | Additionally, you'll find Swagger documentation for the API in the same location. 34 | 35 | More documentation on the OpenVPN API can be found here: 36 | [Cloud Connexa API Documentation](https://openvpn.net/cloud-docs/developer/cloudconnexa-api.html) -------------------------------------------------------------------------------- /docs/resources/connector.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_connector Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_connector to create an Cloud Connexa connector. 7 | ~> NOTE: This only creates the Cloud Connexa connector object. Additional manual steps are required to associate a host in your infrastructure with the connector. Go to https://openvpn.net/cloud-docs/connector/ for more information. 8 | --- 9 | 10 | # cloudconnexa_connector (Resource) 11 | 12 | Use `cloudconnexa_connector` to create an Cloud Connexa connector. 13 | 14 | ~> NOTE: This only creates the Cloud Connexa connector object. Additional manual steps are required to associate a host in your infrastructure with the connector. Go to https://openvpn.net/cloud-docs/connector/ for more information. 15 | 16 | 17 | 18 | 19 | ## Schema 20 | 21 | ### Required 22 | 23 | - `name` (String) The connector display name. 24 | - `network_item_id` (String) The id of the network with which this connector is associated. 25 | - `network_item_type` (String) The type of network item of the connector. Supported values are `HOST` and `NETWORK`. 26 | - `vpn_region_id` (String) The id of the region where the connector will be deployed. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of this resource. 31 | - `ip_v4_address` (String) The IPV4 address of the connector. 32 | - `ip_v6_address` (String) The IPV6 address of the connector. 33 | 34 | ## Import 35 | 36 | A connector can be imported using the connector ID, which can be fetched directly from the API. 37 | 38 | ``` 39 | terraform import cloudconnexa_connector.connector 40 | ``` 41 | 42 | ~> NOTE: If the Terraform resource settings are different from the imported connector, the next time you run `terraform apply` the provider will attempt to delete and recreate the connector, which will require you to re-configure the instance manually. -------------------------------------------------------------------------------- /docs/resources/dns_record.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_dns_record Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_dns_record to create a DNS record on your VPN. 7 | --- 8 | 9 | # cloudconnexa_dns_record (Resource) 10 | 11 | Use `cloudconnexa_dns_record` to create a DNS record on your VPN. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `domain` (String) The DNS record name. 21 | 22 | ### Optional 23 | 24 | - `ip_v4_addresses` (List of String) The list of IPV4 addresses to which this record will resolve. 25 | - `ip_v6_addresses` (List of String) The list of IPV6 addresses to which this record will resolve. 26 | 27 | ### Read-Only 28 | 29 | - `id` (String) The ID of this resource. 30 | 31 | ## Import 32 | 33 | A connector can be imported using the DNS record ID, which can be fetched directly from the API. 34 | 35 | ``` 36 | terraform import cloudconnexa_dns_record.record 37 | ``` -------------------------------------------------------------------------------- /docs/resources/host.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_host Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_host to create an Cloud Connexa host. 7 | --- 8 | 9 | # cloudconnexa_host (Resource) 10 | 11 | Use `cloudconnexa_host` to create an Cloud Connexa host. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `connector` (Block Set, Min: 1) The set of connectors to be associated with this host. Can be defined more than once. (see [below for nested schema](#nestedblock--connector)) 21 | - `name` (String) The display name of the host. 22 | 23 | ### Optional 24 | 25 | - `description` (String) The description for the UI. Defaults to `Managed by Terraform`. 26 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of this resource. 31 | - `system_subnets` (Set of String) The IPV4 and IPV6 subnets automatically assigned to this host. 32 | 33 | 34 | ### Nested Schema for `connector` 35 | 36 | Required: 37 | 38 | - `name` (String) Name of the connector associated with this host. 39 | - `vpn_region_id` (String) The id of the region where the connector will be deployed. 40 | 41 | Read-Only: 42 | 43 | - `id` (String) The ID of this resource. 44 | - `ip_v4_address` (String) The IPV4 address of the connector. 45 | - `ip_v6_address` (String) The IPV6 address of the connector. 46 | - `network_item_id` (String) The host id. 47 | - `network_item_type` (String) The network object type. This typically will be set to `HOST`. 48 | 49 | ## Import 50 | 51 | A host can be imported using the DNS record ID, which can be fetched directly from the API. 52 | 53 | ``` 54 | terraform import cloudconnexa_host.host 55 | ``` -------------------------------------------------------------------------------- /docs/resources/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_network Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_network to create an Cloud Connexa Network. 7 | --- 8 | 9 | # cloudconnexa_network (Resource) 10 | 11 | Use `cloudconnexa_network` to create an Cloud Connexa Network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `default_connector` (Block List, Min: 1, Max: 1) The default connector of this network. (see [below for nested schema](#nestedblock--default_connector)) 21 | - `default_route` (Block List, Min: 1, Max: 1) The default route of this network. (see [below for nested schema](#nestedblock--default_route)) 22 | - `name` (String) The display name of the network. 23 | 24 | ### Optional 25 | 26 | - `description` (String) The display description for this resource. Defaults to `Managed by Terraform`. 27 | - `egress` (Boolean) Boolean to control whether this network provides an egress or not. 28 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 29 | 30 | ### Read-Only 31 | 32 | - `id` (String) The ID of this resource. 33 | - `system_subnets` (Set of String) The IPV4 and IPV6 subnets automatically assigned to this network. 34 | 35 | 36 | ### Nested Schema for `default_connector` 37 | 38 | Required: 39 | 40 | - `name` (String) Name of the connector automatically created and attached to this network. 41 | - `vpn_region_id` (String) The id of the region where the default connector will be deployed. 42 | 43 | Read-Only: 44 | 45 | - `id` (String) The ID of this resource. 46 | - `ip_v4_address` (String) The IPV4 address of the default connector. 47 | - `ip_v6_address` (String) The IPV6 address of the default connector. 48 | - `network_item_id` (String) The parent network id. 49 | - `network_item_type` (String) The network object type. This typically will be set to `NETWORK`. 50 | 51 | 52 | 53 | ### Nested Schema for `default_route` 54 | 55 | Required: 56 | 57 | - `value` (String) The target value of the default route. 58 | 59 | Optional: 60 | 61 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 62 | 63 | Read-Only: 64 | 65 | - `id` (String) The ID of this resource. 66 | 67 | A network can be imported using the network ID, which can be fetched directly from the API. 68 | 69 | ``` 70 | terraform import cloudconnexa_network.network 71 | ``` 72 | 73 | ~> NOTE: This will only import the network itslef, but it'll create a new connector and a new route as its defaults. There is currently no way to import an existing connector and route along with a network. The existing connector(s)/route(s) will continue to work, but you'll need to set a `default_connector` and a `default_route` that don't collide with your existing resources. -------------------------------------------------------------------------------- /docs/resources/route.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_route Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_route to create a route on an Cloud Connexa network. 7 | --- 8 | 9 | # cloudconnexa_route (Resource) 10 | 11 | Use `cloudconnexa_route` to create a route on an Cloud Connexa network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `network_item_id` (String) The id of the network on which to create the route. 21 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 22 | - `value` (String) The target value of the default route. 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) The ID of this resource. 27 | 28 | ## Import 29 | 30 | A route can be imported using the route ID, which can be fetched directly from the API. 31 | 32 | ``` 33 | terraform import cloudconnexa_route.route 34 | ``` -------------------------------------------------------------------------------- /docs/resources/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_user Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_user to create an Cloud Connexa user. 7 | --- 8 | 9 | # cloudconnexa_user (Resource) 10 | 11 | Use `cloudconnexa_user` to create an Cloud Connexa user. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `email` (String) An invitation to Cloud Connexa account will be sent to this email. It will include an initial password and a VPN setup guide. 21 | - `first_name` (String) User's first name. 22 | - `last_name` (String) User's last name. 23 | - `username` (String) A username for the user. 24 | 25 | ### Optional 26 | 27 | - `devices` (Block List, Max: 1) When a user signs in, the device that they use will be added to their account. You can read more at [Cloud Connexa Device](https://openvpn.net/cloud-docs/device/). (see [below for nested schema](#nestedblock--devices)) 28 | - `group_id` (String) The UUID of a user's group. 29 | 30 | ### Read-Only 31 | 32 | - `id` (String) The ID of this resource. 33 | 34 | 35 | ### Nested Schema for `devices` 36 | 37 | Required: 38 | 39 | - `description` (String) A device description. 40 | - `name` (String) A device name. 41 | 42 | Optional: 43 | 44 | - `ipv4_address` (String) An IPv4 address of the device. 45 | - `ipv6_address` (String) An IPv6 address of the device. 46 | 47 | ## Import 48 | 49 | A user can be imported using the user ID using the format below. 50 | 51 | ``` 52 | terraform import cloudconnexa_user.user username@accountname 53 | ``` 54 | -------------------------------------------------------------------------------- /e2e/integration_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "github.com/OpenVPN/terraform-provider-openvpn-cloud/cloudconnexa" 6 | "github.com/gruntwork-io/terratest/modules/terraform" 7 | api "github.com/openvpn/cloudconnexa-go-client/v2/cloudconnexa" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "os" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | const ( 16 | CloudConnexaHostKey = "OVPN_HOST" 17 | ) 18 | 19 | func TestCreationDeletion(t *testing.T) { 20 | validateEnvVars(t) 21 | 22 | terraformOptions := &terraform.Options{ 23 | 24 | NoColor: os.Getenv("NO_COLOR") == "1", 25 | 26 | // The path to where our Terraform code is located 27 | TerraformDir: "./setup", 28 | 29 | // Variables to pass to our Terraform code using -var options 30 | Vars: map[string]interface{}{}, 31 | } 32 | 33 | // At the end of the test, run `terraform destroy` to clean up any resources that were created 34 | t.Cleanup(func() { 35 | terraform.Destroy(t, terraformOptions) 36 | }) 37 | 38 | // This will run `terraform init` and `terraform apply` and fail the test if there are any errors 39 | terraform.InitAndApply(t, terraformOptions) 40 | 41 | // Run `terraform output` to get the value of an output variable 42 | hostID := terraform.Output(t, terraformOptions, "host_id") 43 | connectorID := terraform.Output(t, terraformOptions, "connector_id") 44 | 45 | assert.NotEmpty(t, hostID) 46 | assert.NotEmpty(t, connectorID) 47 | 48 | client, err := api.NewClient( 49 | os.Getenv(CloudConnexaHostKey), 50 | os.Getenv(cloudconnexa.ClientIDEnvVar), 51 | os.Getenv(cloudconnexa.ClientSecretEnvVar), 52 | ) 53 | require.NoError(t, err) 54 | 55 | // Total waiting time: 1min 56 | totalAttempts := 10 57 | attemptWaitingTime := 6 * time.Second 58 | 59 | connectorWasOnline := false 60 | for i := 0; i < totalAttempts; i++ { 61 | t.Logf("Waiting for connector to be online (%d/%d)", i+1, totalAttempts) 62 | connector, err := client.Connectors.GetByID(connectorID) 63 | require.NoError(t, err, "Invalid connector ID in output") 64 | if connector.ConnectionStatus == "online" { 65 | connectorWasOnline = true 66 | break 67 | } 68 | time.Sleep(attemptWaitingTime) 69 | } 70 | assert.True(t, connectorWasOnline) 71 | } 72 | 73 | func validateEnvVars(t *testing.T) { 74 | validateEnvVar(t, CloudConnexaHostKey) 75 | validateEnvVar(t, cloudconnexa.ClientIDEnvVar) 76 | validateEnvVar(t, cloudconnexa.ClientSecretEnvVar) 77 | } 78 | 79 | func validateEnvVar(t *testing.T, envVar string) { 80 | fmt.Println(os.Getenv(envVar)) 81 | require.NotEmptyf(t, os.Getenv(envVar), "%s must be set for acceptance tests", envVar) 82 | } 83 | -------------------------------------------------------------------------------- /e2e/setup/ec2.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = "eu-central-1" 3 | default_tags { 4 | tags = { 5 | task-group = "terraform-provider-openvpn-cloud" 6 | created-by = "Terraform/terraform-provider-openvpn-cloud" 7 | } 8 | } 9 | } 10 | 11 | data "aws_ami" "ubuntu" { 12 | most_recent = true 13 | 14 | filter { 15 | name = "name" 16 | values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] 17 | } 18 | 19 | filter { 20 | name = "virtualization-type" 21 | values = ["hvm"] 22 | } 23 | 24 | owners = ["099720109477"] # Canonical 25 | } 26 | 27 | data "aws_vpc" "default" { 28 | default = true 29 | } 30 | 31 | data "template_file" "init" { 32 | template = file("${path.module}/user_data.sh.tpl") 33 | 34 | vars = { 35 | profile = local.connector_profile 36 | } 37 | } 38 | 39 | resource "aws_instance" "example" { 40 | ami = data.aws_ami.ubuntu.id 41 | instance_type = "t3.micro" 42 | 43 | user_data = data.template_file.init.rendered 44 | key_name = aws_key_pair.key.key_name 45 | 46 | security_groups = [aws_security_group.example.name] 47 | 48 | tags = { 49 | Name = "Terraform OpenVPN Provider Example for ${var.host_name}" 50 | } 51 | } 52 | 53 | resource "aws_security_group" "example" { 54 | name = "${var.host_name}-sg" 55 | description = "Terraform Provider Example Security Group for ${var.host_name}" 56 | vpc_id = data.aws_vpc.default.id 57 | 58 | // To Allow SSH Transport 59 | ingress { 60 | from_port = 22 61 | protocol = "tcp" 62 | to_port = 22 63 | cidr_blocks = ["0.0.0.0/0"] 64 | } 65 | 66 | // To Allow Port 80 Transport 67 | ingress { 68 | from_port = 80 69 | protocol = "tcp" 70 | to_port = 80 71 | cidr_blocks = ["0.0.0.0/0"] 72 | } 73 | 74 | egress { 75 | from_port = 0 76 | to_port = 0 77 | protocol = "-1" 78 | cidr_blocks = ["0.0.0.0/0"] 79 | } 80 | 81 | lifecycle { 82 | create_before_destroy = true 83 | } 84 | tags = {} 85 | } 86 | 87 | resource "aws_key_pair" "key" { 88 | key_name = "${var.host_name}-key" 89 | public_key = file("~/.ssh/id_rsa.pub") 90 | tags = {} 91 | } 92 | 93 | output "instance_id" { 94 | value = aws_instance.example.id 95 | } 96 | 97 | output "instance_public_ip" { 98 | value = aws_instance.example.public_ip 99 | } 100 | 101 | output "instance_private_ip" { 102 | value = aws_instance.example.private_ip 103 | } 104 | -------------------------------------------------------------------------------- /e2e/setup/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | openvpn-cloud = { 4 | version = "0.0.11" 5 | source = "cloudconnexa.dev/openvpn/openvpncloud" 6 | } 7 | } 8 | } 9 | 10 | provider "openvpn-cloud" { 11 | base_url = "" 12 | } 13 | 14 | variable "host_name" { 15 | default = "tf-autotest" 16 | type = string 17 | } 18 | 19 | resource "cloudconnexa_host" "host" { 20 | name = "TEST_HOST_NAME" 21 | description = "Terraform test description 2" 22 | internet_access = "LOCAL" 23 | 24 | connector { 25 | name = "test" 26 | vpn_region_id = "us-west-1" 27 | } 28 | 29 | provider = openvpn-cloud 30 | } 31 | 32 | locals { 33 | connector_profile = [for connector in cloudconnexa_host.host.connector : connector.profile][0] 34 | } 35 | 36 | 37 | output "host_id" { 38 | value = cloudconnexa_host.host.id 39 | } 40 | 41 | output "connector_id" { 42 | value = [for connector in cloudconnexa_host.host.connector : connector.id][0] 43 | } 44 | -------------------------------------------------------------------------------- /e2e/setup/user_data.sh.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /opt/openvpn 4 | cat > /opt/openvpn/profile.ovpn < >(tee /var/log/user-data.log|logger -t user-data -s 2>/dev/console) 2>&1 10 | 11 | apt install -y apt-transport-https 12 | wget https://swupdate.openvpn.net/repos/openvpn-repo-pkg-key.pub 13 | apt-key add openvpn-repo-pkg-key.pub 14 | wget -O /etc/apt/sources.list.d/openvpn3.list https://swupdate.openvpn.net/community/openvpn3/repos/openvpn3-focal.list 15 | apt update 16 | apt install -y openvpn3 17 | 18 | # does not work on reboot 19 | openvpn3 session-start --config /opt/openvpn/profile.ovpn 20 | # add auto-reload -------------------------------------------------------------------------------- /example/backend.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | backend "local" {} 3 | required_providers { 4 | openvpncloud = { 5 | source = "OpenVPN/cloudconnexa" 6 | version = "0.0.12" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/connectors.tf: -------------------------------------------------------------------------------- 1 | data "cloudconnexa_network" "test-net" { 2 | name = "test-net" 3 | } 4 | 5 | resource "cloudconnexa_connector" "test-connector" { 6 | name = "test-connector" 7 | vpn_region_id = "eu-central-1" 8 | network_item_type = "NETWORK" 9 | network_item_id = data.cloudconnexa_network.test-net.network_id 10 | } 11 | -------------------------------------------------------------------------------- /example/hosts.tf: -------------------------------------------------------------------------------- 1 | resource "cloudconnexa_host" "test-host" { 2 | name = "test-host" 3 | connector { 4 | name = "test-connector" 5 | vpn_region_id = "eu-central-1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/networks.tf: -------------------------------------------------------------------------------- 1 | resource "cloudconnexa_network" "test-network" { 2 | name = "test-network" 3 | egress = false 4 | default_route { 5 | value = "192.168.0.0/24" 6 | } 7 | default_connector { 8 | name = "test-connector" 9 | vpn_region_id = "eu-central-1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/provider.tf: -------------------------------------------------------------------------------- 1 | provider "openvpncloud" { 2 | base_url = "https://${var.company_name}.api.openvpn.com" 3 | } 4 | 5 | ## Environment Variables example: 6 | # export CLOUDCONNEXA_CLIENT_ID="" 7 | # export CLOUDCONNEXA_CLIENT_SECRET="" 8 | -------------------------------------------------------------------------------- /example/routes.tf: -------------------------------------------------------------------------------- 1 | resource "cloudconnexa_route" "this" { 2 | for_each = { 3 | for key, route in var.routes : route.value => route 4 | } 5 | network_item_id = var.networks["example-network"] 6 | type = "IP_V4" 7 | value = each.value.value 8 | description = each.value.description 9 | } 10 | -------------------------------------------------------------------------------- /example/services.tf: -------------------------------------------------------------------------------- 1 | data "cloudconnexa_network" "test-net" { 2 | name = "test-net" 3 | } 4 | 5 | resource "cloudconnexa_service" "test-service" { 6 | name = "test-service" 7 | type = "IP_SOURCE" 8 | description = "test-description" 9 | routes = ["10.0.0.2/32"] 10 | network_item_type = "NETWORK" 11 | network_item_id = data.cloudconnexa_network.test-net.network_id 12 | 13 | config { 14 | service_types = ["ANY"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/user_groups.tf: -------------------------------------------------------------------------------- 1 | resource "cloudconnexa_user_group" "this" { 2 | name = "test-group" 3 | vpn_region_ids = ["eu-central-1"] 4 | connect_auth = "AUTH" 5 | } 6 | -------------------------------------------------------------------------------- /example/users.tf: -------------------------------------------------------------------------------- 1 | resource "cloudconnexa_user" "this" { 2 | for_each = var.users 3 | username = each.value.username 4 | email = each.value.email 5 | first_name = split("_", each.key)[0] 6 | last_name = split("_", each.key)[1] 7 | group_id = lookup(var.groups, each.value.group) 8 | role = each.value.role 9 | } 10 | -------------------------------------------------------------------------------- /example/variables.tf: -------------------------------------------------------------------------------- 1 | variable "company_name" { 2 | type = string 3 | description = "Company name in CloudConnexa" 4 | # default = "" 5 | } 6 | 7 | variable "users" { 8 | type = map( 9 | object({ 10 | username = string 11 | email = string 12 | group = string 13 | role = string 14 | }) 15 | ) 16 | default = { 17 | "Username1" = { 18 | username = "Username1" 19 | email = "username1@company.com" 20 | group = "Default" 21 | role = "ADMIN" 22 | } 23 | "Username2" = { 24 | username = "Username2" 25 | email = "username2@company.com" 26 | group = "Developer" 27 | role = "MEMBER" 28 | } 29 | "Username3" = { 30 | username = "Username3" 31 | email = "username3@company.com" 32 | group = "Support" 33 | role = "MEMBER" 34 | } 35 | } 36 | } 37 | 38 | variable "groups" { 39 | type = map(string) 40 | default = { 41 | "Default" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 42 | "Developer" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 43 | "Support" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 44 | } 45 | } 46 | 47 | variable "networks" { 48 | type = map(string) 49 | default = { 50 | "example-network" = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 51 | } 52 | } 53 | 54 | variable "routes" { 55 | type = list(map(string)) 56 | default = [ 57 | { 58 | value = "10.0.0.0/18" 59 | description = "Example Route with subnet /18" 60 | }, 61 | { 62 | value = "10.10.0.0/20" 63 | description = "Example Route with subnet /20" 64 | }, 65 | { 66 | value = "10.20.0.0/24" 67 | description = "Example Route with subnet /24" 68 | }, 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OpenVPN/terraform-provider-openvpn-cloud 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.3 6 | 7 | require ( 8 | github.com/gruntwork-io/terratest v0.46.1 9 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 10 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 11 | github.com/openvpn/cloudconnexa-go-client/v2 v2.0.2 12 | github.com/stretchr/testify v1.9.0 13 | ) 14 | 15 | require ( 16 | cloud.google.com/go v0.112.0 // indirect 17 | cloud.google.com/go/compute v1.24.0 // indirect 18 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 19 | cloud.google.com/go/iam v1.1.6 // indirect 20 | cloud.google.com/go/storage v1.36.0 // indirect 21 | github.com/ProtonMail/go-crypto v1.1.0-alpha.0 // indirect 22 | github.com/agext/levenshtein v1.2.3 // indirect 23 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 24 | github.com/aws/aws-sdk-go v1.44.122 // indirect 25 | github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect 26 | github.com/cloudflare/circl v1.3.7 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/fatih/color v1.16.0 // indirect 29 | github.com/go-logr/logr v1.4.1 // indirect 30 | github.com/go-logr/stdr v1.2.2 // indirect 31 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 32 | github.com/golang/protobuf v1.5.4 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/s2a-go v0.1.7 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 37 | github.com/googleapis/gax-go/v2 v2.12.0 // indirect 38 | github.com/hashicorp/errwrap v1.1.0 // indirect 39 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 40 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 41 | github.com/hashicorp/go-getter v1.7.4 // indirect 42 | github.com/hashicorp/go-hclog v1.6.3 // indirect 43 | github.com/hashicorp/go-multierror v1.1.1 // indirect 44 | github.com/hashicorp/go-plugin v1.6.0 // indirect 45 | github.com/hashicorp/go-safetemp v1.0.0 // indirect 46 | github.com/hashicorp/go-uuid v1.0.3 // indirect 47 | github.com/hashicorp/go-version v1.6.0 // indirect 48 | github.com/hashicorp/hc-install v0.6.3 // indirect 49 | github.com/hashicorp/hcl/v2 v2.20.1 // indirect 50 | github.com/hashicorp/logutils v1.0.0 // indirect 51 | github.com/hashicorp/terraform-exec v0.20.0 // indirect 52 | github.com/hashicorp/terraform-json v0.21.0 // indirect 53 | github.com/hashicorp/terraform-plugin-go v0.22.2 // indirect 54 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 55 | github.com/hashicorp/terraform-registry-address v0.2.3 // indirect 56 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 57 | github.com/hashicorp/yamux v0.1.1 // indirect 58 | github.com/jinzhu/copier v0.0.0-20190924061706-b57f9002281a // indirect 59 | github.com/jmespath/go-jmespath v0.4.0 // indirect 60 | github.com/klauspost/compress v1.15.11 // indirect 61 | github.com/mattn/go-colorable v0.1.13 // indirect 62 | github.com/mattn/go-isatty v0.0.20 // indirect 63 | github.com/mattn/go-zglob v0.0.2-0.20190814121620-e3c945676326 // indirect 64 | github.com/mitchellh/copystructure v1.2.0 // indirect 65 | github.com/mitchellh/go-homedir v1.1.0 // indirect 66 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 67 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 68 | github.com/mitchellh/mapstructure v1.5.0 // indirect 69 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 70 | github.com/oklog/run v1.1.0 // indirect 71 | github.com/pmezard/go-difflib v1.0.0 // indirect 72 | github.com/tmccombs/hcl2json v0.3.3 // indirect 73 | github.com/ulikunitz/xz v0.5.10 // indirect 74 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 75 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 76 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 77 | github.com/zclconf/go-cty v1.14.4 // indirect 78 | go.opencensus.io v0.24.0 // indirect 79 | go.opentelemetry.io/otel v1.22.0 // indirect 80 | go.opentelemetry.io/otel/metric v1.22.0 // indirect 81 | go.opentelemetry.io/otel/trace v1.22.0 // indirect 82 | golang.org/x/crypto v0.22.0 // indirect 83 | golang.org/x/mod v0.17.0 // indirect 84 | golang.org/x/net v0.24.0 // indirect 85 | golang.org/x/oauth2 v0.17.0 // indirect 86 | golang.org/x/sync v0.7.0 // indirect 87 | golang.org/x/sys v0.19.0 // indirect 88 | golang.org/x/text v0.14.0 // indirect 89 | golang.org/x/time v0.5.0 // indirect 90 | golang.org/x/tools v0.20.0 // indirect 91 | google.golang.org/api v0.162.0 // indirect 92 | google.golang.org/appengine v1.6.8 // indirect 93 | google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect 94 | google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect 95 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect 96 | google.golang.org/grpc v1.63.2 // indirect 97 | google.golang.org/protobuf v1.34.0 // indirect 98 | gopkg.in/yaml.v3 v3.0.1 // indirect 99 | ) 100 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/OpenVPN/terraform-provider-openvpn-cloud/cloudconnexa" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 7 | ) 8 | 9 | func main() { 10 | plugin.Serve(&plugin.ServeOpts{ 11 | ProviderFunc: func() *schema.Provider { 12 | return cloudconnexa.Provider() 13 | }, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /templates/data-sources/host.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_host Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use an cloudconnexa_host data source to read an existing Cloud Connexa connector. 7 | --- 8 | 9 | # cloudconnexa_host (Data Source) 10 | 11 | Use an `cloudconnexa_host` data source to read an existing Cloud Connexa connector. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The name of the host. 21 | 22 | ### Read-Only 23 | 24 | - `connectors` (List of Object) The list of connectors to be associated with this host. (see [below for nested schema](#nestedatt--connectors)) 25 | - `id` (String) The ID of this resource. 26 | - `internet_access` (String) The type of internet access provided. 27 | - `system_subnets` (List of String) The IPV4 and IPV6 subnets automatically assigned to this host. 28 | 29 | 30 | ### Nested Schema for `connectors` 31 | 32 | Read-Only: 33 | 34 | - `id` (String) The connector id. 35 | - `ip_v4_address` (String) The IPV4 address of the connector. 36 | - `ip_v6_address` (String) The IPV6 address of the connector. 37 | - `name` (String) The connector name. 38 | - `network_item_id` (String) The id of the host with which the connector is associated. 39 | - `network_item_type` (String) The network object type of the connector. This typically will be set to `HOST`. 40 | - `vpn_region_id` (String) The id of the region where the connector is deployed. 41 | 42 | 43 | -------------------------------------------------------------------------------- /templates/data-sources/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_network Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use a cloudconnexa_network data source to read an Cloud Connexa network. 7 | --- 8 | 9 | # cloudconnexa_network (Data Source) 10 | 11 | Use a `cloudconnexa_network` data source to read an Cloud Connexa network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `name` (String) The network name. 21 | 22 | ### Read-Only 23 | 24 | - `connectors` (List of Object) The list of connectors associated with this network. (see [below for nested schema](#nestedatt--connectors)) 25 | - `egress` (Boolean) Boolean to indicate whether this network provides an egress or not. 26 | - `id` (String) The ID of this resource. 27 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 28 | - `network_id` (String) The network ID. 29 | - `routes` (List of Object) The routes associated with this network. (see [below for nested schema](#nestedatt--routes)) 30 | - `system_subnets` (List of String) The IPV4 and IPV6 subnets automatically assigned to this network. 31 | 32 | 33 | ### Nested Schema for `connectors` 34 | 35 | Read-Only: 36 | 37 | - `id` (String) The connector id. 38 | - `ip_v4_address` (String) The IPV4 address of the connector. 39 | - `ip_v6_address` (String) The IPV6 address of the connector. 40 | - `name` (String) The connector name. 41 | - `network_item_id` (String) The id of the network with which the connector is associated. 42 | - `network_item_type` (String) The network object type of the connector. This typically will be set to `NETWORK`. 43 | - `vpn_region_id` (String) The id of the region where the connector is deployed. 44 | 45 | 46 | 47 | ### Nested Schema for `routes` 48 | 49 | Read-Only: 50 | 51 | - `id` (String) The route id. 52 | - `subnet` (String) The value of the route, either an IPV4 address, an IPV6 address, or a DNS hostname. 53 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 54 | 55 | 56 | -------------------------------------------------------------------------------- /templates/data-sources/network_routes.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_network_routes Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use an cloudconnexa_network_routes data source to read all the routes associated with an Cloud Connexa network. 7 | --- 8 | 9 | # cloudconnexa_network_routes (Data Source) 10 | 11 | Use an `cloudconnexa_network_routes` data source to read all the routes associated with an Cloud Connexa network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `network_item_id` (String) The id of the Cloud Connexa network of the routes to be discovered. 21 | 22 | ### Read-Only 23 | 24 | - `id` (String) The ID of this resource. 25 | - `routes` (List of Object) The list of routes. (see [below for nested schema](#nestedatt--routes)) 26 | 27 | 28 | ### Nested Schema for `routes` 29 | 30 | Read-Only: 31 | 32 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 33 | - `value` (String) The value of the route, either an IPV4 address, an IPV6 address, or a DNS hostname. 34 | 35 | 36 | -------------------------------------------------------------------------------- /templates/data-sources/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_user Data Source - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use a cloudconnexa_user data source to read a specific Cloud Connexa user. 7 | --- 8 | 9 | # cloudconnexa_user (Data Source) 10 | 11 | Use a `cloudconnexa_user` data source to read a specific Cloud Connexa user. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `role` (String) The type of user role. Valid values are `ADMIN`, `MEMBER`, or `OWNER`. 21 | - `username` (String) The username of the user. 22 | 23 | ### Read-Only 24 | 25 | - `auth_type` (String) The authentication type of the user. 26 | - `devices` (List of Object) The list of user devices. (see [below for nested schema](#nestedatt--devices)) 27 | - `email` (String) The email address of the user. 28 | - `first_name` (String) The user's first name. 29 | - `group_id` (String) The user's group id. 30 | - `id` (String) The ID of this resource. 31 | - `last_name` (String) The user's last name. 32 | - `status` (String) The user's status. 33 | - `user_id` (String) The ID of this resource. 34 | 35 | 36 | ### Nested Schema for `devices` 37 | 38 | Read-Only: 39 | 40 | - `description` (String) The device's description. 41 | - `id` (String) The device's id. 42 | - `ip_v4_address` (String) The device's IPV4 address. 43 | - `ip_v6_address` (String) The device's IPV6 address. 44 | - `name` (String) The device's name. 45 | 46 | 47 | -------------------------------------------------------------------------------- /templates/index.md.tmpl: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "{{ .ProviderShortName }} Provider" 4 | subcategory: "" 5 | description: |- 6 | 7 | --- 8 | 9 | # {{ .ProviderShortName }} Provider 10 | 11 | !> **WARNING:** This provider is experimental and support for it is on a best-effort basis. Additionally, the underlying API for Cloud Connexa is on Beta, which means that future versions of the provider may introduce breaking changes. Should that happen, migration documentation and support will also be provided on a best-effort basis. 12 | 13 | Use this provider to interact with the [Cloud Connexa API](https://openvpn.net/cloud-docs/api-guide/). 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - **base_url** (String) The base url of your Cloud Connexa accout. 21 | 22 | ### Optional 23 | 24 | - **client_id** (String, Sensitive) If not provided, it will default to the value of the `CLOUDCONNEXA_CLIENT_ID` environment variable. 25 | - **client_secret** (String, Sensitive) If not provided, it will default to the value of the `CLOUDCONNEXA_CLIENT_SECRET` environment variable. 26 | -------------------------------------------------------------------------------- /templates/resources/connector.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_connector Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_connector to create an Cloud Connexa connector. 7 | ~> NOTE: This only creates the Cloud Connexa connector object. Additional manual steps are required to associate a host in your infrastructure with the connector. Go to https://openvpn.net/cloud-docs/connector/ for more information. 8 | --- 9 | 10 | # cloudconnexa_connector (Resource) 11 | 12 | Use `cloudconnexa_connector` to create an Cloud Connexa connector. 13 | 14 | ~> NOTE: This only creates the Cloud Connexa connector object. Additional manual steps are required to associate a host in your infrastructure with the connector. Go to https://openvpn.net/cloud-docs/connector/ for more information. 15 | 16 | 17 | 18 | 19 | ## Schema 20 | 21 | ### Required 22 | 23 | - `name` (String) The connector display name. 24 | - `network_item_id` (String) The id of the network with which this connector is associated. 25 | - `network_item_type` (String) The type of network item of the connector. Supported values are `HOST` and `NETWORK`. 26 | - `vpn_region_id` (String) The id of the region where the connector will be deployed. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of this resource. 31 | - `ip_v4_address` (String) The IPV4 address of the connector. 32 | - `ip_v6_address` (String) The IPV6 address of the connector. 33 | 34 | ## Import 35 | 36 | A connector can be imported using the connector ID, which can be fetched directly from the API. 37 | 38 | ``` 39 | terraform import cloudconnexa_connector.connector 40 | ``` 41 | 42 | ~> NOTE: If the Terraform resource settings are different from the imported connector, the next time you run `terraform apply` the provider will attempt to delete and recreate the connector, which will require you to re-configure the instance manually. -------------------------------------------------------------------------------- /templates/resources/dns_record.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_dns_record Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_dns_record to create a DNS record on your VPN. 7 | --- 8 | 9 | # cloudconnexa_dns_record (Resource) 10 | 11 | Use `cloudconnexa_dns_record` to create a DNS record on your VPN. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `domain` (String) The DNS record name. 21 | 22 | ### Optional 23 | 24 | - `ip_v4_addresses` (List of String) The list of IPV4 addresses to which this record will resolve. 25 | - `ip_v6_addresses` (List of String) The list of IPV6 addresses to which this record will resolve. 26 | 27 | ### Read-Only 28 | 29 | - `id` (String) The ID of this resource. 30 | 31 | ## Import 32 | 33 | A connector can be imported using the DNS record ID, which can be fetched directly from the API. 34 | 35 | ``` 36 | terraform import cloudconnexa_dns_record.record 37 | ``` -------------------------------------------------------------------------------- /templates/resources/host.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_host Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_host to create an Cloud Connexa host. 7 | --- 8 | 9 | # cloudconnexa_host (Resource) 10 | 11 | Use `cloudconnexa_host` to create an Cloud Connexa host. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `connector` (Block Set, Min: 1) The set of connectors to be associated with this host. Can be defined more than once. (see [below for nested schema](#nestedblock--connector)) 21 | - `name` (String) The display name of the host. 22 | 23 | ### Optional 24 | 25 | - `description` (String) The description for the UI. Defaults to `Managed by Terraform`. 26 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 27 | 28 | ### Read-Only 29 | 30 | - `id` (String) The ID of this resource. 31 | - `system_subnets` (Set of String) The IPV4 and IPV6 subnets automatically assigned to this host. 32 | 33 | 34 | ### Nested Schema for `connector` 35 | 36 | Required: 37 | 38 | - `name` (String) Name of the connector associated with this host. 39 | - `vpn_region_id` (String) The id of the region where the connector will be deployed. 40 | 41 | Read-Only: 42 | 43 | - `id` (String) The ID of this resource. 44 | - `ip_v4_address` (String) The IPV4 address of the connector. 45 | - `ip_v6_address` (String) The IPV6 address of the connector. 46 | - `network_item_id` (String) The host id. 47 | - `network_item_type` (String) The network object type. This typically will be set to `HOST`. 48 | 49 | ## Import 50 | 51 | A host can be imported using the DNS record ID, which can be fetched directly from the API. 52 | 53 | ``` 54 | terraform import cloudconnexa_host.host 55 | ``` -------------------------------------------------------------------------------- /templates/resources/network.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_network Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_network to create an Cloud Connexa Network. 7 | --- 8 | 9 | # cloudconnexa_network (Resource) 10 | 11 | Use `cloudconnexa_network` to create an Cloud Connexa Network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `default_connector` (Block List, Min: 1, Max: 1) The default connector of this network. (see [below for nested schema](#nestedblock--default_connector)) 21 | - `default_route` (Block List, Min: 1, Max: 1) The default route of this network. (see [below for nested schema](#nestedblock--default_route)) 22 | - `name` (String) The display name of the network. 23 | 24 | ### Optional 25 | 26 | - `description` (String) The display description for this resource. Defaults to `Managed by Terraform`. 27 | - `egress` (Boolean) Boolean to control whether this network provides an egress or not. 28 | - `internet_access` (String) The type of internet access provided. Valid values are `BLOCKED`, `GLOBAL_INTERNET`, or `LOCAL`. Defaults to `LOCAL`. 29 | 30 | ### Read-Only 31 | 32 | - `id` (String) The ID of this resource. 33 | - `system_subnets` (Set of String) The IPV4 and IPV6 subnets automatically assigned to this network. 34 | 35 | 36 | ### Nested Schema for `default_connector` 37 | 38 | Required: 39 | 40 | - `name` (String) Name of the connector automatically created and attached to this network. 41 | - `vpn_region_id` (String) The id of the region where the default connector will be deployed. 42 | 43 | Read-Only: 44 | 45 | - `id` (String) The ID of this resource. 46 | - `ip_v4_address` (String) The IPV4 address of the default connector. 47 | - `ip_v6_address` (String) The IPV6 address of the default connector. 48 | - `network_item_id` (String) The parent network id. 49 | - `network_item_type` (String) The network object type. This typically will be set to `NETWORK`. 50 | 51 | 52 | 53 | ### Nested Schema for `default_route` 54 | 55 | Required: 56 | 57 | - `value` (String) The target value of the default route. 58 | 59 | Optional: 60 | 61 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 62 | 63 | Read-Only: 64 | 65 | - `id` (String) The ID of this resource. 66 | 67 | A network can be imported using the network ID, which can be fetched directly from the API. 68 | 69 | ``` 70 | terraform import cloudconnexa_network.network 71 | ``` 72 | 73 | ~> NOTE: This will only import the network itslef, but it'll create a new connector and a new route as its defaults. There is currently no way to import an existing connector and route along with a network. The existing connector(s)/route(s) will continue to work, but you'll need to set a `default_connector` and a `default_route` that don't collide with your existing resources. -------------------------------------------------------------------------------- /templates/resources/route.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_route Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_route to create a route on an Cloud Connexa network. 7 | --- 8 | 9 | # cloudconnexa_route (Resource) 10 | 11 | Use `cloudconnexa_route` to create a route on an Cloud Connexa network. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `network_item_id` (String) The id of the network on which to create the route. 21 | - `type` (String) The type of route. Valid values are `IP_V4`, `IP_V6`, and `DOMAIN`. 22 | - `value` (String) The target value of the default route. 23 | 24 | ### Read-Only 25 | 26 | - `id` (String) The ID of this resource. 27 | 28 | ## Import 29 | 30 | A route can be imported using the route ID, which can be fetched directly from the API. 31 | 32 | ``` 33 | terraform import cloudconnexa_route.route 34 | ``` -------------------------------------------------------------------------------- /templates/resources/user.md: -------------------------------------------------------------------------------- 1 | --- 2 | # generated by https://github.com/hashicorp/terraform-plugin-docs 3 | page_title: "cloudconnexa_user Resource - terraform-provider-cloudconnexa" 4 | subcategory: "" 5 | description: |- 6 | Use cloudconnexa_user to create an Cloud Connexa user. 7 | --- 8 | 9 | # cloudconnexa_user (Resource) 10 | 11 | Use `cloudconnexa_user` to create an Cloud Connexa user. 12 | 13 | 14 | 15 | 16 | ## Schema 17 | 18 | ### Required 19 | 20 | - `email` (String) An invitation to Cloud Connexa account will be sent to this email. It will include an initial password and a VPN setup guide. 21 | - `first_name` (String) User's first name. 22 | - `last_name` (String) User's last name. 23 | - `username` (String) A username for the user. 24 | 25 | ### Optional 26 | 27 | - `devices` (Block List, Max: 1) When a user signs in, the device that they use will be added to their account. You can read more at [Cloud Connexa Device](https://openvpn.net/cloud-docs/device/). (see [below for nested schema](#nestedblock--devices)) 28 | - `group_id` (String) The UUID of a user's group. 29 | 30 | ### Read-Only 31 | 32 | - `id` (String) The ID of this resource. 33 | 34 | 35 | ### Nested Schema for `devices` 36 | 37 | Required: 38 | 39 | - `description` (String) A device description. 40 | - `name` (String) A device name. 41 | 42 | Optional: 43 | 44 | - `ipv4_address` (String) An IPv4 address of the device. 45 | - `ipv6_address` (String) An IPv6 address of the device. 46 | 47 | ## Import 48 | 49 | A user can be imported using the user ID using the format below. 50 | 51 | ``` 52 | terraform import cloudconnexa_user.user username@accountname 53 | ``` 54 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["5.0"] 5 | } 6 | } 7 | --------------------------------------------------------------------------------