diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..141a3f2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Vitesco-Technologies/kubernetes diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8e70c7c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +--- +version: 2 + +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily diff --git a/.github/workflows/golang.yml b/.github/workflows/golang.yml new file mode 100644 index 0000000..4ac1547 --- /dev/null +++ b/.github/workflows/golang.yml @@ -0,0 +1,25 @@ +name: golang + +on: + push: + branches: + - main + pull_request: + +jobs: + golang: + runs-on: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + - name: go test + run: go test -v ./... + + - name: go build + run: go build . diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..542f3cd --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,22 @@ +name: golangci-lint + +on: + push: + branches: + - main + pull_request: + +jobs: + golangci-lint: + runs-on: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 diff --git a/.github/workflows/release.yml.disabled b/.github/workflows/release.yml.disabled new file mode 100644 index 0000000..8992eee --- /dev/null +++ b/.github/workflows/release.yml.disabled @@ -0,0 +1,39 @@ +name: golang + +# TODO: define new release method. Terraform Registry? +on: + push: + tags: + - v* + +jobs: + golang: + runs-on: [self-hosted,cloudplatforms] + # TODO: github.com + # runs-on: [ubuntu-latest] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version-file: go.mod + + # - name: Import GPG key + # uses: crazy-max/ghaction-import-gpg@v6 + # id: import_gpg + # with: + # gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + # passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Run goreleaser + uses: goreleaser/goreleaser-action@v5 + with: + version: latest + args: release --rm-dist + env: + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..caa7d73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,59 @@ +terraform-provider-qip +terraform-provider-qip.exe +.env* + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +__debug* + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +### GoReleaser ### +/dist/ + +### Terraform ### +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# End of https://www.toptal.com/developers/gitignore/api/go,terraform + +# Ignore CLI configuration files +.terraformrc +terraform.rc +terraform +.terraform* diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..430412f --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,99 @@ +--- +# https://golangci-lint.run/usage/configuration/ +run: + timeout: 5m + +linters: + disable: + - noctx # Needs to be added later + - contextcheck # Needs to be added later + - exhaustivestruct + - exhaustruct + - varnamelen + - gochecknoinits + - gochecknoglobals + + presets: + - bugs + - comment + # - complexity + - error + - format + - import + - metalinter + - module + - performance + - style + - test + - unused + +linters-settings: + gci: + sections: + - standard + - default + - prefix(github.com/Vitesco-Technologies/terraform-provider-qip) + paralleltest: + ignore-missing: true + testpackage: + allow-packages: + - provider + - main + lll: + line-length: 140 + depguard: + rules: + main: + files: + - "$all" + - "!$test" + - "!**/pkg/utils/**.go" + - "!**/pkg/qip/test/*.go" + allow: + - "$gostd" + - github.com/Vitesco-Technologies/terraform-provider-qip + - github.com/hashicorp/terraform-plugin-sdk/v2 + - github.com/hashicorp/terraform-plugin-log/tflog + - github.com/hashicorp/go-cty/cty + deny: + - pkg: reflect + desc: Please don't use reflect package + utils: + files: + - "**/pkg/utils/**.go" + allow: + - "$gostd" + - github.com/iancoleman/orderedmap + deny: + - pkg: reflect + desc: Please don't use reflect package + test: + files: + - "$test" + - "**/pkg/qip/test/*.go" + allow: + - $gostd + - github.com/Vitesco-Technologies/terraform-provider-qip + - github.com/hashicorp/terraform-plugin-sdk/v2 + - github.com/stretchr/testify + - github.com/jarcoal/httpmock + deny: + - pkg: reflect + desc: Please don't use reflect package + goheader: + template: |- + Copyright {{ YEAR }} Vitesco Technologies Group AG + + SPDX-License-Identifier: Apache-2.0 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..c12913e --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,46 @@ +# Visit https://goreleaser.com for documentation on how to customize this +# behavior. +builds: +- env: + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + goos: + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm64 + ignore: + - goos: darwin + goarch: '386' + binary: '{{ .ProjectName }}_v{{ .Version }}' +archives: +- format: zip + name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' +checksum: + extra_files: + - glob: 'terraform-registry-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' + algorithm: sha256 +# signs: +# - artifacts: checksum +# args: +# - "--batch" +# - "--local-user" +# - "{{ .Env.GPG_FINGERPRINT }}" +# - "--output" +# - "${signature}" +# - "--detach-sign" +# - "${artifact}" +# release: +# extra_files: +# - glob: 'signkey.asc' +# - glob: 'terraform-registry-manifest.json' +# name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..18b1862 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + exclude: ^docs/ + - id: end-of-file-fixer + exclude: ^docs/ + - id: check-yaml + - id: check-added-large-files + +- repo: https://github.com/golangci/golangci-lint + rev: v1.54.2 + hooks: + - id: golangci-lint + +- repo: local + hooks: + - id: go-generate + name: go generate + language: system + entry: go generate ./... + pass_filenames: false diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..201650f --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +HOSTNAME=registry.terraform.io +NAMESPACE=vitesco-technologies +NAME=qip +BINARY=terraform-provider-${NAME} +VERSION=0.1.99 +OS_ARCH=$(shell go env GOOS)_$(shell go env GOARCH) + +default: install + +build: + go build -o ${BINARY} + +docs: + go generate + +install: build + mkdir -p ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} + cp ${BINARY} ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME}/${VERSION}/${OS_ARCH} + +uninstall: + rm -rf ~/.terraform.d/plugins/${HOSTNAME}/${NAMESPACE}/${NAME} + +test: + go test -v ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..ced447c --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# Terraform Provider for Nokia QIP + +The provider for Nokia QIP will allow you to retrieve metadata from QIP or manage IPv4 addresses including their DNS names. + +Features: + +- Data sources for `qip_v4address` and `qip_v4subnet` +- Manage addresses with `qip_v4address` + + + +Build based on the Swagger API documentation that should be available with your QIP instance: `https://qip.example.com.com/rest-api/` + +## How to use + +Please see the [documentation](docs/) for some examples. + +Very basic usage: + +```terraform +resource "qip_v4address" "address" { + address = "192.0.2.23" + subnet = "192.0.2.0" + + name = "my-example" + // object_class = "Virtual Server" + // description = "Example System" + // domain_name = "corp.example.com" +} + +resource "qip_v4address" "address" { + // selecting a free address in the subnet + subnet = "192.0.2.0" + + subnet_range_start = "192.0.2.30" + subnet_range_end = "192.0.2.50" + + name = "my-example" +} + +data "qip_v4subnet" "subnet" { + address = "192.0.2.0" +} + +provider "qip" { + server = "https://qip.example.com" + org = "Example" + username = "admin" + password = "admin" +} + +terraform { + required_providers { + qip = { + source = "registry.terraform.io/vitesco-technologies/qip" + version = ">0" + } + } +} +``` + +## Installation for testing purposes + +First, build and install the provider locally. + +```shell +make install +``` + +Then, run the following command to initialize the workspace and apply the sample configuration. + +```shell +terraform init && terraform apply +``` diff --git a/docs/data-sources/v4address.md b/docs/data-sources/v4address.md new file mode 100644 index 0000000..0e6676e --- /dev/null +++ b/docs/data-sources/v4address.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "qip_v4address Data Source - terraform-provider-qip" +subcategory: "" +description: |- + IPv4 address object in QIP. +--- + +# qip_v4address (Data Source) + +IPv4 address object in QIP. + +## Example Usage + +```terraform +data "qip_v4address" "test1" { + address = "192.0.2.23" +} +``` + + +## Schema + +### Required + +- `address` (String) IPv4 address. + +### Read-Only + +- `description` (String) Description for the address. +- `domain_name` (String) DNS Zone of the address. +- `id` (String) The ID of this resource. +- `name` (String) Hostname for the address. +- `object_class` (String) Object class for the address. Must be known by the QIP server. +- `subnet` (String) Subnet of the IPv4 address. diff --git a/docs/data-sources/v4subnet.md b/docs/data-sources/v4subnet.md new file mode 100644 index 0000000..9be2a05 --- /dev/null +++ b/docs/data-sources/v4subnet.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "qip_v4subnet Data Source - terraform-provider-qip" +subcategory: "" +description: |- + IPv4 address object in QIP. +--- + +# qip_v4subnet (Data Source) + +IPv4 address object in QIP. + +## Example Usage + +```terraform +data "qip_v4subnet" "subnet" { + address = "192.0.2.0" +} +``` + + +## Schema + +### Required + +- `address` (String) IPv4 subnet address. + +### Read-Only + +- `address_cidr` (String) IPv4 subnet address in CIDR notation. +- `default_routers` (List of String) List of default routers for this subnet. +- `description` (String) Description of the V4 subnet. +- `dns_servers` (List of String) List of DNS servers preferred for this subnet. +- `domains` (List of String) List of domains for the V4 subnet. +- `id` (String) The ID of this resource. +- `mask` (String) IPv4 subnet network mask. +- `name` (String) Name of the V4 subnet. +- `network` (String) IPv4 network address this subnet belongs to. +- `ntp_servers` (List of String) List of NTP time servers preferred for this subnet. +- `prefix_length` (Number) IPv4 CIDR prefix length for the subnet. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..66a3447 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "qip Provider" +subcategory: "" +description: |- + +--- + +# qip Provider + + + +## Example Usage + +```terraform +provider "qip" { + server = "https://qip.example.com" + org = "Example" + username = "admin" + password = "admin" +} + +terraform { + required_providers { + qip = { + source = "registry.terraform.io/vitesco-technologies/qip" + version = ">0" + } + } +} +``` + + +## Schema + +### Optional + +- `org` (String) Organization name inside QIP (e.g. Example). (env: `QIP_ORG`) +- `password` (String, Sensitive) Password to authenticate against the QIP REST API. (env: `QIP_PASSWORD`) +- `request_timeout` (Number) Timeout of HTTP requests of the provider in seconds. +- `server` (String) Base URL of the QIP Server (e.g. https://qip.example.com). (env: `QIP_SERVER`) +- `username` (String) Username to authenticate against the QIP REST API. (env: `QIP_USERNAME`) diff --git a/docs/resources/v4address.md b/docs/resources/v4address.md new file mode 100644 index 0000000..9d6845c --- /dev/null +++ b/docs/resources/v4address.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "qip_v4address Resource - terraform-provider-qip" +subcategory: "" +description: |- + Managing an IPv4 address object in QIP. +--- + +# qip_v4address (Resource) + +Managing an IPv4 address object in QIP. + +## Example Usage + +```terraform +resource "qip_v4address" "simple" { + address = "192.0.2.23" + subnet = "192.0.2.0" + name = "my-example" +} + +resource "qip_v4address" "full" { + address = "192.0.2.23" + subnet = "192.0.2.0" + + subnet_range_start = "192.0.2.30" + subnet_range_end = "192.0.2.50" + + name = "my-example" + object_class = "Virtual Server" + description = "Example System" + domain_name = "corp.example.com" +} +``` + + +## Schema + +### Required + +- `name` (String) Hostname for the address. +- `subnet` (String) Subnet of the IPv4 address. + +### Optional + +- `address` (String) IPv4 address. +- `description` (String) Description for the address. +- `domain_name` (String) DNS Zone of the address. +- `object_class` (String) Object class for the address. Must be known by the QIP server. +- `subnet_range_end` (String) Ending address of a range to select a free IPv4 address from. Will be passed to QIP. +- `subnet_range_start` (String) Starting address of a range to select a free IPv4 address from. Will be passed to QIP. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import qip_v4address.address 192.0.2.23 +``` diff --git a/docs/resources/v4address_rr.md b/docs/resources/v4address_rr.md new file mode 100644 index 0000000..8328119 --- /dev/null +++ b/docs/resources/v4address_rr.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "qip_v4address_rr Resource - terraform-provider-qip" +subcategory: "" +description: |- + Managing additional RR for IPv4 address objects in QIP. Only supports A records as of now. +--- + +# qip_v4address_rr (Resource) + +Managing additional RR for IPv4 address objects in QIP. Only supports A records as of now. + +## Example Usage + +```terraform +# We use an address to derive the current domain and address from. +resource "qip_v4address" "address" { + subnet = "192.0.2.0" + name = "my-example" +} + +resource "qip_v4address_rr" "wildcard" { + name = "*.my-example" + address = qip_v4address.address.address + domain_name = qip_v4address.address.domain_name +} + +resource "qip_v4address_rr" "other_record" { + name = "other-example" + address = qip_v4address.address.address + domain_name = qip_v4address.address.domain_name +} + +resource "qip_v4address_rr" "manually" { + name = "extra-example" + address = "192.0.2.42" + domain_name = "int.example.com" +} +``` + + +## Schema + +### Required + +- `address` (String) IPv4 address to attach a RR to. +- `domain_name` (String) DNS Zone for the additional RR. +- `name` (String) Hostname for the address. (e.g. `entry-extra` or `*.entry-extra`) + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..9b81a69 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +# Examples + +Some examples on how to use the provider for documentation purposes. diff --git a/examples/data-sources/qip_v4address/data-source.tf b/examples/data-sources/qip_v4address/data-source.tf new file mode 100644 index 0000000..f4639ac --- /dev/null +++ b/examples/data-sources/qip_v4address/data-source.tf @@ -0,0 +1,3 @@ +data "qip_v4address" "test1" { + address = "192.0.2.23" +} diff --git a/examples/data-sources/qip_v4subnet/data-source.tf b/examples/data-sources/qip_v4subnet/data-source.tf new file mode 100644 index 0000000..f194164 --- /dev/null +++ b/examples/data-sources/qip_v4subnet/data-source.tf @@ -0,0 +1,3 @@ +data "qip_v4subnet" "subnet" { + address = "192.0.2.0" +} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf new file mode 100644 index 0000000..0e25d4f --- /dev/null +++ b/examples/provider/provider.tf @@ -0,0 +1,15 @@ +provider "qip" { + server = "https://qip.example.com" + org = "Example" + username = "admin" + password = "admin" +} + +terraform { + required_providers { + qip = { + source = "registry.terraform.io/vitesco-technologies/qip" + version = ">0" + } + } +} diff --git a/examples/resources/qip_v4address/import.sh b/examples/resources/qip_v4address/import.sh new file mode 100644 index 0000000..30981ca --- /dev/null +++ b/examples/resources/qip_v4address/import.sh @@ -0,0 +1 @@ +terraform import qip_v4address.address 192.0.2.23 diff --git a/examples/resources/qip_v4address/resource.tf b/examples/resources/qip_v4address/resource.tf new file mode 100644 index 0000000..e945b8f --- /dev/null +++ b/examples/resources/qip_v4address/resource.tf @@ -0,0 +1,18 @@ +resource "qip_v4address" "simple" { + address = "192.0.2.23" + subnet = "192.0.2.0" + name = "my-example" +} + +resource "qip_v4address" "full" { + address = "192.0.2.23" + subnet = "192.0.2.0" + + subnet_range_start = "192.0.2.30" + subnet_range_end = "192.0.2.50" + + name = "my-example" + object_class = "Virtual Server" + description = "Example System" + domain_name = "corp.example.com" +} diff --git a/examples/resources/qip_v4address_rr/resource.tf b/examples/resources/qip_v4address_rr/resource.tf new file mode 100644 index 0000000..6a7d656 --- /dev/null +++ b/examples/resources/qip_v4address_rr/resource.tf @@ -0,0 +1,23 @@ +# We use an address to derive the current domain and address from. +resource "qip_v4address" "address" { + subnet = "192.0.2.0" + name = "my-example" +} + +resource "qip_v4address_rr" "wildcard" { + name = "*.my-example" + address = qip_v4address.address.address + domain_name = qip_v4address.address.domain_name +} + +resource "qip_v4address_rr" "other_record" { + name = "other-example" + address = qip_v4address.address.address + domain_name = qip_v4address.address.domain_name +} + +resource "qip_v4address_rr" "manually" { + name = "extra-example" + address = "192.0.2.42" + domain_name = "int.example.com" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5575aef --- /dev/null +++ b/go.mod @@ -0,0 +1,81 @@ +module github.com/Vitesco-Technologies/terraform-provider-qip + +go 1.21 + +require ( + github.com/hashicorp/terraform-plugin-docs v0.16.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 + github.com/iancoleman/orderedmap v0.3.0 + github.com/jarcoal/httpmock v1.3.0 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/agext/levenshtein v1.2.2 // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 + github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.5.1 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/hcl/v2 v2.18.0 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.19.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 + github.com/hashicorp/terraform-registry-address v0.2.2 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/run v1.0.0 // indirect + github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect + github.com/zclconf/go-cty v1.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/grpc v1.57.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect +) + +require ( + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.1.1 // indirect + github.com/Masterminds/sprig/v3 v3.2.2 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/armon/go-radix v1.0.0 // indirect + github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/cloudflare/circl v1.3.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/hc-install v0.6.0 // indirect + github.com/hashicorp/terraform-exec v0.19.0 // indirect + github.com/hashicorp/terraform-json v0.17.1 // indirect + github.com/huandu/xstrings v1.3.2 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/mitchellh/cli v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/posener/complete v1.2.3 // indirect + github.com/russross/blackfriday v1.6.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df // indirect + golang.org/x/mod v0.12.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..dec1ca3 --- /dev/null +++ b/go.sum @@ -0,0 +1,267 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= +github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= +github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= +github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= +github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= +github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= +github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= +github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= +github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.5.1 h1:oGm7cWBaYIp3lJpx1RUEfLWophprE2EV/KUeqBYo+6k= +github.com/hashicorp/go-plugin v1.5.1/go.mod h1:w1sAEES3g3PuV/RzUrgow20W2uErMly84hhD3um1WL4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.6.0 h1:fDHnU7JNFNSQebVKYhHZ0va1bC6SrPQ8fpebsvNr2w4= +github.com/hashicorp/hc-install v0.6.0/go.mod h1:10I912u3nntx9Umo1VAeYPUUuehk0aRQJYpMwbX5wQA= +github.com/hashicorp/hcl/v2 v2.18.0 h1:wYnG7Lt31t2zYkcquwgKo6MWXzRUDIeIVU5naZwHLl8= +github.com/hashicorp/hcl/v2 v2.18.0/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81SpgVtZNNtFSM= +github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= +github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA= +github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= +github.com/hashicorp/terraform-plugin-docs v0.16.0 h1:UmxFr3AScl6Wged84jndJIfFccGyBZn52KtMNsS12dI= +github.com/hashicorp/terraform-plugin-docs v0.16.0/go.mod h1:M3ZrlKBJAbPMtNOPwHicGi1c+hZUh7/g0ifT/z7TVfA= +github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= +github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= +github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= +github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 h1:wcOKYwPI9IorAJEBLzgclh3xVolO7ZorYd6U1vnok14= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0/go.mod h1:qH/34G25Ugdj5FcM95cSoXzUgIbgfhVLXCcEcYaMwq8= +github.com/hashicorp/terraform-registry-address v0.2.2 h1:lPQBg403El8PPicg/qONZJDC6YlgCVbWDtNmmZKtBno= +github.com/hashicorp/terraform-registry-address v0.2.2/go.mod h1:LtwNbCihUoUZ3RYriyS2wF/lGPB6gF9ICLRtuDk7hSo= +github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= +github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= +github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc= +github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= +google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/provider/data_source_v4address.go b/internal/provider/data_source_v4address.go new file mode 100644 index 0000000..55d88bd --- /dev/null +++ b/internal/provider/data_source_v4address.go @@ -0,0 +1,76 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/v4address" +) + +func dataSourceV4Address() *schema.Resource { + return &schema.Resource{ + Description: "IPv4 address object in QIP.", + + ReadContext: dataSourceV4AddressRead, + + Schema: schemaV4Address(true), + } +} + +func dataSourceV4AddressRead(_ context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*terraformClient) //nolint:forcetypeassert + + addr, err := v4address.Load(client.QIPClient, d.Get("address").(string)) + if err != nil { + return diag.Errorf("could not find IPv4 object: %s", err) + } + + d.SetId(addr.ObjectAddr) + + err = d.Set("subnet", addr.SubnetAddr) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("name", addr.ObjectName) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("description", addr.ObjectDesc) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("object_class", addr.ObjectClass) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("domain_name", addr.DomainName) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/provider/data_source_v4address_test.go b/internal/provider/data_source_v4address_test.go new file mode 100644 index 0000000..b6f921a --- /dev/null +++ b/internal/provider/data_source_v4address_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceV4Address(t *testing.T) { + address := os.Getenv("QIP_TEST_ACC_DATA_IP") + if address == "" { + t.Skip("must set QIP_TEST_ACC_DATA_IP for this test") + } + + testSrc := ` + data "qip_v4address" "test" { + address = "` + address + `" + } + ` + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testSrc, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("data.qip_v4address.test", "id", stringRe(address)), + resource.TestMatchResourceAttr("data.qip_v4address.test", "subnet", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("data.qip_v4address.test", "name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("data.qip_v4address.test", "domain_name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("data.qip_v4address.test", "object_class", stringNoWhitespaceRe), + ), + }, + }, + }) +} diff --git a/internal/provider/data_source_v4subnet.go b/internal/provider/data_source_v4subnet.go new file mode 100644 index 0000000..9bf7384 --- /dev/null +++ b/internal/provider/data_source_v4subnet.go @@ -0,0 +1,147 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "fmt" + "net" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/v4subnet" +) + +func dataSourceV4Subnet() *schema.Resource { + return &schema.Resource{ + Description: "IPv4 address object in QIP.", + + ReadContext: dataSourceV4SubnetRead, + + Schema: map[string]*schema.Schema{ + "address": { + Description: "IPv4 subnet address.", + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateIPV4Address, + }, + "address_cidr": { + Description: "IPv4 subnet address in CIDR notation.", + Type: schema.TypeString, + Computed: true, + }, + "mask": { + Description: "IPv4 subnet network mask.", + Type: schema.TypeString, + Computed: true, + }, + "prefix_length": { + Description: "IPv4 CIDR prefix length for the subnet.", + Type: schema.TypeInt, + Computed: true, + }, + "network": { + Description: "IPv4 network address this subnet belongs to.", + Type: schema.TypeString, + Computed: true, + }, + "name": { + Description: "Name of the V4 subnet.", + Type: schema.TypeString, + Computed: true, + }, + "description": { + Description: "Description of the V4 subnet.", + Type: schema.TypeString, + Computed: true, + }, + "domains": { + Description: "List of domains for the V4 subnet.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "dns_servers": { + Description: "List of DNS servers preferred for this subnet.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "ntp_servers": { + Description: "List of NTP time servers preferred for this subnet.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "default_routers": { + Description: "List of default routers for this subnet.", + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceV4SubnetRead(_ context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*terraformClient) //nolint:forcetypeassert + + subnet, err := v4subnet.Load(client.QIPClient, d.Get("address").(string)) + if err != nil { + return diag.Errorf("could not find IPv4 object: %s", err) + } + + d.SetId(subnet.SubnetAddress) + + // Calculate prefix length from netmask + mask := net.IPMask(net.ParseIP(subnet.SubnetMask).To4()) + prefixLength, _ := mask.Size() + + values := map[string]any{ + "address": subnet.SubnetAddress, + "address_cidr": fmt.Sprintf("%s/%d", subnet.SubnetAddress, prefixLength), + "mask": subnet.SubnetMask, + "prefix_length": prefixLength, + "network": subnet.NetworkAddress, + "name": subnet.SubnetName, + "description": subnet.SubnetDescription, + "domains": subnet.Domains.Name, + "dns_servers": subnet.PreferredDNSServers.Name, + "ntp_servers": subnet.PreferredTimeServers.Name, + "default_routers": subnet.DefaultRouters.Name, + } + + for k, v := range values { + err = d.Set(k, v) + if err != nil { + return diag.FromErr(err) + } + } + + return nil +} diff --git a/internal/provider/data_source_v4subnet_test.go b/internal/provider/data_source_v4subnet_test.go new file mode 100644 index 0000000..2731473 --- /dev/null +++ b/internal/provider/data_source_v4subnet_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceV4Subnet(t *testing.T) { + address := os.Getenv("QIP_TEST_ACC_SUBNET") + if address == "" { + t.Skip("must set QIP_TEST_ACC_SUBNET for this test") + } + + addrRe := stringRe(address) + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` + data "qip_v4subnet" "test" { + address = "` + address + `" + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "id", addrRe), + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "address", addrRe), + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "address_cidr", ipv4CIDRRe), + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "name", stringNonEmptyRe), + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "mask", ipv4AddressRe), + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "prefix_length", ipv4PrefixLengthRe), + resource.TestMatchResourceAttr("data.qip_v4subnet.test", "network", ipv4AddressRe), + ), + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go new file mode 100644 index 0000000..c52aab5 --- /dev/null +++ b/internal/provider/provider.go @@ -0,0 +1,123 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" +) + +func init() { + schema.DescriptionKind = schema.StringMarkdown +} + +func New(version string) func() *schema.Provider { + return func() *schema.Provider { + p := &schema.Provider{ + Schema: map[string]*schema.Schema{ + "server": { + Type: schema.TypeString, + Optional: true, + Description: "Base URL of the QIP Server (e.g. https://qip.example.com). (env: `QIP_SERVER`)", + DefaultFunc: schema.EnvDefaultFunc("QIP_SERVER", nil), + }, + "org": { + Type: schema.TypeString, + Optional: true, + Description: "Organization name inside QIP (e.g. Example). (env: `QIP_ORG`)", + DefaultFunc: schema.EnvDefaultFunc("QIP_ORG", nil), + }, + "username": { + Type: schema.TypeString, + Optional: true, + Description: "Username to authenticate against the QIP REST API. (env: `QIP_USERNAME`)", + DefaultFunc: schema.EnvDefaultFunc("QIP_USERNAME", nil), + }, + "password": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: "Password to authenticate against the QIP REST API. (env: `QIP_PASSWORD`)", + DefaultFunc: schema.EnvDefaultFunc("QIP_PASSWORD", nil), + }, + "request_timeout": { + Type: schema.TypeInt, + Optional: true, + Description: "Timeout of HTTP requests of the provider in seconds.", + Default: qip.DefaultTimeout.Seconds(), + }, + }, + DataSourcesMap: map[string]*schema.Resource{ + "qip_v4address": dataSourceV4Address(), + "qip_v4subnet": dataSourceV4Subnet(), + }, + ResourcesMap: map[string]*schema.Resource{ + "qip_v4address": resourceV4Address(), + "qip_v4address_rr": resourceV4AddressRR(), + }, + } + + p.ConfigureContextFunc = configure(version, p) + + return p + } +} + +type terraformClient struct { + QIPClient *qip.Client +} + +func configure(_ string, _ *schema.Provider) func(context.Context, *schema.ResourceData) (any, diag.Diagnostics) { + return func(_ context.Context, d *schema.ResourceData) (any, diag.Diagnostics) { + client := &terraformClient{} + + //nolint:forcetypeassert + var ( + err error + server = d.Get("server").(string) + org = d.Get("org").(string) + username = d.Get("username").(string) + password = d.Get("password").(string) + requestTimeout = d.Get("request_timeout").(int) + ) + + if server == "" || org == "" || username == "" || password == "" { + return nil, diag.Errorf("Unable to create QIP client: server, org, username and password must be set") + } + + client.QIPClient, err = qip.NewClient(server, org) + if err != nil { + return nil, diag.Errorf("could not setup QIP Client: %s", err) + } + + client.QIPClient.Client.Timeout = time.Duration(requestTimeout) * time.Second + + err = client.QIPClient.Login(username, password) + if err != nil { + return nil, diag.Errorf("could not authenticate against QIP API: %s", err) + } + + return client, nil + } +} diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go new file mode 100644 index 0000000..2c53af7 --- /dev/null +++ b/internal/provider/provider_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/utils" +) + +var providerFactories = map[string]func() (*schema.Provider, error){ + "qip": func() (*schema.Provider, error) { //nolint:unparam + return New("dev")(), nil + }, +} + +var ( + ipv4AddressRe = regexp.MustCompile(`^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$`) + // ipv4CIDRRe checks for an IPV4 CIDR with prefix length 8-32. + ipv4PrefixLengthRe = regexp.MustCompile(`([89]|[1-2][0-9]|3[0-2])`) + ipv4CIDRRe = regexp.MustCompile(`^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}/` + ipv4PrefixLengthRe.String() + `$`) + stringNoWhitespaceRe = regexp.MustCompile(`^\S+$`) + stringNonEmptyRe = regexp.MustCompile(`^.+$`) +) + +func TestProvider(t *testing.T) { + if err := New("dev")().InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func testAccPreCheck(t *testing.T) { + t.Helper() + + _ = getRequiredEnv(t, "QIP_SERVER") + _ = getRequiredEnv(t, "QIP_USERNAME") + _ = getRequiredEnv(t, "QIP_PASSWORD") +} + +func getRequiredEnv(t *testing.T, name string) string { + t.Helper() + + value := os.Getenv(name) + if value == "" { + t.Skip("env:" + name + " must be set for tests") + } + + return value +} + +func getRandomName(prefix string) string { + return prefix + "-" + utils.ShortID(4) +} + +func stringRe(text string) *regexp.Regexp { + return regexp.MustCompile(`^` + regexp.QuoteMeta(text) + `$`) +} diff --git a/internal/provider/resource_v4address.go b/internal/provider/resource_v4address.go new file mode 100644 index 0000000..6f53b2d --- /dev/null +++ b/internal/provider/resource_v4address.go @@ -0,0 +1,226 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "errors" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/v4address" +) + +var ErrIDRequiredToLoad = errors.New("can not load object with empty id") + +func resourceV4Address() *schema.Resource { + return &schema.Resource{ + Description: "Managing an IPv4 address object in QIP.", + + CreateContext: resourceV4AddressCreate, + ReadContext: resourceV4AddressRead, + UpdateContext: resourceV4AddressUpdate, + DeleteContext: resourceV4AddressDelete, + + Schema: schemaV4Address(false), + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceV4AddressCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*terraformClient) //nolint:forcetypeassert + + //nolint:forcetypeassert + var ( + err error + address = d.Get("address").(string) + subnet = d.Get("subnet").(string) + name = d.Get("name").(string) + description = d.Get("description").(string) + class = d.Get("object_class").(string) + domain = d.Get("domain_name").(string) + rangeStart = d.Get("subnet_range_start").(string) + rangeEnd = d.Get("subnet_range_end").(string) + ) + + if subnet == "" { + return diag.Errorf("subnet must be set") + } + + var addressIsSelected bool + + if address == "" { + var addressRange *v4address.SelectedAddrRange + + if rangeStart != "" && rangeEnd != "" { + addressRange = &v4address.SelectedAddrRange{ + StartAddress: rangeStart, + EndAddress: rangeEnd, + } + } + + selectedAddress, err := v4address.CreateSelected(client.QIPClient, subnet, addressRange) + if err != nil { + return diag.FromErr(err) + } + + tflog.Trace(ctx, "Got V4Address selected: "+selectedAddress) + + address = selectedAddress + addressIsSelected = true + } + + addr := &v4address.V4Address{ + ObjectAddr: address, + SubnetAddr: subnet, + ObjectName: name, + ObjectClass: class, + ObjectDesc: description, + DomainName: domain, + } + + if addressIsSelected { + err = v4address.Update(client.QIPClient, addr) + } else { + err = v4address.Create(client.QIPClient, addr) + } + + if err != nil { + return diag.FromErr(err) + } + + d.SetId(addr.ObjectAddr) + + diags := resourceV4AddressRead(ctx, d, meta) + if diags != nil { + return diags + } + + tflog.Trace(ctx, "Created V4Address "+d.Id()) + + return nil +} + +func resourceV4AddressLoad(_ context.Context, d *schema.ResourceData, meta any) (*v4address.V4Address, error) { + if d.Id() == "" { + return nil, ErrIDRequiredToLoad + } + + addr, err := v4address.Load(meta.(*terraformClient).QIPClient, d.Id()) + if err != nil { + return nil, err //nolint:wrapcheck + } + + return addr, nil +} + +func resourceV4AddressRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + addr, err := resourceV4AddressLoad(ctx, d, meta) + if err != nil { + var notFoundErr *qip.HTTPNotFoundError + + if errors.As(err, ¬FoundErr) { + // Object is not found, so reset Id and return no error + d.SetId("") + + return nil + } + + return diag.FromErr(err) + } + + // Update state from object + err = d.Set("address", addr.ObjectAddr) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("subnet", addr.SubnetAddr) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("name", addr.ObjectName) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("description", addr.ObjectDesc) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("object_class", addr.ObjectClass) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set("domain_name", addr.DomainName) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +//nolint:forcetypeassert +func resourceV4AddressUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + addr, err := resourceV4AddressLoad(ctx, d, meta) + if err != nil { + return diag.FromErr(err) + } + + // Check if address changed + if d.Get("address").(string) != d.Id() { + return diag.Errorf("address can not be changed after object was created") + } + + addr.ObjectName = d.Get("name").(string) + addr.ObjectDesc = d.Get("description").(string) + addr.ObjectClass = d.Get("object_class").(string) + addr.DomainName = d.Get("domain_name").(string) + + err = v4address.Update(meta.(*terraformClient).QIPClient, addr) + if err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceV4AddressDelete(_ context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*terraformClient) //nolint:forcetypeassert + + if d.Id() == "" { + return diag.Errorf("can not delete V4Address with empty id") + } + + err := v4address.Delete(client.QIPClient, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/provider/resource_v4address_rr.go b/internal/provider/resource_v4address_rr.go new file mode 100644 index 0000000..111eb85 --- /dev/null +++ b/internal/provider/resource_v4address_rr.go @@ -0,0 +1,225 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/rr" +) + +var ErrNonUniqueRR = errors.New("non unique RR found") + +func resourceV4AddressRR() *schema.Resource { + return &schema.Resource{ + Description: "Managing additional RR for IPv4 address objects in QIP. Only supports A records as of now.", + + CreateContext: resourceV4AddressRRCreate, + ReadContext: resourceV4AddressRRRead, + UpdateContext: resourceV4AddressRRUpdate, + DeleteContext: resourceV4AddressRRDelete, + + Schema: map[string]*schema.Schema{ + "address": { + Description: "IPv4 address to attach a RR to.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": { + Description: "Hostname for the address. (e.g. `entry-extra` or `*.entry-extra`)", + Type: schema.TypeString, + Required: true, + }, + "domain_name": { + Description: "DNS Zone for the additional RR.", + Type: schema.TypeString, + Required: true, + }, + }, + + // Importer: &schema.ResourceImporter{ + // StateContext: schema.ImportStatePassthroughContext, + // }, + } +} + +// getIDFromRR uses the base64 encoded JSON representation of a record as ID. +func getIDFromRR(record *rr.RR) (string, error) { + data, err := json.Marshal(record) + if err != nil { + return "", fmt.Errorf("could not encode JSON: %w", err) + } + + return base64.StdEncoding.EncodeToString(data), nil +} + +// getRRFromID returns a rr.RR decoded from the ID, which is a base64 encoded JSON representation of a record. +func getRRFromID(id string) (*rr.RR, error) { + data, err := base64.StdEncoding.DecodeString(id) + if err != nil { + return nil, fmt.Errorf("could not decode base64: %w", err) + } + + var record rr.RR + + err = json.Unmarshal(data, &record) + if err != nil { + return nil, fmt.Errorf("could not decode JSON: %w", err) + } + + return &record, nil +} + +func resourceV4AddressRRCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + //nolint:forcetypeassert + var ( + err error + client = meta.(*terraformClient).QIPClient + address = d.Get("address").(string) + name = d.Get("name").(string) + domain = d.Get("domain_name").(string) + ) + + fqdn := name + "." + domain + + record := rr.NewAForObject(fqdn, address) + + err = rr.Create(client, record) + if err != nil { + return diag.FromErr(err) + } + + id, err := getIDFromRR(record) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(id) + + tflog.Trace(ctx, "Created RR for V4Address "+d.Id()) + + return nil +} + +func resourceV4AddressRRLoad(_ context.Context, d *schema.ResourceData, meta any) (*rr.RR, error) { + if d.Id() == "" { + return nil, ErrIDRequiredToLoad + } + + idRecord, err := getRRFromID(d.Id()) + if err != nil { + return nil, err + } + + records, err := rr.LoadAllForObject(meta.(*terraformClient).QIPClient, idRecord.InfraAddr) + if err != nil { + return nil, err //nolint:wrapcheck + } + + var singleRecord *rr.RR + + // Search for a single record + for _, record := range records { + if record.Equal(idRecord) { + if singleRecord == nil { + singleRecord = record + } else { + return nil, ErrNonUniqueRR + } + } + } + + return singleRecord, nil +} + +func resourceV4AddressRRRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + record, err := resourceV4AddressRRLoad(ctx, d, meta) + if err != nil { + return diag.FromErr(err) + } + + if record == nil { + return diag.Errorf("could not find a record for id: %s", d.Id()) + } + + // Currently we have no state to refresh, all attributes are identifying + + return nil +} + +//nolint:forcetypeassert +func resourceV4AddressRRUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + record, err := resourceV4AddressRRLoad(ctx, d, meta) + if err != nil { + return diag.FromErr(err) + } + + if record == nil { + return diag.Errorf("could not find a record for id: %s", d.Id()) + } + + updatedRecord := *record + + // Only allow to change the record owner (FQDN as of now) + updatedRecord.Owner = d.Get("name").(string) + "." + d.Get("domain_name").(string) + + err = rr.Update(meta.(*terraformClient).QIPClient, record, &updatedRecord) + if err != nil { + return diag.FromErr(err) + } + + // update ID after the change - because some values are identifying + id, err := getIDFromRR(&updatedRecord) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(id) + + return nil +} + +//nolint:forcetypeassert +func resourceV4AddressRRDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + record, err := resourceV4AddressRRLoad(ctx, d, meta) + if err != nil { + return diag.FromErr(err) + } + + if d.Id() == "" || record == nil { + // Nothing to delete + return nil + } + + err = rr.Delete(meta.(*terraformClient).QIPClient, record) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/provider/resource_v4address_rr_test.go b/internal/provider/resource_v4address_rr_test.go new file mode 100644 index 0000000..d7534bd --- /dev/null +++ b/internal/provider/resource_v4address_rr_test.go @@ -0,0 +1,75 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceV4AddressRR(t *testing.T) { + subnet := getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_SUBNET") + name := getRandomName("terraform-qip-rr") + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` + resource "qip_v4address" "test" { + subnet = "` + subnet + `" + name = "` + name + `" + } + + resource "qip_v4address_rr" "test" { + name = "` + name + `-extra" + address = qip_v4address.test.address + domain_name = qip_v4address.test.domain_name + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("qip_v4address.test", "name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address_rr.test", "address", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address_rr.test", "name", stringRe(name+"-extra")), + ), + }, + { + Config: ` + resource "qip_v4address" "test" { + subnet = "` + subnet + `" + name = "` + name + `" + } + + resource "qip_v4address_rr" "test" { + name = "*.` + name + `" + address = qip_v4address.test.address + domain_name = qip_v4address.test.domain_name + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("qip_v4address.test", "name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address_rr.test", "address", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address_rr.test", "name", stringRe(`*.`+name)), + ), + }, + }, + }) +} diff --git a/internal/provider/resource_v4address_test.go b/internal/provider/resource_v4address_test.go new file mode 100644 index 0000000..1e53475 --- /dev/null +++ b/internal/provider/resource_v4address_test.go @@ -0,0 +1,135 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccResourceV4Address(t *testing.T) { + address := getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_IP") + subnet := getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_SUBNET") + + name := getRandomName("terraform-qip") + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` + resource "qip_v4address" "test" { + address = "` + address + `" + subnet = "` + subnet + `" + name = "` + name + `" + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("qip_v4address.test", "address", stringRe(address)), + resource.TestMatchResourceAttr("qip_v4address.test", "subnet", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "domain_name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "object_class", regexp.MustCompile(`^Virtualized Server$`)), + ), + }, + { + Config: ` + resource "qip_v4address" "test" { + address = "` + address + `" + subnet = "` + subnet + `" + name = "` + name + `" + + description = "Added description" + object_class = "Server" + } + `, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("qip_v4address.test", "object_class", regexp.MustCompile(`^Server$`)), + resource.TestMatchResourceAttr("qip_v4address.test", "description", regexp.MustCompile(`^Added`)), + ), + }, + }, + }) +} + +func TestAccResourceV4Address_WithSelect(t *testing.T) { + subnet := getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_SUBNET") + + testSrc := ` + resource "qip_v4address" "test" { + subnet = "` + subnet + `" + name = "` + getRandomName("terraform-qip") + `" + } + ` + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testSrc, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("qip_v4address.test", "address", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "subnet", stringRe(subnet)), + resource.TestMatchResourceAttr("qip_v4address.test", "name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "domain_name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "object_class", stringNonEmptyRe), + ), + }, + }, + }) +} + +func TestAccResourceV4Address_WithSelectRange(t *testing.T) { + var ( + subnet = getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_SUBNET") + rangeStart = getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_SUBNET_START") + rangeEnd = getRequiredEnv(t, "QIP_TEST_ACC_RESOURCE_SUBNET_END") + ) + + testSrc := ` + resource "qip_v4address" "test" { + subnet = "` + subnet + `" + name = "` + getRandomName("terraform-qip") + `" + + subnet_range_start = "` + rangeStart + `" + subnet_range_end = "` + rangeEnd + `" + } + ` + + resource.UnitTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: testSrc, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("qip_v4address.test", "address", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "subnet", regexp.MustCompile("^"+regexp.QuoteMeta(subnet)+"$")), + resource.TestMatchResourceAttr("qip_v4address.test", "name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "domain_name", stringNoWhitespaceRe), + resource.TestMatchResourceAttr("qip_v4address.test", "object_class", regexp.MustCompile(`^.+$`)), + ), + }, + }, + }) +} diff --git a/internal/provider/schema.go b/internal/provider/schema.go new file mode 100644 index 0000000..2f1a9bf --- /dev/null +++ b/internal/provider/schema.go @@ -0,0 +1,129 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "net" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const MaxObjectDescriptionLength = 32 + +func schemaV4Address(forData bool) map[string]*schema.Schema { + s := map[string]*schema.Schema{ + "address": { + Description: "IPv4 address.", + Type: schema.TypeString, + Required: forData, + Optional: !forData, + Computed: !forData, + }, + "subnet": { + Description: "Subnet of the IPv4 address.", + Type: schema.TypeString, + Required: !forData, + Computed: forData, + }, + "name": { + Description: "Hostname for the address.", + Type: schema.TypeString, + Required: !forData, + Computed: forData, + }, + "description": { + Description: "Description for the address.", + Type: schema.TypeString, + Optional: !forData, + Computed: forData, + }, + "object_class": { + Description: "Object class for the address. Must be known by the QIP server.", + Type: schema.TypeString, + Optional: !forData, + Computed: forData, + Default: ifSet(!forData, "Virtualized Server"), + }, + "domain_name": { + Description: "DNS Zone of the address.", + Type: schema.TypeString, + Optional: !forData, + Computed: true, + }, + } + + if !forData { + // Add schema entries only for the resource + s["address"].ValidateDiagFunc = validateIPV4Address + s["subnet"].ValidateDiagFunc = validateIPV4Address + + s["description"].DiffSuppressFunc = func(_, oldValue, newValue string, _ *schema.ResourceData) bool { + // Do not change a value when the description length is larger than MaxObjectDescriptionLength + // and the non exceeding characters are equal. + if len(newValue) > MaxObjectDescriptionLength { + shortenedValue := newValue[0:MaxObjectDescriptionLength] + if oldValue == shortenedValue { + return true + } + } + + return false + } + + s["subnet_range_start"] = &schema.Schema{ + Description: "Starting address of a range to select a free IPv4 address from. Will be passed to QIP.", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateIPV4Address, + } + + s["subnet_range_end"] = &schema.Schema{ + Description: "Ending address of a range to select a free IPv4 address from. Will be passed to QIP.", + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateIPV4Address, + } + } + + return s +} + +func validateIPV4Address(value interface{}, _ cty.Path) diag.Diagnostics { + address, ok := value.(string) + if !ok { + return diag.Errorf("value is not a string") + } + + ip := net.ParseIP(address) + if ip == nil || ip.To4() == nil { + return diag.Errorf("value is not an IPv4 address") + } + + return nil +} + +func ifSet(condition bool, value any) any { + if condition { + return value + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2cd498e --- /dev/null +++ b/main.go @@ -0,0 +1,54 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +// Run "go generate" to format example terraform files and generate the docs for the registry/website + +// If you do not have terraform installed, you can remove the formatting command, but its suggested to +// ensure the documentation is formatted properly. +//go:generate terraform fmt -recursive ./examples/ + +// Run the docs generation tool, check its repository for more information on how it works and how docs +// can be customized. +//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs + +import ( + "flag" + + "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + + "github.com/Vitesco-Technologies/terraform-provider-qip/internal/provider" +) + +var version = "dev" + +func main() { + var debugMode bool + + flag.BoolVar(&debugMode, "debug", false, "set to true to run the provider with support for debuggers like delve") + flag.Parse() + + opts := &plugin.ServeOpts{ + Debug: debugMode, + ProviderAddr: "registry.terraform.io/vitesco-technologies/qip", + ProviderFunc: provider.New(version), + } + + plugin.Serve(opts) +} diff --git a/pkg/qip/client.go b/pkg/qip/client.go new file mode 100644 index 0000000..b460d51 --- /dev/null +++ b/pkg/qip/client.go @@ -0,0 +1,170 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package qip + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/rest" +) + +type Client struct { + BaseURL string + OrgName string + AuthToken string + Client *http.Client +} + +var ErrNoAuthToken = errors.New("no authentication token was returned in header") + +const DefaultTimeout = 20 * time.Second + +func NewClient(baseURL, orgName string) (*Client, error) { + // validate URL by parsing it + if _, err := url.Parse(baseURL); err != nil { + return nil, fmt.Errorf("base URL is not valid: %w", err) + } + + return &Client{ + BaseURL: baseURL, + OrgName: orgName, + Client: &http.Client{ + Timeout: DefaultTimeout, + }, + }, nil +} + +func (c *Client) Login(username, password string) error { + body := struct { + Username string `json:"username"` + Password string `json:"password"` + Expires uint16 `json:"expires"` + }{ + username, + password, + 10 * 60, // token will be valid for 10 minutes + } + + request, err := rest.NewRequest("POST", c.apiURL("login"), body) + if err != nil { + return fmt.Errorf("could not build login request: %w", err) + } + + // Clear the token now + c.AuthToken = "" + + response, err := c.Do(request) + if err != nil { + return err + } + + if response != nil && response.Body != nil { + _ = response.Body.Close() + } + + c.AuthToken = response.Header.Get("authentication") + if c.AuthToken == "" { + return ErrNoAuthToken + } + + return nil +} + +// Do executes and returns the http.Response. +// +// For this implementation, status codes are checked and error is returned accordingly. +func (c *Client) Do(request *http.Request) (*http.Response, error) { + if c.AuthToken != "" { + // Pass auth token to request if set + request.Header.Set("Authentication", "Token "+c.AuthToken) + } + + response, err := c.Client.Do(request) + + // Read all of body and store in buffer + var rawBody []byte + if response != nil && response.Body != nil { + rawBody, err = io.ReadAll(response.Body) + if err == nil { + response.Body.Close() + + // re-insert the body as buffer to the response + response.Body = io.NopCloser(bytes.NewBuffer(rawBody)) + } + } + + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + + switch { + case response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices: + return response, nil + case response.StatusCode >= http.StatusMultipleChoices && response.StatusCode < http.StatusBadRequest: + return response, &HTTPUnexpectedRedirectError{response} + case response.StatusCode == http.StatusUnauthorized: + return response, &HTTPUnauthorizedError{response} + case response.StatusCode == http.StatusNotFound: + return response, &HTTPNotFoundError{response} + } + + // try to parse error from + errorBody := struct { + Error string `json:"error"` + }{} + _ = json.Unmarshal(rawBody, &errorBody) + + if response.StatusCode >= 400 && response.StatusCode < 500 { + return response, &HTTPClientError{errorBody.Error, response} + } + + // response.StatusCode >= 500 + return response, &HTTPServerError{errorBody.Error, response} +} + +// apiURL builds a full URL from base and specified parts. +func (c *Client) apiURL(path ...string) string { + path = append([]string{"api"}, path...) + + fullURL, err := url.JoinPath(c.BaseURL, path...) + if err != nil { + // Errors here should not happen, we validate the URL earlier + panic(fmt.Errorf("could not join URL: %w", err)) + } + + return fullURL +} + +func (c *Client) APITenantURL(path ...string) string { + path = append([]string{"v1", c.OrgName}, path...) + + return c.apiURL(path...) +} + +// func (c *Client) ApiGlobalURL(path ...string) string { +// path = append([]string{"global", "v1"}, path...) +// return c.apiUrl(path...) +// } diff --git a/pkg/qip/client_test.go b/pkg/qip/client_test.go new file mode 100644 index 0000000..622d5f7 --- /dev/null +++ b/pkg/qip/client_test.go @@ -0,0 +1,67 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package qip_test + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/test" +) + +func TestClient_Login(t *testing.T) { + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + c, err := qip.NewClient(test.QIPServer, test.QIPOrg) + assert.NoError(t, err) + + httpmock.RegisterResponder("POST", test.QIPServer+"/api/login", + func(req *http.Request) (*http.Response, error) { + body := make(map[string]interface{}) + if err := json.NewDecoder(req.Body).Decode(&body); err != nil { + return httpmock.NewStringResponse(400, ""), nil //nolint:nilerr + } + + if user, ok := body["username"].(string); ok && user != "unknown-user" { + resp := httpmock.NewStringResponse(200, "") + resp.Header.Set("authentication", "THIS_WOULD_BE_A_BASE64_TOKEN") + + return resp, nil + } + + return httpmock.NewStringResponse(401, ""), nil + }) + + err = c.Login("unknown-user", "dummy-password") + assert.Error(t, err) + + var targetErr *qip.HTTPUnauthorizedError + + assert.ErrorAs(t, err, &targetErr) + + err = c.Login("admin", "password123") + assert.NoError(t, err) + assert.Equal(t, "THIS_WOULD_BE_A_BASE64_TOKEN", c.AuthToken) +} diff --git a/pkg/qip/errors.go b/pkg/qip/errors.go new file mode 100644 index 0000000..47a749b --- /dev/null +++ b/pkg/qip/errors.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package qip + +import ( + "net/http" +) + +// HTTPUnexpectedRedirectError - status 3XX represents a redirect to another resource. +type HTTPUnexpectedRedirectError struct { + Response *http.Response +} + +func (e *HTTPUnexpectedRedirectError) Error() string { + return "HTTP 3XX unexpected redirect" +} + +// HTTPUnauthorizedError - status 401 represents not authorized. +type HTTPUnauthorizedError struct { + Response *http.Response +} + +func (e *HTTPUnauthorizedError) Error() string { + return "HTTP 401 Authentication failed" +} + +// HTTPNotFoundError - status 404 represents resource not found. +type HTTPNotFoundError struct { + Response *http.Response +} + +func (e *HTTPNotFoundError) Error() string { + return "HTTP 404 Not Found" +} + +// HTTPClientError - status 4XX represents a client error. +type HTTPClientError struct { + Message string + Response *http.Response +} + +func (e *HTTPClientError) Error() string { + s := "HTTP 4XX other client error" + if e.Message != "" { + s += ": " + e.Message + } + + return s +} + +// HTTPServerError - status 5XX represents various server errors. +type HTTPServerError struct { + Message string + Response *http.Response +} + +func (e *HTTPServerError) Error() string { + s := "HTTP 500 Server Error" + if e.Message != "" { + s += ": " + e.Message + } + + return s +} diff --git a/pkg/qip/rr/rr.go b/pkg/qip/rr/rr.go new file mode 100644 index 0000000..411e613 --- /dev/null +++ b/pkg/qip/rr/rr.go @@ -0,0 +1,173 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rr + +import ( + "fmt" + "net/url" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/rest" +) + +//go:generate go run github.com/Vitesco-Technologies/terraform-provider-qip/pkg/utils/qip_type -type RR -package rr + +// Equal checks if two RR are equal by checking the identifying attributes. +func (record *RR) Equal(otherRecord *RR) bool { + return record.Owner == otherRecord.Owner && + record.ClassType == otherRecord.ClassType && + record.RRType == otherRecord.RRType && + record.InfraType == otherRecord.InfraType && + record.InfraAddr == otherRecord.InfraAddr && + record.InfraFQDN == otherRecord.InfraFQDN +} + +// DeleteInfo is a subset of RR to delete a RR (singleDelete is added for deletion). +type DeleteInfo struct { + Owner string `json:"owner,omitempty"` + RRType string `json:"rrType,omitempty"` + InfraType string `json:"infraType,omitempty"` + InfraFQDN string `json:"infraFQDN,omitempty"` //nolint:tagliatelle + InfraAddr string `json:"infraAddr,omitempty"` + SingleDelete bool `json:"singleDelete"` +} + +const ( + PublishingAlways = "ALWAYS" + + InfraTypeObject = "OBJECT" + + /* Unused other values for InfraType. + InfraTypeV6Address = "V6ADDRESS" + InfraTypeZone = "ZONE" + InfraTypeV4ReverseZone = "V4REVERSEZONE" + InfraTypeV6ReverseZone = "V6REVERSEZONE" + InfraTypeNode = "NODE" + InfraTypeAll = "ALL" + */ +) + +type loadResult struct { + List []*RR +} + +// NewAForObject returns a RR for a A record belonging to an object in QIP. +// +// Owner is the respective FQDN for the DNS entry. +func NewAForObject(owner, address string) *RR { + return &RR{ + Owner: owner, + ClassType: "IN", + RRType: "A", + Data1: address, + InfraType: InfraTypeObject, + InfraAddr: address, + Publishing: PublishingAlways, + TTL: -1, + IsCreatingReverseZoneRR: false, + IsDefaultRR: false, + } +} + +func LoadAllForObject(client *qip.Client, address string) ([]*RR, error) { + query := url.Values{} + query.Set("address", address) + query.Set("type", InfraTypeObject) + query.Set("getDefaultRRs", "false") + + request, err := rest.NewRequest("GET", client.APITenantURL("rr.json")+"?"+query.Encode(), nil) + if err != nil { + return nil, fmt.Errorf("could not build get request: %w", err) + } + + response, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("could not load RR: %w", err) + } + + var results loadResult + + err = rest.UnmarshalResponse(response, &results) + if err != nil { + return nil, fmt.Errorf("could not unmarshal JSON result: %w", err) + } + + return results.List, nil +} + +func Create(client *qip.Client, rr *RR) error { + request, err := rest.NewRequest("POST", client.APITenantURL("rr"), rr) + if err != nil { + return fmt.Errorf("could not build create request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not create RR: %w", err) + } + + return nil +} + +func Update(client *qip.Client, oldRR, newRR *RR) error { + data := map[string]*RR{ + "oldRRRec": oldRR, + "updatedRRRec": newRR, + } + + request, err := rest.NewRequest("PUT", client.APITenantURL("rr"), data) + if err != nil { + return fmt.Errorf("could not build update request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not update RR: %w", err) + } + + return nil +} + +// Delete will remove a RR from QIP in connection to the belonging object. +// +// Note: this copies values from an RR instance to DeleteInfo, so the API understands the deletion request. +// Sending a simple RR objects yields a NullPointerException within the API. +// This is not really well documented, you will notice the "singleDelete" attribute in the model, but not the example. +func Delete(client *qip.Client, rr *RR) error { + deleteInfo := &DeleteInfo{ + Owner: rr.Owner, + RRType: rr.RRType, + InfraType: rr.InfraType, + InfraFQDN: rr.InfraFQDN, + InfraAddr: rr.InfraAddr, + SingleDelete: true, + } + + request, err := rest.NewRequest("DELETE", client.APITenantURL("rr"), deleteInfo) + if err != nil { + return fmt.Errorf("could not build delete request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not delete RR: %w", err) + } + + return nil +} diff --git a/pkg/qip/rr/rr.json b/pkg/qip/rr/rr.json new file mode 100644 index 0000000..d4c2883 --- /dev/null +++ b/pkg/qip/rr/rr.json @@ -0,0 +1,16 @@ +{ + "owner": "Owner1", + "classType": "IN", + "rrType": "PTR", + "data1": "data1", + "publishing": "ALWAYS", + "ttl": 3200, + "infraType": "OBJECT", + "infraFQDN": "infraFQDN", + "data2": "data2", + "data3": "data3", + "data4": "data4", + "infraAddr": "192.168.88.1", + "isCreatingReverseZoneRR": true, + "isDefaultRR": false +} diff --git a/pkg/qip/rr/rr_test.go b/pkg/qip/rr/rr_test.go new file mode 100644 index 0000000..5e3c7db --- /dev/null +++ b/pkg/qip/rr/rr_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rr_test + +import ( + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/rr" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/test" +) + +func TestLoadAllForObject(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("GET", test.QIPServer+"/api/v1/"+test.QIPOrg+"/rr.json", + httpmock.NewStringResponder(200, `{ + "list": [ + { + "owner": "*.test.int.example.com", + "classType": "IN", + "rrType": "A", + "data1": "192.0.2.50", + "publishing": "ALWAYS", + "ttl": -1, + "infraType": "OBJECT", + "infraAddr": "192.0.2.50", + "tombstoned": 0, + "isCreatingReverseZoneRR": false, + "isDefaultRR": false + } + ] + }`)) + + records, err := rr.LoadAllForObject(c, "192.0.2.50") + assert.NoError(t, err) + + if assert.Len(t, records, 1) { + assert.Equal(t, "192.0.2.50", records[0].InfraAddr) + assert.Equal(t, "192.0.2.50", records[0].Data1) + } +} + +func TestCreate(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("POST", test.QIPServer+"/api/v1/"+test.QIPOrg+"/rr", + httpmock.NewStringResponder(200, `OK`)) + + record := rr.NewAForObject("*.test.int.example.com", "192.0.2.50") + + err := rr.Create(c, record) + assert.NoError(t, err) +} + +func TestUpdate(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("PUT", test.QIPServer+"/api/v1/"+test.QIPOrg+"/rr", + httpmock.NewStringResponder(200, `OK`)) + + oldRecord := rr.NewAForObject("*.test.int.example.com", "192.0.2.50") + newRecord := rr.NewAForObject("*.test2.int.example.com", "192.0.2.50") + + err := rr.Update(c, oldRecord, newRecord) + assert.NoError(t, err) +} + +func TestDelete(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("DELETE", test.QIPServer+"/api/v1/"+test.QIPOrg+"/rr", + httpmock.NewStringResponder(200, `OK`)) + + oldRecord := rr.NewAForObject("*.test2.int.example.com", "192.0.2.50") + + err := rr.Delete(c, oldRecord) + assert.NoError(t, err) +} diff --git a/pkg/qip/rr/rr_type.go b/pkg/qip/rr/rr_type.go new file mode 100644 index 0000000..0bb4362 --- /dev/null +++ b/pkg/qip/rr/rr_type.go @@ -0,0 +1,20 @@ +// Code generated by "qip_type -type RR -package rr"; DO NOT EDIT. + +package rr + +type RR struct { + Owner string `json:"owner,omitempty"` + ClassType string `json:"classType,omitempty"` + RRType string `json:"rrType,omitempty"` + Data1 string `json:"data1,omitempty"` + Publishing string `json:"publishing,omitempty"` + TTL int `json:"ttl,omitempty"` + InfraType string `json:"infraType,omitempty"` + InfraFQDN string `json:"infraFQDN,omitempty"` + Data2 string `json:"data2,omitempty"` + Data3 string `json:"data3,omitempty"` + Data4 string `json:"data4,omitempty"` + InfraAddr string `json:"infraAddr,omitempty"` + IsCreatingReverseZoneRR bool `json:"isCreatingReverseZoneRR,omitempty"` + IsDefaultRR bool `json:"isDefaultRR,omitempty"` +} diff --git a/pkg/qip/test/client.go b/pkg/qip/test/client.go new file mode 100644 index 0000000..c7e0ef6 --- /dev/null +++ b/pkg/qip/test/client.go @@ -0,0 +1,92 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package test + +import ( + "os" + "testing" + + "github.com/jarcoal/httpmock" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" +) + +const ( + QIPServer = "https://qip.example.com" + QIPOrg = "Example" +) + +func GetTestClient(t *testing.T) (*qip.Client, func()) { + t.Helper() + + httpmock.Activate() + + c, err := qip.NewClient(QIPServer, QIPOrg) + if err != nil { + t.Error(err) + } + + c.AuthToken = "TEST_TOKEN" + + // err = c.Login("dummy-username", "dummy-password") + // if err != nil { + // t.Error(err) + // } + + return c, func() { + httpmock.DeactivateAndReset() + } +} + +func GetIntegrationTestClient(t *testing.T) *qip.Client { + t.Helper() + + var ( + testServer = os.Getenv("QIP_SERVER") + testOrg = os.Getenv("QIP_ORG") + testUsername = os.Getenv("QIP_USERNAME") + testPassword = os.Getenv("QIP_PASSWORD") + ) + + if testServer == "" && testOrg == "" && testUsername == "" && testPassword == "" { + t.Skip("can not test without real QIP credentials") + } + + c, err := qip.NewClient(testServer, testOrg) + if err != nil { + t.Error(err) + } + + err = c.Login(testUsername, testPassword) + if err != nil { + t.Error(err) + } + + return c +} + +// SkipIfNotEnabled skips a testing.T if the named environment variable is not set. +func SkipIfNotEnabled(t *testing.T, name string) { + t.Helper() + + enabled := os.Getenv(name) + if enabled == "" || enabled == "0" || enabled == "false" { + t.Skip("Test must be enabled using " + name) + } +} diff --git a/pkg/qip/v4address/v4address.go b/pkg/qip/v4address/v4address.go new file mode 100644 index 0000000..f1a9c19 --- /dev/null +++ b/pkg/qip/v4address/v4address.go @@ -0,0 +1,121 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//go:generate go run github.com/Vitesco-Technologies/terraform-provider-qip/pkg/utils/qip_type -type V4Address -package v4address +package v4address + +import ( + "errors" + "fmt" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/rest" +) + +var ( + ErrBothAddrRequired = errors.New("ObjectAddr and SubnetAddr is required") + ErrObjectNameRequired = errors.New("ObjectName is required") +) + +func Load(client *qip.Client, address string) (*V4Address, error) { + request, err := rest.NewRequest("GET", client.APITenantURL("v4address", address+".json"), nil) + if err != nil { + return nil, fmt.Errorf("could not build get request: %w", err) + } + + response, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("could not load V4Address: %w", err) + } + + var o V4Address + + err = rest.UnmarshalResponse(response, &o) + if err != nil { + return nil, fmt.Errorf("could not unmarshal JSON result: %w", err) + } + + return &o, nil +} + +// Create a V4Address from input values. +// +// Required fields: +// - ObjectAddr or SubnetAddr +// - ObjectName +func Create(client *qip.Client, addr *V4Address) error { + if addr.ObjectAddr == "" || addr.SubnetAddr == "" { + return ErrBothAddrRequired + } else if addr.ObjectName == "" { + return ErrObjectNameRequired + } + + request, err := rest.NewRequest("POST", client.APITenantURL("v4address"), addr) + if err != nil { + return fmt.Errorf("could not build create request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not create V4Address: %w", err) + } + + return nil +} + +// Update an existing object or converts a select address into an object. +// +// Warning, all fields should be set during an update, leaving out e.g. domain will unset a domain association. +// +// Recommended uses: +// - LoadV4Address -> Update +// - SelectV4Address -> Update +func Update(client *qip.Client, addr *V4Address) error { + if addr.ObjectAddr == "" || addr.SubnetAddr == "" { + return ErrBothAddrRequired + } else if addr.ObjectName == "" { + return ErrObjectNameRequired + } + + request, err := rest.NewRequest("PUT", client.APITenantURL("v4address"), addr) + if err != nil { + return fmt.Errorf("could not build update request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not update V4Address: %w", err) + } + + return nil +} + +// Delete an object and frees its address in the subnet. +func Delete(client *qip.Client, addr string) error { + request, err := rest.NewRequest("DELETE", client.APITenantURL("v4address", addr, "/"), addr) + if err != nil { + return fmt.Errorf("could not build delete request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not delete V4Address: %w", err) + } + + return nil +} diff --git a/pkg/qip/v4address/v4address.json b/pkg/qip/v4address/v4address.json new file mode 100644 index 0000000..301b873 --- /dev/null +++ b/pkg/qip/v4address/v4address.json @@ -0,0 +1,90 @@ +{ + "objectAddr": "string", + "subnetAddr": "string", + "objectName": "string", + "objectClass": "string", + "domainName": "string", + "expiredDate": "string", + "serverType": "string", + "applName": "string", + "objectTag": "string", + "roomId": "string", + "manufacturer": "string", + "modelType": "string", + "serialNumber": "string", + "assetNumber": "string", + "hostId": "string", + "purchaseDate": "string", + "objectDesc": "string", + "hubName": "string", + "slotName": "string", + "portNumber": "string", + "locationId": "string", + "street1": "string", + "street2": "string", + "city": "string", + "state": "string", + "zip": "string", + "country": "string", + "contactId": "string", + "contactLastName": "string", + "contactFirstName": "string", + "contactEmail": "string", + "contactPhone": "string", + "contactPager": "string", + "routerGroup": "string", + "dynamicConfig": "string", + "macAddr": "string", + "tftpServer": "string", + "bootFileName": "string", + "hardwareType": "string", + "aliases": "string", + "mailForwarders": "string", + "mailHosts": "string", + "hubSlots": "string", + "dnsServers": "string", + "timeServers": "string", + "defaultRouters": "string", + "userClasses": "string", + "users": "string", + "nameService": "string", + "dynamicDnsUpdate": "string", + "dhcpServer": "string", + "dhcpOptionTemplate": "string", + "dhcpPolicyTemplate": "string", + "leaseTime": "string", + "ttlTime": "string", + "vendorClass": "string", + "clientId": "string", + "dualProtocol": "string", + "decNetArea": "string", + "decNetAddr": "string", + "decNetNode": "string", + "talkType": "string", + "ipxNode": "string", + "ipxNetworkNumber": "string", + "netBiosDomain": "string", + "netBiosName": "string", + "usageBillServices": "string", + "usageBillLocation": "string", + "usageBillUserGroup": "string", + "usageBillObjectClass": "string", + "allowModifyDynamicRRs": "string", + "tombstoned": "string", + "externalComment": "string", + "externalTimestamp": "string", + "manualFlag": "string", + "nodeId": "string", + "uniqueNodeId": "string", + "aTTL": "string", + "ptrTTL": "string", + "publishA": "string", + "publishPTR": "string", + "dhcpClientClass": "string", + "isUpdate": "string", + "isAddSelected": "string", + "isCheckDupName": "string", + "isCheckOnlyFQDNDups": "string", + "isSwapAliasAndObjectName": "string", + "localManualFlag": "string" +} diff --git a/pkg/qip/v4address/v4address_select.go b/pkg/qip/v4address/v4address_select.go new file mode 100644 index 0000000..75467f9 --- /dev/null +++ b/pkg/qip/v4address/v4address_select.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4address + +import ( + "errors" + "fmt" + "sync" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/rest" +) + +type SelectedAddrRange struct { + StartAddress string `json:"startAddress"` + EndAddress string `json:"endAddress"` +} + +// selectMutex for ensuring that no other selectedv4address operation happens at the same time. +var selectMutex sync.Mutex //nolint:gochecknoglobals + +var ErrNoSelection = errors.New("no object address was returned") + +// CreateSelected reserves a new address within the range and returns the objectAddr. +// +// If you don't want to create the IP, you need to free it, not sure if it will expire. +func CreateSelected(client *qip.Client, subnet string, addrs *SelectedAddrRange) (string, error) { + var body any + + if addrs != nil { + body = struct { + AddrRange []*SelectedAddrRange `json:"addrRange"` + }{ + AddrRange: []*SelectedAddrRange{addrs}, + } + } + + request, err := rest.NewRequest("PUT", client.APITenantURL("selectedv4address", subnet+".json"), body) + if err != nil { + return "", fmt.Errorf("could not build select request: %w", err) + } + + selectMutex.Lock() + + response, err := client.Do(request) + if err != nil { + selectMutex.Unlock() + + return "", fmt.Errorf("could not create SelectedV4Address: %w", err) + } + + selectMutex.Unlock() + + var addr V4Address + + err = rest.UnmarshalResponse(response, &addr) + if err != nil { + return "", fmt.Errorf("could not unmarshal v4address: %w", err) + } else if addr.ObjectAddr == "" { + return "", ErrNoSelection + } + + return addr.ObjectAddr, nil +} + +// DeleteSelected clears the reservation in the API for an address. +func DeleteSelected(client *qip.Client, addr string) error { + request, err := rest.NewRequest("DELETE", client.APITenantURL("selectedv4address", addr, "/"), nil) + if err != nil { + return fmt.Errorf("could not build delete request: %w", err) + } + + _, err = client.Do(request) + if err != nil { + return fmt.Errorf("could not delete SelectedV4Address: %w", err) + } + + // Notes for error handling: + // - Unknown address should return "Internal Server Error - IP address [address] does not have an object associated with it" + // but it fails with a NullPointerException + + return nil +} diff --git a/pkg/qip/v4address/v4address_select_test.go b/pkg/qip/v4address/v4address_select_test.go new file mode 100644 index 0000000..47982bf --- /dev/null +++ b/pkg/qip/v4address/v4address_select_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4address_test + +import ( + "os" + "sync" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/test" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/v4address" +) + +func TestCreateSelected(t *testing.T) { + c, cleanup := test.GetTestClient(t) + + defer cleanup() + + httpmock.RegisterResponder("PUT", test.QIPServer+"/api/v1/"+test.QIPOrg+"/selectedv4address/192.0.2.0.json", + httpmock.NewStringResponder(200, `{"objectAddr":"192.0.2.2"}`)) + + addr, err := v4address.CreateSelected(c, "192.0.2.0", nil) + assert.NoError(t, err) + assert.Equal(t, "192.0.2.2", addr) + + httpmock.RegisterResponder("PUT", test.QIPServer+"/api/v1/"+test.QIPOrg+"/selectedv4address/192.0.2.0.json", + httpmock.NewStringResponder(200, `{"objectAddr":"192.0.2.25"}`)) + + addressRange := &v4address.SelectedAddrRange{ + StartAddress: "192.0.2.25", + EndAddress: "192.0.2.30", + } + addr, err = v4address.CreateSelected(c, "192.0.2.0", addressRange) + assert.NoError(t, err) + assert.Equal(t, "192.0.2.25", addr) +} + +func TestDeleteSelected(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("DELETE", test.QIPServer+"/api/v1/"+test.QIPOrg+"/selectedv4address/192.0.2.25/", + httpmock.NewStringResponder(200, ``)) + + err := v4address.DeleteSelected(c, "192.0.2.25") + assert.NoError(t, err) +} + +// TestAccCreateBulkSelected will test if multiple selects against the QIP API fail. +// +// This is a race condition bug in the QIP API, that needs a workaround on client side. +func TestAccCreateBulkSelected(t *testing.T) { + c := test.GetIntegrationTestClient(t) + + test.SkipIfNotEnabled(t, "QIP_TEST_ACC_BULK_SELECT_ENABLED") + + subnet, addressRange := getAccSubnet(t) + + var ( + wait sync.WaitGroup + count = 4 + addrs = make([]*string, count) + ) + + wait.Add(count) + + for num := 0; num < count; num++ { + go func(num int) { + defer wait.Done() + + addr, err := v4address.CreateSelected(c, subnet, addressRange) + assert.NoError(t, err) + assert.NotEmpty(t, addr) + + t.Logf("Selected address #%d: %s", num, addr) + + addrs[num] = &addr + }(num) + } + + wait.Wait() + + // One could check here if all addresses are unique + + for num := 0; num < count; num++ { + if addrs[num] == nil || *addrs[num] == "" { + continue + } + + assert.NoError(t, v4address.DeleteSelected(c, *addrs[num])) + } +} + +func getAccSubnet(t *testing.T) (string, *v4address.SelectedAddrRange) { + t.Helper() + + subnet := os.Getenv("QIP_TEST_ACC_SUBNET") + if subnet == "" { + t.Skip("QIP_TEST_ACC_SUBNET required") + } + + var ( + subnetStart = os.Getenv("QIP_TEST_ACC_RESOURCE_SUBNET_START") + subnetEnd = os.Getenv("QIP_TEST_ACC_RESOURCE_SUBNET_END") + ) + + var addressRange *v4address.SelectedAddrRange + + if subnetStart != "" && subnetEnd != "" { + addressRange = &v4address.SelectedAddrRange{ + StartAddress: subnetStart, + EndAddress: subnetEnd, + } + } + + return subnet, addressRange +} diff --git a/pkg/qip/v4address/v4address_test.go b/pkg/qip/v4address/v4address_test.go new file mode 100644 index 0000000..c7ebaa8 --- /dev/null +++ b/pkg/qip/v4address/v4address_test.go @@ -0,0 +1,168 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4address_test + +import ( + "os" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/test" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/v4address" +) + +func TestLoad(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("GET", test.QIPServer+"/api/v1/"+test.QIPOrg+"/v4address/192.0.2.50.json", + httpmock.NewStringResponder(200, `{"objectAddr":"192.0.2.50","subnetAddr":"192.0.2.0","objectName":"test-host"}`)) + + addr, err := v4address.Load(c, "192.0.2.50") + assert.NoError(t, err) + assert.Equal(t, "192.0.2.50", addr.ObjectAddr) +} + +func TestCreate(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + addr := &v4address.V4Address{ + SubnetAddr: "192.0.2.0", + ObjectAddr: "192.0.2.50", + ObjectName: "test-host", + } + + httpmock.RegisterResponder("POST", test.QIPServer+"/api/v1/"+test.QIPOrg+"/v4address", + httpmock.NewStringResponder(200, "")) + + err := v4address.Create(c, addr) + assert.NoError(t, err) +} + +func TestUpdate(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + addr := &v4address.V4Address{ + SubnetAddr: "192.0.2.0", + ObjectAddr: "192.0.2.50", + ObjectName: "test-host", + } + + httpmock.RegisterResponder("PUT", test.QIPServer+"/api/v1/"+test.QIPOrg+"/v4address", + httpmock.NewStringResponder(200, "")) + + err := v4address.Update(c, addr) + assert.NoError(t, err) +} + +func TestDelete(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("DELETE", test.QIPServer+"/api/v1/"+test.QIPOrg+"/v4address/192.0.2.55/", + httpmock.NewStringResponder(200, "")) + + err := v4address.Delete(c, "192.0.2.55") + assert.NoError(t, err) +} + +func TestE2E(t *testing.T) { + c := test.GetIntegrationTestClient(t) + testSubnet, _ := getTestSubnet(t) + + addr := os.Getenv("QIP_TEST_SUBNET_IP") + if addr == "" { + t.Skip("can not test without QIP_TEST_SUBNET_IP") + } + + addrObj := &v4address.V4Address{ + ObjectAddr: addr, + SubnetAddr: testSubnet, + ObjectName: "terraform-provider-qip", + ObjectDesc: "Terraform integration testing", + ObjectClass: "Virtualized Server", + } + + err := v4address.Create(c, addrObj) + assert.NoError(t, err) + + err = v4address.Delete(c, addr) + assert.NoError(t, err) +} + +func TestE2E_WithSelect(t *testing.T) { + c := test.GetIntegrationTestClient(t) + testSubnet, testAddrRange := getTestSubnet(t) + + addr, err := v4address.CreateSelected(c, testSubnet, testAddrRange) + assert.NoError(t, err) + assert.NotEmpty(t, addr) + + // Not tested here - we update the selected address + // err = qip.DeleteSelectedV4Address(c, addr) + // assert.NoError(t, err) + + addrObj := &v4address.V4Address{ + ObjectAddr: addr, + SubnetAddr: testSubnet, + ObjectName: "terraform-provider-qip", + ObjectDesc: "Terraform integration testing", + ObjectClass: "Virtualized Server", + } + + err = v4address.Update(c, addrObj) + assert.NoError(t, err) + + updatedObj, err := v4address.Load(c, addr) + assert.NoError(t, err) + assert.Equal(t, "Virtualized Server", updatedObj.ObjectClass) + assert.NotEmpty(t, updatedObj.ObjectDesc) + assert.NotEqual(t, "None", updatedObj.DomainName) + + err = v4address.Delete(c, addr) + assert.NoError(t, err) +} + +func getTestSubnet(t *testing.T) (string, *v4address.SelectedAddrRange) { + t.Helper() + + var ( + testSubnet = os.Getenv("QIP_TEST_SUBNET") + addrRange *v4address.SelectedAddrRange + testSubnetRangeStart = os.Getenv("QIP_TEST_SUBNET_RANGE_START") + testSubnetRangeEnd = os.Getenv("QIP_TEST_SUBNET_RANGE_END") + ) + + if testSubnet == "" { + t.Skip("can not run without QIP_TEST_SUBNET") + } + + if testSubnetRangeStart != "" && testSubnetRangeEnd != "" { + addrRange = &v4address.SelectedAddrRange{ + StartAddress: testSubnetRangeStart, + EndAddress: testSubnetRangeEnd, + } + } + + return testSubnet, addrRange +} diff --git a/pkg/qip/v4address/v4address_type.go b/pkg/qip/v4address/v4address_type.go new file mode 100644 index 0000000..9688161 --- /dev/null +++ b/pkg/qip/v4address/v4address_type.go @@ -0,0 +1,94 @@ +// Code generated by "qip_type -type V4Address -package v4address"; DO NOT EDIT. + +package v4address + +type V4Address struct { + ObjectAddr string `json:"objectAddr,omitempty"` + SubnetAddr string `json:"subnetAddr,omitempty"` + ObjectName string `json:"objectName,omitempty"` + ObjectClass string `json:"objectClass,omitempty"` + DomainName string `json:"domainName,omitempty"` + ExpiredDate string `json:"expiredDate,omitempty"` + ServerType string `json:"serverType,omitempty"` + ApplName string `json:"applName,omitempty"` + ObjectTag string `json:"objectTag,omitempty"` + RoomId string `json:"roomId,omitempty"` + Manufacturer string `json:"manufacturer,omitempty"` + ModelType string `json:"modelType,omitempty"` + SerialNumber string `json:"serialNumber,omitempty"` + AssetNumber string `json:"assetNumber,omitempty"` + HostId string `json:"hostId,omitempty"` + PurchaseDate string `json:"purchaseDate,omitempty"` + ObjectDesc string `json:"objectDesc,omitempty"` + HubName string `json:"hubName,omitempty"` + SlotName string `json:"slotName,omitempty"` + PortNumber string `json:"portNumber,omitempty"` + LocationId string `json:"locationId,omitempty"` + Street1 string `json:"street1,omitempty"` + Street2 string `json:"street2,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Zip string `json:"zip,omitempty"` + Country string `json:"country,omitempty"` + ContactId string `json:"contactId,omitempty"` + ContactLastName string `json:"contactLastName,omitempty"` + ContactFirstName string `json:"contactFirstName,omitempty"` + ContactEmail string `json:"contactEmail,omitempty"` + ContactPhone string `json:"contactPhone,omitempty"` + ContactPager string `json:"contactPager,omitempty"` + RouterGroup string `json:"routerGroup,omitempty"` + DynamicConfig string `json:"dynamicConfig,omitempty"` + MacAddr string `json:"macAddr,omitempty"` + TftpServer string `json:"tftpServer,omitempty"` + BootFileName string `json:"bootFileName,omitempty"` + HardwareType string `json:"hardwareType,omitempty"` + Aliases string `json:"aliases,omitempty"` + MailForwarders string `json:"mailForwarders,omitempty"` + MailHosts string `json:"mailHosts,omitempty"` + HubSlots string `json:"hubSlots,omitempty"` + DnsServers string `json:"dnsServers,omitempty"` + TimeServers string `json:"timeServers,omitempty"` + DefaultRouters string `json:"defaultRouters,omitempty"` + UserClasses string `json:"userClasses,omitempty"` + Users string `json:"users,omitempty"` + NameService string `json:"nameService,omitempty"` + DynamicDnsUpdate string `json:"dynamicDnsUpdate,omitempty"` + DhcpServer string `json:"dhcpServer,omitempty"` + DhcpOptionTemplate string `json:"dhcpOptionTemplate,omitempty"` + DhcpPolicyTemplate string `json:"dhcpPolicyTemplate,omitempty"` + LeaseTime string `json:"leaseTime,omitempty"` + TTLTime string `json:"ttlTime,omitempty"` + VendorClass string `json:"vendorClass,omitempty"` + ClientId string `json:"clientId,omitempty"` + DualProtocol string `json:"dualProtocol,omitempty"` + DecNetArea string `json:"decNetArea,omitempty"` + DecNetAddr string `json:"decNetAddr,omitempty"` + DecNetNode string `json:"decNetNode,omitempty"` + TalkType string `json:"talkType,omitempty"` + IpxNode string `json:"ipxNode,omitempty"` + IpxNetworkNumber string `json:"ipxNetworkNumber,omitempty"` + NetBiosDomain string `json:"netBiosDomain,omitempty"` + NetBiosName string `json:"netBiosName,omitempty"` + UsageBillServices string `json:"usageBillServices,omitempty"` + UsageBillLocation string `json:"usageBillLocation,omitempty"` + UsageBillUserGroup string `json:"usageBillUserGroup,omitempty"` + UsageBillObjectClass string `json:"usageBillObjectClass,omitempty"` + AllowModifyDynamicRRs string `json:"allowModifyDynamicRRs,omitempty"` + Tombstoned string `json:"tombstoned,omitempty"` + ExternalComment string `json:"externalComment,omitempty"` + ExternalTimestamp string `json:"externalTimestamp,omitempty"` + ManualFlag string `json:"manualFlag,omitempty"` + NodeId string `json:"nodeId,omitempty"` + UniqueNodeId string `json:"uniqueNodeId,omitempty"` + ATTL string `json:"aTTL,omitempty"` + PtrTTL string `json:"ptrTTL,omitempty"` + PublishA string `json:"publishA,omitempty"` + PublishPTR string `json:"publishPTR,omitempty"` + DhcpClientClass string `json:"dhcpClientClass,omitempty"` + IsUpdate string `json:"isUpdate,omitempty"` + IsAddSelected string `json:"isAddSelected,omitempty"` + IsCheckDupName string `json:"isCheckDupName,omitempty"` + IsCheckOnlyFQDNDups string `json:"isCheckOnlyFQDNDups,omitempty"` + IsSwapAliasAndObjectName string `json:"isSwapAliasAndObjectName,omitempty"` + LocalManualFlag string `json:"localManualFlag,omitempty"` +} diff --git a/pkg/qip/v4subnet/v4address_test.go b/pkg/qip/v4subnet/v4address_test.go new file mode 100644 index 0000000..5edc4a5 --- /dev/null +++ b/pkg/qip/v4subnet/v4address_test.go @@ -0,0 +1,58 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4subnet_test + +import ( + "os" + "testing" + + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/test" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip/v4subnet" +) + +func TestLoad(t *testing.T) { + c, cleanup := test.GetTestClient(t) + defer cleanup() + + httpmock.RegisterResponder("GET", test.QIPServer+"/api/v1/"+test.QIPOrg+"/v4subnet/192.0.2.0.json", + httpmock.NewStringResponder(200, + `{"subnetAddress":"192.0.2.0","subnetMask":"255.255.255.0","subnetName":"test-subnet"}`)) + + addr, err := v4subnet.Load(c, "192.0.2.0") + assert.NoError(t, err) + assert.Equal(t, "192.0.2.0", addr.SubnetAddress) + assert.Equal(t, "test-subnet", addr.SubnetName) +} + +func TestAccLoad(t *testing.T) { + c := test.GetIntegrationTestClient(t) + + testSubnet := os.Getenv("QIP_TEST_SUBNET") + if testSubnet == "" { + t.Skip("can not run without QIP_TEST_SUBNET") + } + + subnet, err := v4subnet.Load(c, testSubnet) + assert.NoError(t, err) + assert.Equal(t, testSubnet, subnet.SubnetAddress) + assert.NotEmpty(t, subnet.SubnetName) +} diff --git a/pkg/qip/v4subnet/v4subnet.go b/pkg/qip/v4subnet/v4subnet.go new file mode 100644 index 0000000..53bba3b --- /dev/null +++ b/pkg/qip/v4subnet/v4subnet.go @@ -0,0 +1,51 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v4subnet + +import ( + "fmt" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/qip" + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/rest" +) + +// Generate the type from a JSON statement (from QIP rest-api documentation) +//go:generate go run github.com/Vitesco-Technologies/terraform-provider-qip/pkg/utils/qip_type -type V4Subnet -package v4subnet + +// Load returns V4Subnet data from the API. +func Load(client *qip.Client, address string) (*V4Subnet, error) { + request, err := rest.NewRequest("GET", client.APITenantURL("v4subnet", address+".json"), nil) + if err != nil { + return nil, fmt.Errorf("could not build get request: %w", err) + } + + response, err := client.Do(request) + if err != nil { + return nil, fmt.Errorf("could not load V4Subnet: %w", err) + } + + var o V4Subnet + + err = rest.UnmarshalResponse(response, &o) + if err != nil { + return nil, fmt.Errorf("could not unmarshal JSON result: %w", err) + } + + return &o, nil +} diff --git a/pkg/qip/v4subnet/v4subnet.json b/pkg/qip/v4subnet/v4subnet.json new file mode 100644 index 0000000..0aaa1cb --- /dev/null +++ b/pkg/qip/v4subnet/v4subnet.json @@ -0,0 +1,81 @@ +{ + "subnetAddress": "string", + "subnetMask": "string", + "networkAddress": "string", + "subnetName": "string", + "subnetOrg": "string", + "application": "string", + "location": { + "id": 0, + "street1": "string", + "street2": "string", + "city": "string", + "state": "string", + "zip": "string", + "country": "string" + }, + "contact": { + "id": 0, + "firstName": "string", + "lastName": "string", + "telephone": "string", + "pager": "string", + "email": "string" + }, + "hardwareType": "ETHERNET", + "tftpServerName": "string", + "allowDHCPClientsToModifyDynamicObjectRRs": "SAME_AS_IN_GLOBAL_POLICIES", + "domains": { + "name": [ + "string" + ] + }, + "showUsage": "string", + "checkUsage": "string", + "subnetDescription": "string", + "sharedNetwork": "string", + "warningType": 0, + "warningPercentage": 0, + "preferredDNSServers": { + "name": [ + "string" + ] + }, + "preferredTimeServers": { + "name": [ + "string" + ] + }, + "defaultRouters": { + "name": [ + "string" + ] + }, + "defaultDHCPServer": "string", + "defaultDHCPOptionTemplate": "string", + "defaultDHCPPolicyTemplate": "string", + "primaryInterface": true, + "optionalAttributeList": { + "udas": [ + { + "name": "string", + "value": "string" + } + ], + "groups": [ + { + "name": "string", + "udas": [ + { + "name": "string", + "value": "string" + } + ] + } + ] + }, + "addressTemplate": "string", + "localTotalDynamicObjects": 0, + "localLeasedDynamicObjects": 0, + "localPercentDynamicObjectsUsed": 0 +} diff --git a/pkg/qip/v4subnet/v4subnet_type.go b/pkg/qip/v4subnet/v4subnet_type.go new file mode 100644 index 0000000..964a85d --- /dev/null +++ b/pkg/qip/v4subnet/v4subnet_type.go @@ -0,0 +1,91 @@ +// Code generated by "qip_type -type V4Subnet -package v4subnet"; DO NOT EDIT. + +package v4subnet + +type V4Subnet struct { + SubnetAddress string `json:"subnetAddress,omitempty"` + SubnetMask string `json:"subnetMask,omitempty"` + NetworkAddress string `json:"networkAddress,omitempty"` + SubnetName string `json:"subnetName,omitempty"` + SubnetOrg string `json:"subnetOrg,omitempty"` + Application string `json:"application,omitempty"` + Location V4SubnetLocation `json:"location,omitempty"` + Contact V4SubnetContact `json:"contact,omitempty"` + HardwareType string `json:"hardwareType,omitempty"` + TftpServerName string `json:"tftpServerName,omitempty"` + AllowDHCPClientsToModifyDynamicObjectRRs string `json:"allowDHCPClientsToModifyDynamicObjectRRs,omitempty"` + Domains V4SubnetDomains `json:"domains,omitempty"` + ShowUsage string `json:"showUsage,omitempty"` + CheckUsage string `json:"checkUsage,omitempty"` + SubnetDescription string `json:"subnetDescription,omitempty"` + SharedNetwork string `json:"sharedNetwork,omitempty"` + WarningType int `json:"warningType,omitempty"` + WarningPercentage float64 `json:"warningPercentage,omitempty"` + PreferredDNSServers V4SubnetPreferredDNSServers `json:"preferredDNSServers,omitempty"` + PreferredTimeServers V4SubnetPreferredTimeServers `json:"preferredTimeServers,omitempty"` + DefaultRouters V4SubnetDefaultRouters `json:"defaultRouters,omitempty"` + DefaultDHCPServer string `json:"defaultDHCPServer,omitempty"` + DefaultDHCPOptionTemplate string `json:"defaultDHCPOptionTemplate,omitempty"` + DefaultDHCPPolicyTemplate string `json:"defaultDHCPPolicyTemplate,omitempty"` + PrimaryInterface bool `json:"primaryInterface,omitempty"` + OptionalAttributeList V4SubnetOptionalAttributeList `json:"optionalAttributeList,omitempty"` + AddressTemplate string `json:"addressTemplate,omitempty"` + LocalTotalDynamicObjects int `json:"localTotalDynamicObjects,omitempty"` + LocalLeasedDynamicObjects int `json:"localLeasedDynamicObjects,omitempty"` + LocalPercentDynamicObjectsUsed float64 `json:"localPercentDynamicObjectsUsed,omitempty"` +} + +type V4SubnetLocation struct { + ID int `json:"id,omitempty"` + Street1 string `json:"street1,omitempty"` + Street2 string `json:"street2,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Zip string `json:"zip,omitempty"` + Country string `json:"country,omitempty"` +} + +type V4SubnetContact struct { + ID int `json:"id,omitempty"` + FirstName string `json:"firstName,omitempty"` + LastName string `json:"lastName,omitempty"` + Telephone string `json:"telephone,omitempty"` + Pager string `json:"pager,omitempty"` + Email string `json:"email,omitempty"` +} + +type V4SubnetDomains struct { + Name []string `json:"name,omitempty"` +} + +type V4SubnetPreferredDNSServers struct { + Name []string `json:"name,omitempty"` +} + +type V4SubnetPreferredTimeServers struct { + Name []string `json:"name,omitempty"` +} + +type V4SubnetDefaultRouters struct { + Name []string `json:"name,omitempty"` +} + +type V4SubnetOptionalAttributeList struct { + Udas []V4SubnetOptionalAttributeListUdas `json:"udas,omitempty"` + Groups []V4SubnetOptionalAttributeListGroups `json:"groups,omitempty"` +} + +type V4SubnetOptionalAttributeListUdas struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} + +type V4SubnetOptionalAttributeListGroups struct { + Name string `json:"name,omitempty"` + Udas []V4SubnetOptionalAttributeListGroupsUdas `json:"udas,omitempty"` +} + +type V4SubnetOptionalAttributeListGroupsUdas struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} diff --git a/pkg/rest/rest.go b/pkg/rest/rest.go new file mode 100644 index 0000000..b70e6e6 --- /dev/null +++ b/pkg/rest/rest.go @@ -0,0 +1,66 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rest + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +func NewRequest(method, url string, body any) (*http.Request, error) { + var buf io.Reader + + if body != nil { + data, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("could not marshal to JSON: %w", err) + } + + buf = bytes.NewBuffer(data) + } + + request, err := http.NewRequest(method, url, buf) + if err != nil { + return nil, fmt.Errorf("building request failed: %w", err) + } + + request.Header.Set("Accept", "application/json") + request.Header.Set("Content-Type", "application/json") + + return request, nil +} + +func UnmarshalResponse(response *http.Response, v any) error { + defer response.Body.Close() + + data, err := io.ReadAll(response.Body) + if err != nil { + return fmt.Errorf("could not read the full response body: %w", err) + } + + err = json.Unmarshal(data, v) + if err != nil { + return fmt.Errorf("could not unmarshal JSON: %w", err) + } + + return nil +} diff --git a/pkg/rest/rest_test.go b/pkg/rest/rest_test.go new file mode 100644 index 0000000..b170530 --- /dev/null +++ b/pkg/rest/rest_test.go @@ -0,0 +1,63 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rest_test + +import ( + "bytes" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/Vitesco-Technologies/terraform-provider-qip/pkg/rest" +) + +type testStruct struct { + Something string `json:"something"` +} + +const testStructJSON = `{"something":"test"}` + +var testStructData = testStruct{"test"} + +func TestNewRESTRequest(t *testing.T) { + request, err := rest.NewRequest("GET", "http://localhost/resource", nil) + assert.NoError(t, err) + assert.Nil(t, request.Body) + + request, err = rest.NewRequest("POST", "http://localhost/login", testStructData) + assert.NoError(t, err) + assert.NotNil(t, request.Body) + + data, err := io.ReadAll(request.Body) + assert.NoError(t, err) + assert.Equal(t, testStructJSON, string(data)) +} + +func TestUnmarshalRESTResponse(t *testing.T) { + response := &http.Response{ + Body: io.NopCloser(bytes.NewBufferString(testStructJSON)), + } + + var o testStruct + err := rest.UnmarshalResponse(response, &o) + assert.NoError(t, err) + assert.Equal(t, "test", o.Something) +} diff --git a/pkg/utils/qip_type/qip_type.go b/pkg/utils/qip_type/qip_type.go new file mode 100644 index 0000000..0b7b761 --- /dev/null +++ b/pkg/utils/qip_type/qip_type.go @@ -0,0 +1,227 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "go/format" + "log" + "os" + "path/filepath" + "strings" + + "github.com/iancoleman/orderedmap" +) + +var ( + typeNames = flag.String("type", "", "comma-separated list of type names. required") + packageName = flag.String("package", "", "name of the golang package. required") + output = flag.String("output", "", "output file name; default srcdir/_type.go") +) + +var uppercaseWords = []string{ + "ttl", + "id", + "rr", +} + +const fileReadWrite = 0o644 + +func Usage() { + fmt.Fprintf(os.Stderr, "Usage of qip_type:\n") + fmt.Fprintf(os.Stderr, " qip_type [flags] -type T\n") + fmt.Fprintf(os.Stderr, "Flags:\n") + flag.PrintDefaults() +} + +func main() { + log.SetFlags(0) + log.SetPrefix("qip_type: ") + + flag.Usage = Usage + flag.Parse() + + if len(*typeNames) == 0 || len(*packageName) == 0 { + flag.Usage() + os.Exit(2) //nolint:gomnd + } + + types := strings.Split(*typeNames, ",") + + var g Generator + + fmt.Fprintf(&g.buf, "// Code generated by \"qip_type %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " ")) + fmt.Fprintf(&g.buf, "\n") + fmt.Fprintf(&g.buf, "package %s", *packageName) + fmt.Fprintf(&g.buf, "\n") + + for _, typeName := range types { + g.Parse(typeName) + } + + src := g.Format() + + outputName := *output + if outputName == "" { + baseName := fmt.Sprintf("%s_type.go", types[0]) + // Output will be to local directory + dir, _ := os.Getwd() + outputName = filepath.Join(dir, strings.ToLower(baseName)) + } + + err := os.WriteFile(outputName, src, fileReadWrite) + if err != nil { + log.Fatalf("writing output: %s", err) + } +} + +type Generator struct { + buf bytes.Buffer +} + +func (g *Generator) Parse(typeName string) { + name := strings.ToLower(typeName) + schemaFile := name + ".json" + + if _, err := os.Stat(schemaFile); err != nil { + log.Printf("warning: could not find schema file %s: %s", schemaFile, err) + + return + } + + content, err := os.ReadFile(schemaFile) + if err != nil { + log.Printf("warning: could not read schema file %s: %s", schemaFile, err) + + return + } + + schema := orderedmap.New() + + err = json.Unmarshal(content, &schema) + if err != nil { + log.Printf("warning: could not parse JSON of schema file %s: %s", schemaFile, err) + + return + } + + g.buf.Write(parseOrderedMap(typeName, schema).Bytes()) +} + +func parseOrderedMap(name string, data *orderedmap.OrderedMap) *bytes.Buffer { + var buf bytes.Buffer + + subTypes := orderedmap.New() + + fmt.Fprintf(&buf, "type %s struct {\n", name) + + for _, key := range data.Keys() { + field := key + + // Uppercase known prefixes or values. + for _, word := range uppercaseWords { + if strings.HasPrefix(field, word) { + l := len(word) + field = strings.ToUpper(field[0:l]) + field[l:] + } + } + + // Ensure field is public, by upper casing first character + field = strings.ToUpper(field[0:1]) + field[1:] + + // parse types from the schema + var goType string + + value, _ := data.Get(key) + switch t := value.(type) { + case string: + // Most strings just contain the word "string" or exemplary values. + // Some strings can refer to ENUMs, for now we are treating them all as strings. + goType = "string" + case float64: + // This can be a float64 or int, QIP API refers to this as "integer" and "number". + // As schema is unclear on this, we will apply it based on the field name. + if strings.Contains(key, "Percent") { + goType = "float64" + } else { + goType = "int" + } + case bool: + goType = "bool" + case orderedmap.OrderedMap: + // Remember map to post process + goType = name + field + subTypes.Set(goType, value) + case []interface{}: + // An array with a single entry is another schema example + if len(t) == 1 { + if v, _ := t[0].(string); v == "string" { + // Check if the array has a single element "string", then simple list of names + goType = "[]string" + } else if v, ok := t[0].(orderedmap.OrderedMap); ok { + // Is a list of sub types + goType = name + field + subTypes.Set(goType, v) + goType = "[]" + goType + } + } + } + + if goType == "" { + log.Printf("warning: unsupported type: %s is %T", key, value) + + if s, ok := value.(string); ok { + log.Printf("warning: unsupported string type: %s is %s", key, s) + } + } else { + fmt.Fprintf(&buf, "\t%s %s `json:\"%s,omitempty\"`\n", field, goType, key) + } + } + + buf.WriteString("}\n\n") + + // Process sub types + for _, key := range subTypes.Keys() { + if interf, ok := subTypes.Get(key); ok { + if value, ok := interf.(orderedmap.OrderedMap); ok { + buf.Write(parseOrderedMap(key, &value).Bytes()) + } + } + } + + return &buf +} + +// format returns the gofmt-ed contents of the Generator's buffer. +func (g *Generator) Format() []byte { + src, err := format.Source(g.buf.Bytes()) + if err != nil { + // Should never happen, but can arise when developing this code. + // The user can compile the output to see the error. + log.Printf("warning: internal error: invalid Go generated: %s", err) + log.Printf("warning: compile the package to analyze the error") + + return g.buf.Bytes() + } + + return src +} diff --git a/pkg/utils/rand.go b/pkg/utils/rand.go new file mode 100644 index 0000000..32404ad --- /dev/null +++ b/pkg/utils/rand.go @@ -0,0 +1,39 @@ +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package utils + +import "crypto/rand" + +var chars = "abcdefghijklmnopqrstuvwxyz" + +// ShortID returns a short alphabetic id generated randomly from all lowercase characters. +func ShortID(length int) string { + ll := len(chars) + buf := make([]byte, length) + + if _, err := rand.Read(buf); err != nil { + panic(err) + } + + for i := 0; i < length; i++ { + buf[i] = chars[int(buf[i])%ll] + } + + return string(buf) +} diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..32fa038 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,27 @@ +//go:build tools +// +build tools + +/* +Copyright 2023 Vitesco Technologies Group AG + +SPDX-License-Identifier: Apache-2.0 + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tools + +import ( + // document generation + _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" +)