├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------