diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f5e590ed..f022e7d1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,24 +1,34 @@ name: build on: - push: - branches: [ master ] pull_request: - branches: [ master ] + branches: [ master, main ] jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v3 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v3 with: - go-version: 1.21.3 + go-version: 1.20.2 - name: Linters - run: make lint + run: make linters - - name: Tests - run: make test + - name: Build + run: go build -v ./... + + - name: Tests data_source + env: + EC_USERNAME: ${{ secrets.EC_USERNAME }} + EC_PASSWORD: ${{ secrets.EC_PASSWORD }} + run: make test_cloud_data_source + + - name: Tests resource + env: + EC_USERNAME: ${{ secrets.EC_USERNAME }} + EC_PASSWORD: ${{ secrets.EC_PASSWORD }} + run: make test_cloud_resource diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b0a841a4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,49 @@ +# This GitHub action can publish assets for release when a tag is created. +# Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). +# +# This uses an action (hashicorp/ghaction-import-gpg) that assumes you set your +# private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` +# secret. If you would rather own your own GPG handling, please fork this action +# or use an alternative one for key handling. +# +# You will need to pass the `--batch` flag to `gpg` in your signing step +# in `goreleaser` to indicate this is being used in a non-interactive mode. +# +name: release +on: + push: + tags: + - 'v*' +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Unshallow + run: git fetch --prune --unshallow + - + name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.20.2 + - + name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v5 + with: + # These secrets will need to be configured for the repository: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + version: latest + args: release --rm-dist + env: + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} +# # GitHub sets this automatically + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index dcaaecfb..f1048b22 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,19 +1,32 @@ run: timeout: 10m - go: "1.21.3" + go: "1.20" issues: max-per-linter: 0 max-same-issues: 0 + exclude: + # revive, stylecheck: ignore constants in all caps + - don't use ALL_CAPS in Go names; use CamelCase + - ST1003 + # gosec + - G401 + - G501 exclude-rules: - - path: (.+)_test.go + - path: \.go linters: - - funlen + - nolintlint + text: should be written without leading space + - text: lifecyclepolicy.CreateScheduleOpts + linters: + - ireturn + - path: utils|_test + linters: + - wrapcheck linters: enable-all: true disable: - # deprecated - deadcode - maligned - varcheck @@ -27,38 +40,37 @@ linters: - rowserrcheck - sqlclosecheck - wastedassign - # skip: dubious benefit - - gochecknoglobals # Checks that no global variables exist. - - exhaustruct # Checks if all structure fields are initialized. - - lll # Reports long lines. + # skip + - gochecknoglobals # check that no global variables exist + - nilnil # Checks that there is no simultaneous return of nil error and an invalid value. + - gomnd # An analyzer to detect magic numbers. + - goerr113 # Golang linter to check the errors handling expressions + - exhaustruct # Checks if all structure fields are initialized + - lll # Reports long lines - godox # Tool for detection of FIXME, TODO and other comment keywords - wsl # Whitespace Linter - Forces you to use empty lines! - - tagliatelle # Checks the struct tags. - - nonamedreturns # Reports all named returns. - - wrapcheck # Checks that errors returned from external packages are wrapped. - - ireturn # Accept Interfaces, Return Concrete Types. - - tagalign # Checks that struct tags are well aligned - - depguard # Checks if package imports are in a list of acceptable packages - - musttag # Enforce field tags in (un)marshaled structs. - - dupl # Tool for code clone detection. - - cyclop # Checks function and package cyclomatic complexity. - forcetypeassert # finds forced type assertions - - goerr113 # Golang linter to check the errors handling expressions + # complexity: need to refactor + - cyclop - funlen + - gocognit + - gocyclo + - maintidx - nestif - # tests - - testpackage # Makes you use a separate _test package. - - paralleltest # Detects missing usage of t.Parallel() method in your Go test. + - dupl + - depguard linters-settings: nlreturn: - block-size: 5 + block-size: 10 gci: sections: - standard # Standard section: captures all standard packages. - default # Default section: contains all imports that could not be matched to another section type. - prefix(github.com/Edge-Center) errcheck: - ignore: github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema:ForceNew|Set,fmt + ignore: github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema:ForceNew|Set,fmt:.*,io:Close,io:WriteString + nakedret: + max-func-lines: 40 varnamelen: min-name-length: 1 diff --git a/.goreleaser.yml b/.goreleaser.yml index 531b6911..f51b9e6e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,13 +1,60 @@ +# Visit https://goreleaser.com for documentation on how to customize this +# behavior. before: hooks: + # this is just an example and not a requirement for provider building/publishing - go mod tidy builds: - - skip: true + - env: + # goreleaser does not work with CGO, it could also complicate + # usage by users in CI/CD systems like Terraform Cloud where + # they are unable to install libraries. + - CGO_ENABLED=0 + mod_timestamp: '{{ .CommitTimestamp }}' + flags: + - -trimpath + ldflags: + - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' + goos: + - freebsd + - windows + - linux + - darwin + goarch: + - amd64 + - '386' + - arm + - 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: + # if you are using this in a GitHub action or some other automated pipeline, you + # need to pass the batch flag to indicate its not interactive. + - "--batch" + - "--local-user" + - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key + - "--output" + - "${signature}" + - "--detach-sign" + - "${artifact}" +release: + extra_files: + - glob: 'terraform-registry-manifest.json' + name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' + # If you want to manually examine the release before its live, uncomment this line: + # draft: true changelog: - skip: true + skip: true \ No newline at end of file diff --git a/Makefile b/Makefile index 3f85df4c..371b6376 100644 --- a/Makefile +++ b/Makefile @@ -1,47 +1,98 @@ -PROJECT_DIR=$(shell pwd) -GOFMT_FILES?=$$(find . -name '*.go' |grep -v vendor) -BIN_DIR=$(PROJECT_DIR)/bin +# ENVS +ifeq ($(OS),Windows_NT) + PROJECT_DIR = $(shell cd) + OS := windows + ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) + ARCH := amd64 + endif + ifeq ($(PROCESSOR_ARCHITECTURE),x86) + ARCH := 386 + endif +else + PROJECT_DIR = $(shell pwd) + OS := $(shell uname | tr '[:upper:]' '[:lower:]') + ARCH := $(shell uname -m) +endif +BIN_DIR = $(PROJECT_DIR)/bin +TEST_DIR = $(PROJECT_DIR)/edgecenter/test +ENV_TESTS_FILE = $(TEST_DIR)/.env # BINARY -BINARY_NAME=terraform-provider-edgecenter -TAG_PREFIX="v" -TAG=$(shell git describe --tags) -VERSION=$(shell git describe --tags $(LAST_TAG_COMMIT) | sed "s/^$(TAG_PREFIX)//") -PLUGIN_PATH=~/.terraform.d/plugins/local.edgecenter.ru/repo/edgecenter/$(VERSION)/$(OS)_$(ARCH) +BINARY_NAME = terraform-provider-edgecenter +TAG_PREFIX = "v" +TAG = $(shell git describe --tags) +VERSION = $(shell git describe --tags $(LAST_TAG_COMMIT) | sed "s/^$(TAG_PREFIX)//") +PLUGIN_PATH = ~/.terraform.d/plugins/local.edgecenter.ru/repo/edgecenter/$(VERSION)/$(OS)_$(ARCH) -.PHONY: tidy tidy: go mod tidy -.PHONY: build -build: fmtcheck +# BUILD +build: tidy mkdir -p $(PLUGIN_PATH) go build -o $(PLUGIN_PATH)/$(BINARY_NAME)_v$(VERSION) go build -o bin/$(BINARY_NAME) -.PHONY: lint -lint: - @go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - @golangci-lint run -v ./... +build_debug: tidy + mkdir -p $(PLUGIN_PATH) + go build -o $(PLUGIN_PATH)/$(BINARY_NAME)_v$(VERSION) -gcflags '-N -l' + go build -o bin/$(BINARY_NAME) -gcflags '-N -l' + +# CHECKS +err_check: + @sh -c "'$(PROJECT_DIR)/scripts/errcheck.sh'" + +linters: + @test -f $(BIN_DIR)/golangci-lint || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.54.2 + @$(BIN_DIR)/golangci-lint run + +linters_docker: # for windows + docker run --rm -v $(PROJECT_DIR):/app -w /app golangci/golangci-lint:v1.54.2 golangci-lint run -v + +# TESTS +envs_reader: + go install github.com/joho/godotenv/cmd/godotenv@latest + +test_cloud_data_source: envs_reader + godotenv -f $(ENV_TESTS_FILE) go test $(TEST_DIR) -tags cloud_data_source -short -timeout=20m -.PHONY: test -test: - go test -v -timeout=2m +test_cloud_resource: envs_reader + godotenv -f $(ENV_TESTS_FILE) go test $(TEST_DIR) -tags cloud_resource -short -timeout=20m -fmt: - gofmt -s -w $(GOFMT_FILES) +test_not_cloud: envs_reader + godotenv -f $(ENV_TESTS_FILE) go test $(TEST_DIR) -tags dns storage cdn -v -timeout=5m -.PHONY: fmtcheck -fmtcheck: - @sh -c "'$(PROJECT_DIR)/scripts/gofmtcheck.sh'" +# local test run (need to export VAULT_TOKEN env) +install_jq: + if test "$(OS)" = "linux"; then \ + curl -L -o $(BIN_DIR)/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64; \ + else \ + curl -L -o $(BIN_DIR)/jq https://github.com/stedolan/jq/releases/download/jq-1.6/jq-osx-amd64; \ + fi + chmod +x $(BIN_DIR)/jq + +install_vault: + curl -L -o vault.zip https://releases.hashicorp.com/vault/1.13.3/vault_1.13.3_$(OS)_$(ARCH).zip + unzip vault.zip && rm -f vault.zip && chmod +x vault + mv vault $(BIN_DIR)/ + +download_env_file: envs_reader + godotenv -f $(ENV_TESTS_FILE) $(BIN_DIR)/vault login -method=token $(VAULT_TOKEN) + godotenv -f $(ENV_TESTS_FILE) $(BIN_DIR)/vault kv get -format=json --field data /CLOUD/terraform | $(BIN_DIR)/jq -r 'to_entries|map("\(.key)=\(.value)")|.[]' >> $(ENV_TESTS_FILE) + +test_local_data_source: envs_reader + godotenv -f .local.env go test $(TEST_DIR) -tags cloud_data_source -short -timeout=5m -v + +test_local_resource: envs_reader + godotenv -f .local.env go test $(TEST_DIR) -tags cloud_resource -short -timeout=10m -v # DOCS -.PHONY: docs_fmt docs_fmt: terraform fmt -recursive ./examples/ -.PHONY: docs docs: docs_fmt go get github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.16 make tidy - tfplugindocs --tf-version=1.6.5 --provider-name=edgecenter + tfplugindocs --tf-version=1.5.0 --provider-name=edgecenter + +.PHONY: tidy build build_debug err_check linters linters_docker envs_reader test_cloud_data_source test_cloud_resource test_not_cloud install_jq install_vault download_env_file test_local_data_source test_local_resource docs_fmt docs diff --git a/README.md b/README.md index e6315c30..fc9380db 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,150 @@ -# !!! v2 Beta, not production ready !!! -EdgeCenter Terraform Provider -================== +Terraform EdgeCenter Provider +------------------------------ + +EdgeCenter +==================================================================================== + +- [![Gitter chat](https://badges.gitter.im/hashicorp-terraform/Lobby.png)](https://gitter.im/hashicorp-terraform/Lobby) +- Mailing list: [Google Groups](http://groups.google.com/group/terraform-tool) + +Requirements +------------ + +- [Terraform](https://www.terraform.io/downloads.html) 0.13.x +- [Go](https://golang.org/doc/install) 1.19 (to build the provider plugin) + +Latest provider +------------ +- [edge-center provider](https://registry.terraform.io/providers/Edge-Center/edgecenter/latest) + +Building the provider +--------------------- +```sh +$ mkdir -p $GOPATH/src/github.com/terraform-providers +$ cd $GOPATH/src/github.com/terraform-providers +$ git clone https://github.com/Edge-Center/terraform-provider-edgecenter.git +$ cd $GOPATH/src/github.com/terraform-providers/terraform-provider-edgecenter +$ make build +``` + +### Override Terraform provider + +To override terraform provider for development goals you do next steps: + +create Terraform configuration file +```shell +$ touch ~/.terraformrc +``` + +point provider to development path +```shell +provider_installation { + + dev_overrides { + "local.edgecenter.ru/repo/edgecenter" = "//terraform-provider-edgecenter/bin" + } + + # For all other providers, install them directly from their origin provider + # registries as normal. If you omit this, Terraform will _only_ use + # the dev_overrides block, and so no other providers will be available. + direct {} +} +``` + +add `local.edgecenter.ru/repo/edgecenter` to .tf configuration file +```shell +terraform { + required_version = ">= 0.13.0" + + required_providers { + edgecenter = { + source = "local.edgecenter.ru/repo/edgecenter" + version = "{version_number}" # need to specify + } + } +} +``` + +Using the provider +------------------ +To use the provider, prepare configuration files based on examples + +```sh +$ cp ./examples/... . +$ terraform init +``` + +Testing +------------------ +Remote: Tests are run with provided secrets envs in the GitHub repository. +Local: execute the command `make test_local_data_source` and `make test_local_resource`. For this command to work, you need to: +* Create a `.local.env` file and fill it with the necessary envs. +* Run `make envs` to automatically fill the envs from Vault (don't forget to export `VAULT_TOKEN` to terminal). +* `make envs` requires the installation of `jq` and the `vault` binary. You can install them with the `make install_vault` and `make install_jq` commands, respectively. + +Docs generating +------------------ +To generate Terraform documentation, use the command `make docs`. This command uses the `terraform-plugin-docs` library to create provider documentation with examples and places it in the `docs` folder. These docs can be viewed on the provider registry page. + +Debugging +------------------ +There are two ways to debug the provider: +### VSCode debugging +1. Create a `launch.json` file: + * In the Run view, click `create a launch.json file`. + * Choose Go: Launch Package from the debug configuration drop-down menu. + * VS Code will create a `launch.json` file in a `.vscode` folder in your workspace. +2. Add a new configuration to `launch.json`: + * The `address` argument must be equal to the `source` field from your `provider.tf`. + ``` { + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Terraform Provider", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}", + "env": {}, + "args": [ + "-debug", + "-address=local.edgecenter.ru/repo/edgecenter" + ] + } + ] + } + ``` +3. Launch the debug mode: `Run > Start Debugging (F5)`. +4. Copy the `TF_REATTACH_PROVIDERS` env from the console and export it to the terminal as follows: + ```shell + export TF_REATTACH_PROVIDERS='{"local.edgecenter.ru/repo/edgecenter":{...' + ``` +5. Set a breakpoint in your code and apply the Terraform config: `terraform apply`. +6. Debugging. + +### using delve +1. Install the Delve library - [installation](https://github.com/go-delve/delve/tree/master/Documentation/installation) +2. Build binary without optimization or use `make build_debug` + ```shell + go build -o bin/$(BINARY_NAME) -gcflags '-N -l' + ``` +3. Open the first terminal: + * Run the binary with the debug option: + ```shell + dlv exec bin/terraform-provider-edgecenter -- -debug + ``` + * Set a breakpoint for the create function with a resource that you want to debug, e.g, + ```shell + break resourceFloatingIPCreate + ``` + * `continue` + * Copy `TF_REATTACH_PROVIDERS` with its value from output +4. Open the second terminal: + * Export `TF_REATTACH_PROVIDERS`: + ```shell + export TF_REATTACH_PROVIDERS='{"local.edgecenter.ru/repo/edgecenter":{...' + ``` + * Launch ```terraform apply``` + * Debug with the `continue` command in the first terminal via `delve` + +Thank You diff --git a/docs/data-sources/floatingip.md b/docs/data-sources/floatingip.md index 572a5342..91ae492b 100644 --- a/docs/data-sources/floatingip.md +++ b/docs/data-sources/floatingip.md @@ -15,26 +15,26 @@ allowing it to have a static public IP address. The floating IP can be re-associ ## Example Usage ```terraform -# Example 1 -data "edgecenter_floatingip" "fip1" { - region_id = var.region_id - project_id = var.project_id - floating_ip_address = "10.10.0.1" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "fip1" { - value = data.edgecenter_floatingip.fip1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_floatingip" "fip2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "fip2" { - value = data.edgecenter_floatingip.fip2 +data "edgecenter_floatingip" "ip" { + floating_ip_address = "10.100.179.172" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_floatingip.ip } ``` @@ -43,28 +43,28 @@ output "fip2" { ### Required -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `floating_ip_address` (String) The floating IP address assigned to the resource. It must be a valid IP address. ### Optional -- `floating_ip_address` (String) floating IP address assigned to the resource, must be a valid IP address -- `id` (String) floating IP uuid +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"}. +- `port_id` (String) The ID (uuid) of the network port that the floating IP is associated with. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `fixed_ip_address` (String) fixed IP address -- `instance` (Map of String) instance that the floating IP is attached to -- `loadbalancer` (Map of String) load balancer that the floating IP is attached to -- `metadata` (List of Object) metadata in detailed format (see [below for nested schema](#nestedatt--metadata)) -- `port_id` (String) network port uuid that the floating IP is associated with -- `region` (String) name of the region -- `router_id` (String) ID of the router -- `status` (String) current status ('DOWN' or 'ACTIVE') of the floating IP resource -- `subnet_id` (String) ID of the subnet - - -### Nested Schema for `metadata` +- `fixed_ip_address` (String) The fixed (reserved) IP address that is associated with the floating IP. +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `router_id` (String) The ID (uuid) of the router that the floating IP is associated with. +- `status` (String) The current status of the floating IP resource. Can be 'DOWN' or 'ACTIVE'. + + +### Nested Schema for `metadata_read_only` Read-Only: diff --git a/docs/data-sources/image.md b/docs/data-sources/image.md new file mode 100644 index 00000000..f2d62cfb --- /dev/null +++ b/docs/data-sources/image.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_image Data Source - edgecenter" +subcategory: "" +description: |- + A cloud image is a pre-configured virtual machine template that you can use to create new instances. +--- + +# edgecenter_image (Data Source) + +A cloud image is a pre-configured virtual machine template that you can use to create new instances. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_image" "ubuntu" { + name = "ubuntu-20.04" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_image.ubuntu +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the image. Use 'os-version', for example 'ubuntu-20.04'. + +### Optional + +- `is_baremetal` (Boolean) Set to true if need to get the baremetal image. +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"}. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `description` (String) A detailed description of the image. +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `min_disk` (Number) Minimum disk space (in GB) required to launch an instance using this image. +- `min_ram` (Number) Minimum VM RAM (in MB) required to launch an instance using this image. +- `os_distro` (String) The distribution of the OS present in the image, e.g. Debian, CentOS, Ubuntu etc. +- `os_version` (String) The version of the OS present in the image. e.g. 19.04 (for Ubuntu) or 9.4 for Debian. + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + + diff --git a/docs/data-sources/instance.md b/docs/data-sources/instance.md index f4dc273b..88214e8c 100644 --- a/docs/data-sources/instance.md +++ b/docs/data-sources/instance.md @@ -3,36 +3,36 @@ page_title: "edgecenter_instance Data Source - edgecenter" subcategory: "" description: |- - A cloud instance is a virtual machine in a cloud environment + A cloud instance is a virtual machine in a cloud environment. Could be used with baremetal also. --- # edgecenter_instance (Data Source) -A cloud instance is a virtual machine in a cloud environment +A cloud instance is a virtual machine in a cloud environment. Could be used with baremetal also. ## Example Usage ```terraform -# Example 1 -data "edgecenter_instance" "instance1" { - region_id = var.region_id - project_id = var.project_id - name = "test-instance" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "instance1" { - value = data.edgecenter_instance.instance1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_instance" "instance2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "instance2" { - value = data.edgecenter_instance.instance2 +data "edgecenter_instance" "vm" { + name = "test-vm" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_instance.vm } ``` @@ -41,28 +41,45 @@ output "instance2" { ### Required -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `name` (String) The name of the instance. ### Optional -- `id` (String) instance uuid -- `name` (String) instance name. this parameter is not unique, if there is more than one instance with the same name, -then the first one will be used. it is recommended to use "id" +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `addresses` (List of Map of String) network addresses associated with the instance -- `flavor` (Map of String) information about the flavor -- `interface` (List of Object) network interfaces attached to the instance (see [below for nested schema](#nestedatt--interface)) -- `keypair_name` (String) name of the keypair -- `metadata_detailed` (List of Object) metadata in detailed format (see [below for nested schema](#nestedatt--metadata_detailed)) -- `region` (String) name of the region -- `security_groups` (List of String) list of security groups names -- `server_group_id` (String) UUID of the anti-affinity or affinity server group (placement groups) -- `status` (String) current status of the instance resource -- `vm_state` (String) state of the virtual machine -- `volumes` (List of String) list of volumes ID's +- `addresses` (List of Object) A list of network addresses associated with the instance, for example "pub_net": [...]. (see [below for nested schema](#nestedatt--addresses)) +- `flavor` (Map of String) A map defining the flavor of the instance, for example, {"flavor_name": "g1-standard-2-4", "ram": 4096, ...}. +- `flavor_id` (String) The ID of the flavor to be used for the instance, determining its compute and memory, for example 'g1-standard-2-4'. +- `id` (String) The ID of this resource. +- `interface` (List of Object) A list defining the network interfaces to be attached to the instance. (see [below for nested schema](#nestedatt--interface)) +- `metadata` (List of Object) (see [below for nested schema](#nestedatt--metadata)) +- `security_group` (List of Object) A list of firewall configurations applied to the instance, defined by their id and name. (see [below for nested schema](#nestedatt--security_group)) +- `status` (String) The current status of the instance. This is computed automatically and can be used to track the instance's state. +- `vm_state` (String) The current virtual machine state of the instance, +allowing you to start or stop the VM. Possible values are stopped and active. +- `volume` (Set of Object) A set defining the volumes to be attached to the instance. (see [below for nested schema](#nestedatt--volume)) + + +### Nested Schema for `addresses` + +Read-Only: + +- `net` (List of Object) (see [below for nested schema](#nestedobjatt--addresses--net)) + + +### Nested Schema for `addresses.net` + +Read-Only: + +- `addr` (String) +- `type` (String) + + ### Nested Schema for `interface` @@ -75,13 +92,29 @@ Read-Only: - `subnet_id` (String) - -### Nested Schema for `metadata_detailed` + +### Nested Schema for `metadata` Read-Only: - `key` (String) -- `read_only` (Boolean) - `value` (String) + +### Nested Schema for `security_group` + +Read-Only: + +- `name` (String) + + + +### Nested Schema for `volume` + +Read-Only: + +- `delete_on_termination` (Boolean) +- `volume_id` (String) + + diff --git a/docs/data-sources/k8s.md b/docs/data-sources/k8s.md new file mode 100644 index 00000000..52a56586 --- /dev/null +++ b/docs/data-sources/k8s.md @@ -0,0 +1,86 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_k8s Data Source - edgecenter" +subcategory: "" +description: |- + Represent k8s cluster with one default pool. +--- + +# edgecenter_k8s (Data Source) + +Represent k8s cluster with one default pool. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_k8s" "cluster" { + project_id = 1 + region_id = 1 + cluster_id = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" +} +``` + + +## Schema + +### Required + +- `cluster_id` (String) The uuid of the Kubernetes cluster. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `api_address` (String) API endpoint address for the Kubernetes cluster. +- `auto_healing_enabled` (Boolean) Indicates whether auto-healing is enabled for the Kubernetes cluster. +- `certificate_authority_data` (String) The certificate_authority_data field from the Kubernetes cluster config. +- `cluster_template_id` (String) Template identifier from which the Kubernetes cluster was instantiated. +- `container_version` (String) The container runtime version used in the Kubernetes cluster. +- `created_at` (String) The timestamp when the Kubernetes cluster was created. +- `discovery_url` (String) URL used for node discovery within the Kubernetes cluster. +- `faults` (Map of String) +- `fixed_network` (String) Fixed network (uuid) associated with the Kubernetes cluster. +- `fixed_subnet` (String) Subnet (uuid) associated with the fixed network. +- `health_status` (String) Overall health status of the Kubernetes cluster. +- `health_status_reason` (Map of String) +- `id` (String) The ID of this resource. +- `keypair` (String) +- `master_addresses` (List of String) List of IP addresses for master nodes in the Kubernetes cluster. +- `master_flavor_id` (String) Identifier for the master node flavor in the Kubernetes cluster. +- `master_lb_floating_ip_enabled` (Boolean) Flag indicating if the master LoadBalancer should have a floating IP. +- `name` (String) The name of the Kubernetes cluster. +- `node_addresses` (List of String) List of IP addresses for worker nodes in the Kubernetes cluster. +- `node_count` (Number) Total number of nodes in the Kubernetes cluster. +- `pool` (List of Object) Configuration details of the node pool in the Kubernetes cluster. (see [below for nested schema](#nestedatt--pool)) +- `status` (String) The current status of the Kubernetes cluster. +- `status_reason` (String) The reason for the current status of the Kubernetes cluster, if ERROR. +- `updated_at` (String) The timestamp when the Kubernetes cluster was updated. +- `user_id` (String) User identifier associated with the Kubernetes cluster. +- `version` (String) The version of the Kubernetes cluster. + + +### Nested Schema for `pool` + +Read-Only: + +- `created_at` (String) +- `docker_volume_size` (Number) +- `docker_volume_type` (String) +- `flavor_id` (String) +- `max_node_count` (Number) +- `min_node_count` (Number) +- `name` (String) +- `node_count` (Number) +- `stack_id` (String) +- `uuid` (String) + + diff --git a/docs/data-sources/k8s_client_config.md b/docs/data-sources/k8s_client_config.md new file mode 100644 index 00000000..8ddf6775 --- /dev/null +++ b/docs/data-sources/k8s_client_config.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_k8s_client_config Data Source - edgecenter" +subcategory: "" +description: |- + Represent k8s cluster with one default pool. +--- + +# edgecenter_k8s_client_config (Data Source) + +Represent k8s cluster with one default pool. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_k8s_client_config" "cfg" { + project_id = 1 + region_id = 1 + cluster_id = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" +} +``` + + +## Schema + +### Required + +- `cluster_id` (String) The uuid of the Kubernetes cluster. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `client_certificate_data` (String) The client_certificate_data field from k8s config. +- `client_key_data` (String) The client_key_data field from k8s config. +- `id` (String) The ID of this resource. + + diff --git a/docs/data-sources/k8s_pool.md b/docs/data-sources/k8s_pool.md new file mode 100644 index 00000000..fa0f111b --- /dev/null +++ b/docs/data-sources/k8s_pool.md @@ -0,0 +1,59 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_k8s_pool Data Source - edgecenter" +subcategory: "" +description: |- + Represent k8s cluster's pool. +--- + +# edgecenter_k8s_pool (Data Source) + +Represent k8s cluster's pool. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_k8s_pool" "pool" { + project_id = 1 + region_id = 1 + cluster_id = "6bf878c1-1ce4-47c3-a39b-6b5f1d79bf25" + pool_id = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" +} +``` + + +## Schema + +### Required + +- `cluster_id` (String) The uuid of the Kubernetes cluster this pool belongs to. +- `pool_id` (String) The uuid of the Kubernetes pool within the cluster. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `created_at` (String) The timestamp when the Kubernetes pool was created. +- `docker_volume_size` (Number) The size of the volume used for Docker containers, in gigabytes. +- `docker_volume_type` (String) The type of volume used for the Docker containers. Available values are 'standard', 'ssd_hiiops', 'cold', and 'ultra'. +- `flavor_id` (String) The identifier of the flavor used for nodes in this pool. +- `id` (String) The ID of this resource. +- `is_default` (Boolean) Indicates whether this pool is the default pool in the cluster. +- `max_node_count` (Number) The maximum number of nodes the pool can scale to. +- `min_node_count` (Number) The minimum number of nodes in the pool. +- `name` (String) The name of the Kubernetes pool. +- `node_addresses` (List of String) A list of IP addresses of nodes within the pool. +- `node_count` (Number) The current number of nodes in the pool. +- `node_names` (List of String) A list of names of nodes within the pool. +- `stack_id` (String) The identifier of the underlying infrastructure stack used by this pool. + + diff --git a/docs/data-sources/lblistener.md b/docs/data-sources/lblistener.md index 3b481e00..b58b1fe2 100644 --- a/docs/data-sources/lblistener.md +++ b/docs/data-sources/lblistener.md @@ -3,44 +3,37 @@ page_title: "edgecenter_lblistener Data Source - edgecenter" subcategory: "" description: |- - A listener is a process that checks for connection requests using the protocol and port that you configure. + --- # edgecenter_lblistener (Data Source) -A listener is a process that checks for connection requests using the protocol and port that you configure. + ## Example Usage ```terraform -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 1 -data "edgecenter_lbpool" "pool1" { - region_id = var.region_id - project_id = var.project_id - name = "test-lbpool" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_project" "pr" { + name = "test" } -output "pool1" { - value = data.edgecenter_lbpool.pool1 +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -# Example 2 -data "edgecenter_lbpool" "pool2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_lblistener" "l" { + name = "test-listener" + loadbalancer_id = "59b2eabc-c0a8-4545-8081-979bd963c6ab" //optional + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } -output "pool2" { - value = data.edgecenter_lbpool.pool2 +output "view" { + value = data.edgecenter_lblistener.l } ``` @@ -49,25 +42,24 @@ output "pool2" { ### Required -- `loadbalancer_id` (String) ID of the load balancer -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `name` (String) The name of the load balancer listener. ### Optional -- `id` (String) listener uuid -- `name` (String) listener name. this parameter is not unique, if there is more than one listener with the same name, -then the first one will be used. it is recommended to use "id" +- `loadbalancer_id` (String) The uuid for the load balancer. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `allowed_cidrs` (List of String) allowed CIDRs for listener -- `insert_headers` (Map of String) dictionary of additional header insertion into the HTTP headers. only used with the HTTP and TERMINATED_HTTPS protocols -- `operating_status` (String) operating status of the listener -- `pool_count` (Number) number of pools -- `protocol` (String) protocol of the load balancer -- `protocol_port` (Number) protocol port number of the resource -- `provisioning_status` (String) lifecycle status of the listener -- `secret_id` (String) ID of the secret where PKCS12 file is stored for the TERMINATED_HTTPS load balancer +- `allowed_cidrs` (List of String) The allowed CIDRs for listener. +- `id` (String) The ID of this resource. +- `operating_status` (String) The current operational status of the load balancer. +- `pool_count` (Number) Number of pools associated with the load balancer. +- `protocol` (String) Available values is 'HTTP', 'HTTPS', 'TCP', 'UDP' +- `protocol_port` (Number) The port on which the protocol is bound. +- `provisioning_status` (String) The current provisioning status of the load balancer. diff --git a/docs/data-sources/lbpool.md b/docs/data-sources/lbpool.md index b2a8040a..3c0bc40d 100644 --- a/docs/data-sources/lbpool.md +++ b/docs/data-sources/lbpool.md @@ -3,44 +3,36 @@ page_title: "edgecenter_lbpool Data Source - edgecenter" subcategory: "" description: |- - A pool is a list of virtual machines to which the listener will redirect incoming traffic + --- # edgecenter_lbpool (Data Source) -A pool is a list of virtual machines to which the listener will redirect incoming traffic + ## Example Usage ```terraform -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 1 -data "edgecenter_lblistener" "listener1" { - region_id = var.region_id - project_id = var.project_id - name = "test-lblistener" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_project" "pr" { + name = "test" } -output "listener1" { - value = data.edgecenter_lblistener.listener1 +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -# Example 2 -data "edgecenter_lblistener" "listener2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_lbpool" "pool" { + name = "test-pool" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } -output "listener2" { - value = data.edgecenter_lblistener.listener2 +output "view" { + value = data.edgecenter_lbpool.pool } ``` @@ -49,34 +41,29 @@ output "listener2" { ### Required -- `loadbalancer_id` (String) ID of the load balancer -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `name` (String) The name of the load balancer pool. ### Optional -- `id` (String) lb pool uuid -- `name` (String) lb pool name. this parameter is not unique, if there is more than one lb pool with the same name, -then the first one will be used. it is recommended to use "id" +- `listener_id` (String) The uuid for the load balancer listener. +- `loadbalancer_id` (String) The uuid for the load balancer. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `healthmonitor` (List of Object) configuration for health checks to test the health and state of the backend members. -it determines how the load balancer identifies whether the backend members are healthy or unhealthy (see [below for nested schema](#nestedatt--healthmonitor)) -- `lb_algorithm` (String) algorithm of the load balancer -- `listener_id` (String) ID of the load balancer listener -- `member` (List of Object) members of the Pool (see [below for nested schema](#nestedatt--member)) -- `operating_status` (String) operating status of the pool -- `protocol` (String) protocol of the load balancer -- `provisioning_status` (String) lifecycle status of the pool -- `session_persistence` (List of Object) configuration that enables the load balancer to bind a user's session to a specific backend member. -this ensures that all requests from the user during the session are sent to the same member. (see [below for nested schema](#nestedatt--session_persistence)) -- `timeout_client_data` (Number) timeout for the frontend client inactivity (in milliseconds) -- `timeout_member_connect` (Number) timeout for the backend member connection (in milliseconds) -- `timeout_member_data` (Number) timeout for the backend member inactivity (in milliseconds) - - -### Nested Schema for `healthmonitor` +- `health_monitor` (List of Object) Configuration for health checks to test the health and state of the backend members. +It determines how the load balancer identifies whether the backend members are healthy or unhealthy. (see [below for nested schema](#nestedatt--health_monitor)) +- `id` (String) The ID of this resource. +- `lb_algorithm` (String) Available values is 'ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP', 'SOURCE_IP_PORT' +- `protocol` (String) Available values is 'HTTP' (currently work, other do not work on ed-8), 'HTTPS', 'TCP', 'UDP' +- `session_persistence` (List of Object) Configuration that enables the load balancer to bind a user's session to a specific backend member. +This ensures that all requests from the user during the session are sent to the same member. (see [below for nested schema](#nestedatt--session_persistence)) + + +### Nested Schema for `health_monitor` Read-Only: @@ -91,21 +78,6 @@ Read-Only: - `url_path` (String) - -### Nested Schema for `member` - -Read-Only: - -- `address` (String) -- `admin_state_up` (Boolean) -- `id` (String) -- `instance_id` (String) -- `operating_status` (String) -- `protocol_port` (Number) -- `subnet_id` (String) -- `weight` (Number) - - ### Nested Schema for `session_persistence` diff --git a/docs/data-sources/loadbalancer.md b/docs/data-sources/loadbalancer.md index 8d185333..dc6a47f9 100644 --- a/docs/data-sources/loadbalancer.md +++ b/docs/data-sources/loadbalancer.md @@ -3,38 +3,36 @@ page_title: "edgecenter_loadbalancer Data Source - edgecenter" subcategory: "" description: |- - A loadbalancer is a software service that distributes incoming network traffic - (e.g., web traffic, application requests) across multiple servers or resources. + --- # edgecenter_loadbalancer (Data Source) -A loadbalancer is a software service that distributes incoming network traffic -(e.g., web traffic, application requests) across multiple servers or resources. + ## Example Usage ```terraform -# Example 1 -data "edgecenter_loadbalancer" "lb1" { - region_id = var.region_id - project_id = var.project_id - name = "test-loadbalancer" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" } -output "lb1" { - value = data.edgecenter_loadbalancer.lb1 +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -# Example 2 -data "edgecenter_loadbalancer" "lb2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_loadbalancer" "lb" { + name = "test-lb" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } -output "lb2" { - value = data.edgecenter_loadbalancer.lb2 +output "view" { + value = data.edgecenter_loadbalancer.lb } ``` @@ -43,30 +41,38 @@ output "lb2" { ### Required -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `name` (String) The name of the router. ### Optional -- `id` (String) loadbalancer uuid -- `name` (String) loadbalancer name. this parameter is not unique, if there is more than one loadbalancer with the same name, -then the first one will be used. it is recommended to use "id" +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"} +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `flavor` (Map of String) information about the flavor -- `floating_ip` (Map of String) information about the assigned floating IP -- `metadata_detailed` (List of Object) metadata in detailed format (see [below for nested schema](#nestedatt--metadata_detailed)) -- `operating_status` (String) operating status of the load balancer -- `provisioning_status` (String) lifecycle status of the load balancer -- `region` (String) name of the region -- `vip_address` (String) loadbalancer IP address -- `vip_network_id` (String) ID of the network that the subnet belongs to. the port will be plugged in this network -- `vip_port_id` (String) IP port of the load balancer -- `vrrp_ips` (List of String) list of VRRP IP addresses - - -### Nested Schema for `metadata_detailed` +- `id` (String) The ID of this resource. +- `listener` (List of Object) (see [below for nested schema](#nestedatt--listener)) +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `vip_address` (String) +- `vip_port_id` (String) + + +### Nested Schema for `listener` + +Read-Only: + +- `id` (String) +- `name` (String) +- `protocol` (String) +- `protocol_port` (Number) + + + +### Nested Schema for `metadata_read_only` Read-Only: diff --git a/docs/data-sources/loadbalancerv2.md b/docs/data-sources/loadbalancerv2.md new file mode 100644 index 00000000..80488550 --- /dev/null +++ b/docs/data-sources/loadbalancerv2.md @@ -0,0 +1,71 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_loadbalancerv2 Data Source - edgecenter" +subcategory: "" +description: |- + +--- + +# edgecenter_loadbalancerv2 (Data Source) + + + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_loadbalancerv2" "lb" { + name = "test-lb" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_loadbalancerv2.lb +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the load balancer. + +### Optional + +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"} +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `vip_address` (String) Load balancer IP address +- `vip_port_id` (String) Attached reserved IP. + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + + diff --git a/docs/data-sources/network.md b/docs/data-sources/network.md new file mode 100644 index 00000000..29133f02 --- /dev/null +++ b/docs/data-sources/network.md @@ -0,0 +1,100 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_network Data Source - edgecenter" +subcategory: "" +description: |- + Represent network. A network is a software-defined network in a cloud computing infrastructure +--- + +# edgecenter_network (Data Source) + +Represent network. A network is a software-defined network in a cloud computing infrastructure + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_network" "tnw" { + name = "example" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_network.tnw +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the network. + +### Optional + +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"} +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `shared_with_subnets` (Boolean) Get shared networks with details of subnets. + +### Read-Only + +- `external` (Boolean) +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `mtu` (Number) Maximum Transmission Unit (MTU) for the network. It determines the maximum packet size that can be transmitted without fragmentation. +- `shared` (Boolean) +- `subnets` (Block List) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedblock--subnets)) +- `type` (String) 'vlan' or 'vxlan' network type is allowed. Default value is 'vxlan' + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + + + +### Nested Schema for `subnets` + +Read-Only: + +- `available_ips` (Number) The number of available IPs in the subnet. +- `cidr` (String) Represents the IP address range of the subnet. +- `dns_nameservers` (List of String) List of DNS name servers for the subnet. +- `enable_dhcp` (Boolean) Enable DHCP for this subnet. If true, DHCP will be used to assign IP addresses to instances within this subnet. +- `gateway_ip` (String) The IP address of the gateway for this subnet. +- `has_router` (Boolean) Indicates whether the subnet has a router attached to it. +- `host_routes` (List of Object) List of additional routes to be added to instances that are part of this subnet. (see [below for nested schema](#nestedatt--subnets--host_routes)) +- `id` (String) The ID of the subnet. +- `name` (String) The name of the subnet. +- `total_ips` (Number) The total number of IPs in the subnet. + + +### Nested Schema for `subnets.host_routes` + +Read-Only: + +- `destination` (String) +- `nexthop` (String) + + diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md new file mode 100644 index 00000000..e267efb9 --- /dev/null +++ b/docs/data-sources/project.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_project Data Source - edgecenter" +subcategory: "" +description: |- + Represent project data +--- + +# edgecenter_project (Data Source) + +Represent project data + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} +``` + + +## Schema + +### Required + +- `name` (String) Displayed project name + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/data-sources/region.md b/docs/data-sources/region.md new file mode 100644 index 00000000..0351bff3 --- /dev/null +++ b/docs/data-sources/region.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_region Data Source - edgecenter" +subcategory: "" +description: |- + Represent region data +--- + +# edgecenter_region (Data Source) + +Represent region data + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} +``` + + +## Schema + +### Required + +- `name` (String) Displayed region name + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/data-sources/reservedfixedip.md b/docs/data-sources/reservedfixedip.md new file mode 100644 index 00000000..9edae5a4 --- /dev/null +++ b/docs/data-sources/reservedfixedip.md @@ -0,0 +1,71 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_reservedfixedip Data Source - edgecenter" +subcategory: "" +description: |- + Represent reserved ips +--- + +# edgecenter_reservedfixedip (Data Source) + +Represent reserved ips + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_reservedfixedip" "ip" { + fixed_ip_address = "192.168.0.66" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_reservedfixedip.ip +} +``` + + +## Schema + +### Required + +- `fixed_ip_address` (String) The IP address that is associated with the reserved IP. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `allowed_address_pairs` (List of Object) Group of IP addresses that share the current IP as VIP. (see [below for nested schema](#nestedatt--allowed_address_pairs)) +- `id` (String) The ID of this resource. +- `is_vip` (Boolean) Flag to determine if the reserved fixed IP should be treated as a Virtual IP (VIP). +- `network_id` (String) ID of the network to which the reserved fixed IP is associated. +- `port_id` (String) ID of the port_id underlying the reserved fixed IP +- `status` (String) The current status of the reserved fixed IP. +- `subnet_id` (String) ID of the subnet from which the fixed IP should be reserved. + + +### Nested Schema for `allowed_address_pairs` + +Read-Only: + +- `ip_address` (String) +- `mac_address` (String) + + diff --git a/docs/data-sources/router.md b/docs/data-sources/router.md new file mode 100644 index 00000000..43a0572e --- /dev/null +++ b/docs/data-sources/router.md @@ -0,0 +1,101 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_router Data Source - edgecenter" +subcategory: "" +description: |- + +--- + +# edgecenter_router (Data Source) + + + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_router" "tr" { + name = "test_router" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_router.tr +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the load router. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `external_gateway_info` (List of Object) Information related to the external gateway. (see [below for nested schema](#nestedatt--external_gateway_info)) +- `id` (String) The ID of this resource. +- `interfaces` (List of Object) Set of interfaces associated with the router. (see [below for nested schema](#nestedatt--interfaces)) +- `routes` (List of Object) List of static routes to be applied to the router. (see [below for nested schema](#nestedatt--routes)) +- `status` (String) The current status of the router resource. + + +### Nested Schema for `external_gateway_info` + +Read-Only: + +- `enable_snat` (Boolean) +- `external_fixed_ips` (List of Object) (see [below for nested schema](#nestedobjatt--external_gateway_info--external_fixed_ips)) +- `network_id` (String) + + +### Nested Schema for `external_gateway_info.external_fixed_ips` + +Read-Only: + +- `ip_address` (String) +- `subnet_id` (String) + + + + +### Nested Schema for `interfaces` + +Read-Only: + +- `ip_address` (String) +- `mac_address` (String) +- `network_id` (String) +- `port_id` (String) +- `subnet_id` (String) +- `type` (String) + + + +### Nested Schema for `routes` + +Read-Only: + +- `destination` (String) +- `nexthop` (String) + + diff --git a/docs/data-sources/secret.md b/docs/data-sources/secret.md new file mode 100644 index 00000000..4ee24d2e --- /dev/null +++ b/docs/data-sources/secret.md @@ -0,0 +1,64 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_secret Data Source - edgecenter" +subcategory: "" +description: |- + Represent secret +--- + +# edgecenter_secret (Data Source) + +Represent secret + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_secret" "lb_https" { + name = "lb_https" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_secret.lb_https +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the secret. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `algorithm` (String) The encryption algorithm used for the secret. +- `bit_length` (Number) The bit length of the encryption algorithm. +- `content_types` (Map of String) The content types associated with the secret's payload. +- `created` (String) Datetime when the secret was created. The format is 2025-12-28T19:14:44.180394 +- `expiration` (String) Datetime when the secret will expire. The format is 2025-12-28T19:14:44.180394 +- `id` (String) The ID of this resource. +- `mode` (String) The mode of the encryption algorithm. +- `status` (String) The current status of the secret. + + diff --git a/docs/data-sources/securitygroup.md b/docs/data-sources/securitygroup.md new file mode 100644 index 00000000..75610362 --- /dev/null +++ b/docs/data-sources/securitygroup.md @@ -0,0 +1,88 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_securitygroup Data Source - edgecenter" +subcategory: "" +description: |- + Represent SecurityGroups(Firewall) +--- + +# edgecenter_securitygroup (Data Source) + +Represent SecurityGroups(Firewall) + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_securitygroup" "default" { + name = "default" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_securitygroup.default +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the security group. + +### Optional + +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"} +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `description` (String) A detailed description of the security group. +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `security_group_rules` (Set of Object) Firewall rules control what inbound(ingress) and outbound(egress) traffic is allowed to enter or leave a Instance. At least one 'egress' rule should be set (see [below for nested schema](#nestedatt--security_group_rules)) + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + + + +### Nested Schema for `security_group_rules` + +Read-Only: + +- `created_at` (String) +- `description` (String) +- `direction` (String) +- `ethertype` (String) +- `id` (String) +- `port_range_max` (Number) +- `port_range_min` (Number) +- `protocol` (String) +- `remote_ip_prefix` (String) +- `updated_at` (String) + + diff --git a/docs/data-sources/servergroup.md b/docs/data-sources/servergroup.md new file mode 100644 index 00000000..4cbc4651 --- /dev/null +++ b/docs/data-sources/servergroup.md @@ -0,0 +1,67 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_servergroup Data Source - edgecenter" +subcategory: "" +description: |- + Represent server group data +--- + +# edgecenter_servergroup (Data Source) + +Represent server group data + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_servergroup" "default" { + name = "default" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_servergroup.default +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the server group. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `id` (String) The ID of this resource. +- `instances` (List of Object) Instances in this server group (see [below for nested schema](#nestedatt--instances)) +- `policy` (String) Server group policy. Available value is 'affinity', 'anti-affinity' + + +### Nested Schema for `instances` + +Read-Only: + +- `instance_id` (String) +- `instance_name` (String) + + diff --git a/docs/data-sources/storage_s3.md b/docs/data-sources/storage_s3.md new file mode 100644 index 00000000..8e726c4a --- /dev/null +++ b/docs/data-sources/storage_s3.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_storage_s3 Data Source - edgecenter" +subcategory: "" +description: |- + Represent s3 storage resource. https://storage.edgecenter.ru/storage/list +--- + +# edgecenter_storage_s3 (Data Source) + +Represent s3 storage resource. https://storage.edgecenter.ru/storage/list + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_storage_s3" "example_s3" { + name = "example" +} +``` + + +## Schema + +### Optional + +- `name` (String) A name of new storage resource. +- `storage_id` (Number) An id of new storage resource. + +### Read-Only + +- `client_id` (Number) An client id of new storage resource. +- `generated_endpoint` (String) A s3 entry point for new storage resource. +- `generated_http_endpoint` (String) A http s3 entry point for new storage resource. +- `generated_s3_endpoint` (String) A s3 endpoint for new storage resource. +- `id` (String) The ID of this resource. +- `location` (String) A location of new storage resource. One of (s-dt2) + + diff --git a/docs/data-sources/storage_s3_bucket.md b/docs/data-sources/storage_s3_bucket.md new file mode 100644 index 00000000..08223c02 --- /dev/null +++ b/docs/data-sources/storage_s3_bucket.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_storage_s3_bucket Data Source - edgecenter" +subcategory: "" +description: |- + Represent storage s3 bucket resource. +--- + +# edgecenter_storage_s3_bucket (Data Source) + +Represent storage s3 bucket resource. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_storage_s3_bucket" "example_s3_bucket" { + storage_id = 1 + name = "example1bucket2name" +} +``` + + +## Schema + +### Required + +- `name` (String) A name of storage bucket resource. +- `storage_id` (Number) An id of existing storage resource. + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/data-sources/subnet.md b/docs/data-sources/subnet.md new file mode 100644 index 00000000..b7444fe3 --- /dev/null +++ b/docs/data-sources/subnet.md @@ -0,0 +1,85 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_subnet Data Source - edgecenter" +subcategory: "" +description: |- + +--- + +# edgecenter_subnet (Data Source) + + + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_subnet" "tsn" { + name = "subtest" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_subnet.tsn +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the subnet. + +### Optional + +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"} +- `network_id` (String) The ID of the network to which this subnet belongs. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `cidr` (String) Represents the IP address range of the subnet. +- `connect_to_network_router` (Boolean) True if the network's router should get a gateway in this subnet. Must be explicitly 'false' when gateway_ip is null. +- `dns_nameservers` (List of String) List of DNS name servers for the subnet. +- `enable_dhcp` (Boolean) Enable DHCP for this subnet. If true, DHCP will be used to assign IP addresses to instances within this subnet. +- `gateway_ip` (String) The IP address of the gateway for this subnet. +- `host_routes` (List of Object) List of additional routes to be added to instances that are part of this subnet. (see [below for nested schema](#nestedatt--host_routes)) +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) + + +### Nested Schema for `host_routes` + +Read-Only: + +- `destination` (String) +- `nexthop` (String) + + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + + diff --git a/docs/data-sources/volume.md b/docs/data-sources/volume.md index e604b9f0..aa99d73f 100644 --- a/docs/data-sources/volume.md +++ b/docs/data-sources/volume.md @@ -15,26 +15,26 @@ Volumes can be attached to a virtual machine and manipulated like a physical har ## Example Usage ```terraform -# Example 1 -data "edgecenter_volume" "volume1" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "volume1" { - value = data.edgecenter_volume.volume1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_volume" "volume2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "volume2" { - value = data.edgecenter_volume.volume2 +data "edgecenter_volume" "tv" { + name = "test-hd" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_volume.tv } ``` @@ -43,38 +43,26 @@ output "volume2" { ### Required -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `name` (String) The name of the volume. ### Optional -- `id` (String) volume uuid -- `name` (String) volume name. this parameter is not unique, if there is more than one volume with the same name, -then the first one will be used. it is recommended to use "id" +- `metadata_k` (String) Filtration query opts (only key). +- `metadata_kv` (Map of String) Filtration query opts, for example, {offset = "10", limit = "10"} +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `attachments` (List of Object) the attachment list (see [below for nested schema](#nestedatt--attachments)) -- `bootable` (Boolean) the bootable boolean flag -- `limiter_stats` (Map of Number) the QoS parameters of this volume -- `metadata` (List of Object) metadata in detailed format (see [below for nested schema](#nestedatt--metadata)) -- `region` (String) name of the region -- `size` (Number) size of the volume, specified in gigabytes (GB) -- `status` (String) current status of the volume resource -- `volume_type` (String) volume type - - -### Nested Schema for `attachments` - -Read-Only: - -- `attachment_id` (String) -- `server_id` (String) -- `volume_id` (String) - +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `size` (Number) The size of the volume, specified in gigabytes (GB). +- `type_name` (String) The type of volume to create. Valid values are 'ssd_hiiops', 'standard', 'cold', and 'ultra'. Defaults to 'standard'. - -### Nested Schema for `metadata` + +### Nested Schema for `metadata_read_only` Read-Only: diff --git a/docs/index.md b/docs/index.md index 5046cc7a..1c0540ca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,14 +17,221 @@ terraform { required_providers { edgecenter = { source = "Edge-Center/edgecenter" - version = ">=1.0.0" + version = ">= 0.1.12" } } } provider "edgecenter" { - api_key = "251$d3361.............1b35f26d8" - edgecenter_cloud_api = "https://api.edgecenter.ru/cloud" + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_keypair" "kp" { + project_id = 1 + public_key = "your oub key" + sshkey_name = "testkey" +} + +resource "edgecenter_network" "network" { + name = "network_example" + mtu = 1450 + type = "vxlan" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet" { + name = "subnet_example" + cidr = "192.168.10.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = ["8.8.4.4", "1.1.1.1"] + + host_routes { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" + } + + gateway_ip = "192.168.10.1" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet2" { + name = "subnet2_example" + cidr = "192.168.20.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = ["8.8.4.4", "1.1.1.1"] + + host_routes { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" + } + + gateway_ip = "192.168.20.1" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "first_volume" { + name = "boot volume" + type_name = "ssd_hiiops" + size = 6 + image_id = "f4ce3d30-e29c-4cfd-811f-46f383b6081f" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "second_volume" { + name = "second volume" + type_name = "ssd_hiiops" + image_id = "f4ce3d30-e29c-4cfd-811f-46f383b6081f" + size = 6 + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "third_volume" { + name = "third volume" + type_name = "ssd_hiiops" + size = 6 + region_id = 1 + project_id = 1 +} + +resource "edgecenter_instance" "instance" { + flavor_id = "g1-standard-2-4" + name = "test" + keypair_name = edgecenter_keypair.kp.sshkey_name + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.first_volume.id + boot_index = 0 + } + + interface { + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet.id + } + + interface { + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet2.id + } + + security_group { + id = "66988147-f1b9-43b2-aaef-dee6d009b5b7" + name = "default" + } + + metadata { + key = "some_key" + value = "some_data" + } + + configuration { + key = "some_key" + value = "some_data" + } + + region_id = 1 + project_id = 1 +} + +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test1" + flavor = "lb1-1-2" + listener { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } +} + +resource "edgecenter_lbpool" "pl" { + project_id = 1 + region_id = 1 + name = "test_pool1" + protocol = "HTTP" + lb_algorithm = "LEAST_CONNECTIONS" + loadbalancer_id = edgecenter_loadbalancer.lb.id + listener_id = edgecenter_loadbalancer.lb.listener.0.id + health_monitor { + type = "PING" + delay = 60 + max_retries = 5 + timeout = 10 + } + session_persistence { + type = "APP_COOKIE" + cookie_name = "test_new_cookie" + } +} + +resource "edgecenter_lbmember" "lbm" { + project_id = 1 + region_id = 1 + pool_id = edgecenter_lbpool.pl.id + instance_id = edgecenter_instance.instance.id + address = tolist(edgecenter_instance.instance.interface).0.ip_address + protocol_port = 8081 + weight = 5 +} + +resource "edgecenter_instance" "instance2" { + flavor_id = "g1-standard-2-4" + name = "test2" + keypair_name = edgecenter_keypair.kp.sshkey_name + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.second_volume.id + boot_index = 0 + } + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.third_volume.id + boot_index = 1 + } + + interface { + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet.id + } + + security_group { + id = "66988147-f1b9-43b2-aaef-dee6d009b5b7" + name = "default" + } + + metadata { + key = "some_key" + value = "some_data" + } + + configuration { + key = "some_key" + value = "some_data" + } + + region_id = 1 + project_id = 1 +} + +resource "edgecenter_lbmember" "lbm2" { + project_id = 1 + region_id = 1 + pool_id = edgecenter_lbpool.pl.id + instance_id = edgecenter_instance.instance2.id + address = tolist(edgecenter_instance.instance2.interface).0.ip_address + protocol_port = 8081 + weight = 5 } ``` @@ -33,5 +240,16 @@ provider "edgecenter" { ### Optional -- `api_key` (String, Sensitive) A permanent [API-token](https://support.edgecenter.ru/knowledge_base/item/257788) +- `api_endpoint` (String) A single API endpoint for all products. Will be used when specific product API url is not defined. +- `edgecenter_api` (String, Deprecated) Region API +- `edgecenter_cdn_api` (String) CDN API (define only if you want to override CDN API endpoint) +- `edgecenter_client_id` (String) Client id - `edgecenter_cloud_api` (String) Region API (define only if you want to override Region API endpoint) +- `edgecenter_dns_api` (String) DNS API (define only if you want to override DNS API endpoint) +- `edgecenter_platform` (String, Deprecated) Platform URL is used for generate JWT +- `edgecenter_platform_api` (String) Platform URL is used for generate JWT (define only if you want to override Platform API endpoint) +- `edgecenter_storage_api` (String) Storage API (define only if you want to override Storage API endpoint) +- `ignore_creds_auth_error` (Boolean, Deprecated) Should be set to true when you are gonna to use storage resource with permanent API-token only. +- `password` (String, Deprecated) +- `permanent_api_token` (String, Sensitive) A permanent [API-token](https://support.edgecenter.ru/knowledge_base/item/257788) +- `user_name` (String, Deprecated) diff --git a/docs/resources/baremetal.md b/docs/resources/baremetal.md new file mode 100644 index 00000000..e0aca31f --- /dev/null +++ b/docs/resources/baremetal.md @@ -0,0 +1,138 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_baremetal Resource - edgecenter" +subcategory: "" +description: |- + Represent baremetal instance +--- + +# edgecenter_baremetal (Resource) + +Represent baremetal instance + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_baremetal" "bm" { + name = "test bm instance" + region_id = 1 + project_id = 1 + flavor_id = "bm1-infrastructure-small" + image_id = "1ee7ccee-5003-48c9-8ae0-d96063af75b2" // your image id + + //additional interface, available type is 'subnet' or 'external' + // interface { + // type = "subnet" + // network_id = "9c7867fb-f404-4a2d-8bb5-24acf2fccaf1" //your network_id + // subnet_id = "b68ea6e2-c2b6-4a8d-95eb-7194d12a2156" // your subnet_id + // } + + // interface { + // type = "external" + // is_parent = "true" // if is_parent = true interface cant be detached, and always connected first + // } + + keypair_name = "test" // your keypair name +} +``` + + +## Schema + +### Required + +- `flavor_id` (String) +- `interface` (Block List, Min: 1) (see [below for nested schema](#nestedblock--interface)) + +### Optional + +- `app_config` (Map of String) +- `apptemplate_id` (String) +- `image_id` (String) +- `keypair_name` (String) +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata` (Block List, Deprecated) (see [below for nested schema](#nestedblock--metadata)) +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `name` (String) The name of the baremetal instance. +- `name_template` (String) +- `name_templates` (List of String, Deprecated) +- `password` (String) +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `user_data` (String) +- `username` (String) + +### Read-Only + +- `addresses` (List of Object) (see [below for nested schema](#nestedatt--addresses)) +- `flavor` (Map of String) +- `id` (String) The ID of this resource. +- `status` (String) +- `vm_state` (String) + + +### Nested Schema for `interface` + +Required: + +- `type` (String) Available value is 'subnet', 'any_subnet', 'external', 'reserved_fixed_ip' + +Optional: + +- `existing_fip_id` (String) +- `fip_source` (String) +- `ip_address` (String) +- `is_parent` (Boolean) If not set will be calculated after creation. Trunk interface always attached first. Can't detach interface if is_parent true. Fields affect only on creation +- `network_id` (String) required if type is 'subnet' or 'any_subnet' +- `order` (Number) Order of attaching interface. Trunk interface always attached first, fields affect only on creation +- `port_id` (String) required if type is 'reserved_fixed_ip' +- `subnet_id` (String) required if type is 'subnet' + + + +### Nested Schema for `metadata` + +Required: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) + + + +### Nested Schema for `addresses` + +Read-Only: + +- `net` (List of Object) (see [below for nested schema](#nestedobjatt--addresses--net)) + + +### Nested Schema for `addresses.net` + +Read-Only: + +- `addr` (String) +- `type` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_baremetal.instance1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/cdn_origingroup.md b/docs/resources/cdn_origingroup.md new file mode 100644 index 00000000..17853aed --- /dev/null +++ b/docs/resources/cdn_origingroup.md @@ -0,0 +1,64 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_cdn_origingroup Resource - edgecenter" +subcategory: "" +description: |- + Represent origin group +--- + +# edgecenter_cdn_origingroup (Resource) + +Represent origin group + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_cdn_origingroup" "origin_group_1" { + name = "origin_group_1" + use_next = true + origin { + source = "example.com" + enabled = true + } + origin { + source = "mirror.example.com" + enabled = true + backup = true + } +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the origin group +- `origin` (Block Set, Min: 1) Contains information about all IP address or Domain names of your origin and the port if custom (see [below for nested schema](#nestedblock--origin)) +- `use_next` (Boolean) This options have two possible values: true — The option is active. In case the origin responds with 4XX or 5XX codes, use the next origin from the list. false — The option is disabled. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `origin` + +Required: + +- `source` (String) IP address or Domain name of your origin and the port if custom + +Optional: + +- `backup` (Boolean) true — The option is active. The origin will not be used until one of active origins become unavailable. false — The option is disabled. +- `enabled` (Boolean) The setting allows to enable or disable an Origin source in the Origins group + +Read-Only: + +- `id` (Number) The ID of this resource. + + diff --git a/docs/resources/cdn_resource.md b/docs/resources/cdn_resource.md new file mode 100644 index 00000000..9ca6c652 --- /dev/null +++ b/docs/resources/cdn_resource.md @@ -0,0 +1,622 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_cdn_resource Resource - edgecenter" +subcategory: "" +description: |- + Represent CDN resource +--- + +# edgecenter_cdn_resource (Resource) + +Represent CDN resource + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + + +resource "edgecenter_cdn_resource" "cdn_example_com" { + cname = "cdn.example.com" + origin_group = edgecenter_cdn_origingroup.origin_group_1.id + origin_protocol = "MATCH" + secondary_hostnames = ["cdn2.example.com"] + + options { + edge_cache_settings { + default = "8d" + } + browser_cache_settings { + value = "1d" + } + redirect_http_to_https { + value = true + } + gzip_on { + value = true + } + cors { + value = [ + "*" + ] + } + rewrite { + body = "/(.*) /$1" + } + webp { + jpg_quality = 55 + png_quality = 66 + } + + tls_versions { + enabled = true + value = [ + "TLSv1.2", + ] + } + } +} +``` + + +## Schema + +### Required + +- `cname` (String) A CNAME that will be used to deliver content though a CDN. If you update this field new resource will be created. + +### Optional + +- `active` (Boolean) The setting allows to enable or disable a CDN Resource +- `description` (String) Custom client description of the resource. +- `issue_le_cert` (Boolean) Generate LE certificate. +- `options` (Block List, Max: 1) Each option in CDN resource settings. Each option added to CDN resource settings should have the following mandatory request fields: enabled, value. (see [below for nested schema](#nestedblock--options)) +- `origin` (String) A domain name or IP of your origin source. Specify a port if custom. You can use either 'origin' parameter or 'originGroup' in the resource definition. +- `origin_group` (Number) ID of the Origins Group. Use one of your Origins Group or create a new one. You can use either 'origin' parameter or 'originGroup' in the resource definition. +- `origin_protocol` (String) This option defines the protocol that will be used by CDN servers to request content from an origin source. If not specified, we will use HTTP to connect to an origin server. Possible values are: HTTPS, HTTP, MATCH. +- `secondary_hostnames` (Set of String) List of additional CNAMEs. +- `ssl_automated` (Boolean) generate LE certificate automatically. +- `ssl_data` (Number) Specify the SSL Certificate ID which should be used for the CDN Resource. +- `ssl_enabled` (Boolean) Use HTTPS protocol for content delivery. + +### Read-Only + +- `id` (String) The ID of this resource. +- `status` (String) Status of a CDN resource content availability. Possible values are: Active, Suspended, Processed. + + +### Nested Schema for `options` + +Optional: + +- `allowed_http_methods` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--allowed_http_methods)) +- `brotli_compression` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--brotli_compression)) +- `browser_cache_settings` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--browser_cache_settings)) +- `cache_http_headers` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--cache_http_headers)) +- `cors` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--cors)) +- `country_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--country_acl)) +- `disable_proxy_force_ranges` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--disable_proxy_force_ranges)) +- `edge_cache_settings` (Block List, Max: 1) The cache expiration time for CDN servers. (see [below for nested schema](#nestedblock--options--edge_cache_settings)) +- `fetch_compressed` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--fetch_compressed)) +- `follow_origin_redirect` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--follow_origin_redirect)) +- `force_return` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--force_return)) +- `forward_host_header` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--forward_host_header)) +- `gzip_on` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--gzip_on)) +- `host_header` (Block List, Max: 1) Specify the Host header that CDN servers use when request content from an origin server. Your server must be able to process requests with the chosen header. If the option is in NULL state Host Header value is taken from the CNAME field. (see [below for nested schema](#nestedblock--options--host_header)) +- `http3_enabled` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--http3_enabled)) +- `ignore_cookie` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--ignore_cookie)) +- `ignore_query_string` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--ignore_query_string)) +- `image_stack` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--image_stack)) +- `ip_address_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--ip_address_acl)) +- `limit_bandwidth` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--limit_bandwidth)) +- `proxy_cache_methods_set` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--proxy_cache_methods_set)) +- `query_params_blacklist` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--query_params_blacklist)) +- `query_params_whitelist` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--query_params_whitelist)) +- `redirect_http_to_https` (Block List, Max: 1) Sets redirect from HTTP protocol to HTTPS for all resource requests. (see [below for nested schema](#nestedblock--options--redirect_http_to_https)) +- `redirect_https_to_http` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--redirect_https_to_http)) +- `referrer_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--referrer_acl)) +- `response_headers_hiding_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--response_headers_hiding_policy)) +- `rewrite` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--rewrite)) +- `secure_key` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--secure_key)) +- `slice` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--slice)) +- `sni` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--sni)) +- `stale` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--stale)) +- `static_headers` (Block List, Max: 1) Option has been deprecated. Use - static_response_headers. (see [below for nested schema](#nestedblock--options--static_headers)) +- `static_request_headers` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--static_request_headers)) +- `static_response_headers` (Block List, Max: 1) Specify custom HTTP Headers that a CDN server adds to a response. (see [below for nested schema](#nestedblock--options--static_response_headers)) +- `tls_versions` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--tls_versions)) +- `use_default_le_chain` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--use_default_le_chain)) +- `user_agent_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--user_agent_acl)) +- `websockets` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--websockets)) + + +### Nested Schema for `options.allowed_http_methods` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.brotli_compression` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.browser_cache_settings` + +Optional: + +- `enabled` (Boolean) +- `value` (String) + + + +### Nested Schema for `options.cache_http_headers` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.cors` + +Required: + +- `value` (Set of String) + +Optional: + +- `always` (Boolean) +- `enabled` (Boolean) + + + +### Nested Schema for `options.country_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.disable_proxy_force_ranges` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.edge_cache_settings` + +Optional: + +- `custom_values` (Map of String) Caching time for a response with specific codes. These settings have a higher priority than the value field. Response code ('304', '404' for example). Use 'any' to specify caching time for all response codes. Caching time in seconds ('0s', '600s' for example). Use '0s' to disable caching for a specific response code. +- `default` (String) Content will be cached according to origin cache settings. The value applies for a response with codes 200, 201, 204, 206, 301, 302, 303, 304, 307, 308 if an origin server does not have caching HTTP headers. Responses with other codes will not be cached. +- `enabled` (Boolean) +- `value` (String) Caching time for a response with codes 200, 206, 301, 302. Responses with codes 4xx, 5xx will not be cached. Use '0s' disable to caching. Use custom_values field to specify a custom caching time for a response with specific codes. + + + +### Nested Schema for `options.fetch_compressed` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.follow_origin_redirect` + +Required: + +- `codes` (Set of Number) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.force_return` + +Required: + +- `code` (Number) + +Optional: + +- `body` (String) +- `enabled` (Boolean) + + + +### Nested Schema for `options.forward_host_header` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.gzip_on` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.host_header` + +Required: + +- `value` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.http3_enabled` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.ignore_cookie` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.ignore_query_string` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.image_stack` + +Required: + +- `quality` (Number) + +Optional: + +- `avif_enabled` (Boolean) +- `enabled` (Boolean) +- `png_lossless` (Boolean) +- `webp_enabled` (Boolean) + + + +### Nested Schema for `options.ip_address_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.limit_bandwidth` + +Required: + +- `limit_type` (String) + +Optional: + +- `buffer` (Number) +- `enabled` (Boolean) +- `speed` (Number) + + + +### Nested Schema for `options.proxy_cache_methods_set` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.query_params_blacklist` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.query_params_whitelist` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.redirect_http_to_https` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.redirect_https_to_http` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.referrer_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) Possible values: allow, deny. + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.response_headers_hiding_policy` + +Required: + +- `excepted` (Set of String) +- `mode` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.rewrite` + +Required: + +- `body` (String) + +Optional: + +- `enabled` (Boolean) +- `flag` (String) + + + +### Nested Schema for `options.secure_key` + +Required: + +- `key` (String) +- `type` (Number) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.slice` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.sni` + +Optional: + +- `custom_hostname` (String) Required to set custom hostname in case sni-type='custom' +- `enabled` (Boolean) +- `sni_type` (String) Available values 'dynamic' or 'custom' + + + +### Nested Schema for `options.stale` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.static_headers` + +Required: + +- `value` (Map of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.static_request_headers` + +Required: + +- `value` (Map of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.static_response_headers` + +Required: + +- `value` (Block List, Min: 1) (see [below for nested schema](#nestedblock--options--static_response_headers--value)) + +Optional: + +- `enabled` (Boolean) + + +### Nested Schema for `options.static_response_headers.value` + +Required: + +- `name` (String) +- `value` (Set of String) + +Optional: + +- `always` (Boolean) + + + + +### Nested Schema for `options.tls_versions` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.use_default_le_chain` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.user_agent_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.websockets` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + diff --git a/docs/resources/cdn_rule.md b/docs/resources/cdn_rule.md new file mode 100644 index 00000000..3c1422bc --- /dev/null +++ b/docs/resources/cdn_rule.md @@ -0,0 +1,611 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_cdn_rule Resource - edgecenter" +subcategory: "" +description: |- + Represent cdn resource rule +--- + +# edgecenter_cdn_rule (Resource) + +Represent cdn resource rule + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_cdn_rule" "cdn_example_com_rule_1" { + resource_id = edgecenter_cdn_resource.cdn_example_com.id + name = "All PNG images" + rule = "/folder/images/*.png" + + options { + edge_cache_settings { + default = "14d" + } + browser_cache_settings { + value = "14d" + } + redirect_http_to_https { + value = true + } + gzip_on { + value = true + } + cors { + value = [ + "*" + ] + } + rewrite { + body = "/(.*) /$1" + } + webp { + jpg_quality = 55 + png_quality = 66 + } + ignore_query_string { + value = true + } + } +} + +resource "edgecenter_cdn_rule" "cdn_example_com_rule_2" { + resource_id = edgecenter_cdn_resource.cdn_example_com.id + name = "All JS scripts" + rule = "/folder/images/*.js" + origin_protocol = "HTTP" + + options { + redirect_http_to_https { + enabled = false + value = true + } + gzip_on { + enabled = false + value = true + } + query_params_whitelist { + value = [ + "abc", + ] + } + } +} + +resource "edgecenter_cdn_origingroup" "origin_group_1" { + name = "origin_group_1" + use_next = true + origin { + source = "example.com" + enabled = true + } +} + +resource "edgecenter_cdn_resource" "cdn_example_com" { + cname = "cdn.example.com" + origin_group = edgecenter_cdn_origingroup.origin_group_1.id + origin_protocol = "MATCH" + secondary_hostnames = ["cdn2.example.com"] +} +``` + + +## Schema + +### Required + +- `name` (String) Rule name +- `resource_id` (Number) +- `rule` (String) A pattern that defines when the rule is triggered. By default, we add a leading forward slash to any rule pattern. Specify a pattern without a forward slash. + +### Optional + +- `active` (Boolean) Shows if the location is enabled. +- `options` (Block List, Max: 1) Each option in CDN resource settings. Each option added to CDN resource settings should have the following mandatory request fields: enabled, value. (see [below for nested schema](#nestedblock--options)) +- `origin_group` (Number) ID of the Origins Group. Use one of your Origins Group or create a new one. You can use either 'origin' parameter or 'originGroup' in the resource definition. +- `origin_protocol` (String) This option defines the protocol that will be used by CDN servers to request content from an origin source. If not specified, it will be inherit from resource. Possible values are: HTTPS, HTTP, MATCH. +- `weight` (Number) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `options` + +Optional: + +- `allowed_http_methods` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--allowed_http_methods)) +- `brotli_compression` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--brotli_compression)) +- `browser_cache_settings` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--browser_cache_settings)) +- `cache_http_headers` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--cache_http_headers)) +- `cors` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--cors)) +- `country_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--country_acl)) +- `disable_proxy_force_ranges` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--disable_proxy_force_ranges)) +- `edge_cache_settings` (Block List, Max: 1) The cache expiration time for CDN servers. (see [below for nested schema](#nestedblock--options--edge_cache_settings)) +- `fetch_compressed` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--fetch_compressed)) +- `follow_origin_redirect` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--follow_origin_redirect)) +- `force_return` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--force_return)) +- `forward_host_header` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--forward_host_header)) +- `gzip_on` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--gzip_on)) +- `host_header` (Block List, Max: 1) Specify the Host header that CDN servers use when request content from an origin server. Your server must be able to process requests with the chosen header. If the option is in NULL state Host Header value is taken from the CNAME field. (see [below for nested schema](#nestedblock--options--host_header)) +- `ignore_cookie` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--ignore_cookie)) +- `ignore_query_string` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--ignore_query_string)) +- `image_stack` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--image_stack)) +- `ip_address_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--ip_address_acl)) +- `limit_bandwidth` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--limit_bandwidth)) +- `proxy_cache_methods_set` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--proxy_cache_methods_set)) +- `query_params_blacklist` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--query_params_blacklist)) +- `query_params_whitelist` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--query_params_whitelist)) +- `redirect_http_to_https` (Block List, Max: 1) Sets redirect from HTTP protocol to HTTPS for all resource requests. (see [below for nested schema](#nestedblock--options--redirect_http_to_https)) +- `redirect_https_to_http` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--redirect_https_to_http)) +- `referrer_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--referrer_acl)) +- `response_headers_hiding_policy` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--response_headers_hiding_policy)) +- `rewrite` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--rewrite)) +- `secure_key` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--secure_key)) +- `slice` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--slice)) +- `sni` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--sni)) +- `stale` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--stale)) +- `static_headers` (Block List, Max: 1) Option has been deprecated. Use - static_response_headers. (see [below for nested schema](#nestedblock--options--static_headers)) +- `static_request_headers` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--static_request_headers)) +- `static_response_headers` (Block List, Max: 1) Specify custom HTTP Headers that a CDN server adds to a response. (see [below for nested schema](#nestedblock--options--static_response_headers)) +- `user_agent_acl` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--user_agent_acl)) +- `websockets` (Block List, Max: 1) (see [below for nested schema](#nestedblock--options--websockets)) + + +### Nested Schema for `options.allowed_http_methods` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.brotli_compression` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.browser_cache_settings` + +Optional: + +- `enabled` (Boolean) +- `value` (String) + + + +### Nested Schema for `options.cache_http_headers` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.cors` + +Required: + +- `value` (Set of String) + +Optional: + +- `always` (Boolean) +- `enabled` (Boolean) + + + +### Nested Schema for `options.country_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.disable_proxy_force_ranges` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.edge_cache_settings` + +Optional: + +- `custom_values` (Map of String) Caching time for a response with specific codes. These settings have a higher priority than the value field. Response code ('304', '404' for example). Use 'any' to specify caching time for all response codes. Caching time in seconds ('0s', '600s' for example). Use '0s' to disable caching for a specific response code. +- `default` (String) Content will be cached according to origin cache settings. The value applies for a response with codes 200, 201, 204, 206, 301, 302, 303, 304, 307, 308 if an origin server does not have caching HTTP headers. Responses with other codes will not be cached. +- `enabled` (Boolean) +- `value` (String) Caching time for a response with codes 200, 206, 301, 302. Responses with codes 4xx, 5xx will not be cached. Use '0s' disable to caching. Use custom_values field to specify a custom caching time for a response with specific codes. + + + +### Nested Schema for `options.fetch_compressed` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.follow_origin_redirect` + +Required: + +- `codes` (Set of Number) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.force_return` + +Required: + +- `code` (Number) + +Optional: + +- `body` (String) +- `enabled` (Boolean) + + + +### Nested Schema for `options.forward_host_header` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.gzip_on` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.host_header` + +Required: + +- `value` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.ignore_cookie` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.ignore_query_string` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.image_stack` + +Required: + +- `quality` (Number) + +Optional: + +- `avif_enabled` (Boolean) +- `enabled` (Boolean) +- `png_lossless` (Boolean) +- `webp_enabled` (Boolean) + + + +### Nested Schema for `options.ip_address_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.limit_bandwidth` + +Required: + +- `limit_type` (String) + +Optional: + +- `buffer` (Number) +- `enabled` (Boolean) +- `speed` (Number) + + + +### Nested Schema for `options.proxy_cache_methods_set` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.query_params_blacklist` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.query_params_whitelist` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.redirect_http_to_https` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.redirect_https_to_http` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.referrer_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) Possible values: allow, deny. + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.response_headers_hiding_policy` + +Required: + +- `excepted` (Set of String) +- `mode` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.rewrite` + +Required: + +- `body` (String) + +Optional: + +- `enabled` (Boolean) +- `flag` (String) + + + +### Nested Schema for `options.secure_key` + +Required: + +- `key` (String) +- `type` (Number) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.slice` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.sni` + +Optional: + +- `custom_hostname` (String) Required to set custom hostname in case sni-type='custom' +- `enabled` (Boolean) +- `sni_type` (String) Available values 'dynamic' or 'custom' + + + +### Nested Schema for `options.stale` + +Required: + +- `value` (Set of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.static_headers` + +Required: + +- `value` (Map of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.static_request_headers` + +Required: + +- `value` (Map of String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.static_response_headers` + +Required: + +- `value` (Block List, Min: 1) (see [below for nested schema](#nestedblock--options--static_response_headers--value)) + +Optional: + +- `enabled` (Boolean) + + +### Nested Schema for `options.static_response_headers.value` + +Required: + +- `name` (String) +- `value` (Set of String) + +Optional: + +- `always` (Boolean) + + + + +### Nested Schema for `options.user_agent_acl` + +Required: + +- `excepted_values` (Set of String) +- `policy_type` (String) + +Optional: + +- `enabled` (Boolean) + + + +### Nested Schema for `options.websockets` + +Required: + +- `value` (Boolean) + +Optional: + +- `enabled` (Boolean) + + diff --git a/docs/resources/cdn_sslcert.md b/docs/resources/cdn_sslcert.md new file mode 100644 index 00000000..0b909e6e --- /dev/null +++ b/docs/resources/cdn_sslcert.md @@ -0,0 +1,52 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_cdn_sslcert Resource - edgecenter" +subcategory: "" +description: |- + +--- + +# edgecenter_cdn_sslcert (Resource) + + + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +variable "cert" { + type = string + sensitive = true +} + +variable "private_key" { + type = string + sensitive = true +} + +resource "edgecenter_cdn_sslcert" "cdnopt_cert" { + name = "Test cert for cdnopt_bookatest_by" + cert = var.cert + private_key = var.private_key +} +``` + + +## Schema + +### Required + +- `cert` (String, Sensitive) The public part of the SSL certificate. All chain of the SSL certificate should be added. +- `name` (String) Name of the SSL certificate. Must be unique. +- `private_key` (String, Sensitive) The private key of the SSL certificate. + +### Read-Only + +- `automated` (Boolean) The way SSL certificate was issued. +- `has_related_resources` (Boolean) It shows if the SSL certificate is used by a CDN resource. +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/dns_zone.md b/docs/resources/dns_zone.md new file mode 100644 index 00000000..f17f8f56 --- /dev/null +++ b/docs/resources/dns_zone.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_dns_zone Resource - edgecenter" +subcategory: "" +description: |- + Represent DNS zone resource. https://dns.edgecenter.ru/zones +--- + +# edgecenter_dns_zone (Resource) + +Represent DNS zone resource. https://dns.edgecenter.ru/zones + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_dns_zone" "example_zone" { + name = "example_zone.com" +} +``` + + +## Schema + +### Required + +- `name` (String) A name of DNS Zone resource. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Import is supported using the following syntax: + +```shell +# import using zone name format +terraform import edgecenter_dns_zone.example_zone example_zone.com +``` diff --git a/docs/resources/dns_zone_record.md b/docs/resources/dns_zone_record.md new file mode 100644 index 00000000..7517591e --- /dev/null +++ b/docs/resources/dns_zone_record.md @@ -0,0 +1,168 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_dns_zone_record Resource - edgecenter" +subcategory: "" +description: |- + Represent DNS Zone Record resource. https://dns.edgecenter.ru/zones +--- + +# edgecenter_dns_zone_record (Resource) + +Represent DNS Zone Record resource. https://dns.edgecenter.ru/zones + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +// +// example0: managing zone and records by TF using variables +// +variable "example_domain0" { + type = string + default = "examplezone.com" +} + +resource "edgecenter_dns_zone" "examplezone0" { + name = var.example_domain0 +} + +resource "edgecenter_dns_zone_record" "example_rrset0" { + zone = edgecenter_dns_zone.examplezone0.name + domain = edgecenter_dns_zone.examplezone0.name + type = "A" + ttl = 100 + + resource_record { + content = "127.0.0.100" + } + resource_record { + content = "127.0.0.200" + // enabled = false + } +} + +// +// example1: managing zone outside of TF +// +resource "edgecenter_dns_zone_record" "subdomain_examplezone" { + zone = "examplezone.com" + domain = "subdomain.examplezone.com" + type = "TXT" + ttl = 10 + + filter { + type = "geodistance" + limit = 1 + strict = true + } + + resource_record { + content = "1234" + enabled = true + + meta { + latlong = [52.367, 4.9041] + asn = [12345] + ip = ["1.1.1.1"] + notes = ["notes"] + continents = ["asia"] + countries = ["russia"] + default = true + } + } +} + +resource "edgecenter_dns_zone_record" "subdomain_examplezone_mx" { + zone = "examplezone.com" + domain = "subdomain.examplezone.com" + type = "MX" + ttl = 10 + + resource_record { + content = "10 mail.my.com." + enabled = true + } +} + +resource "edgecenter_dns_zone_record" "subdomain_examplezone_caa" { + zone = "examplezone.com" + domain = "subdomain.examplezone.com" + type = "CAA" + ttl = 10 + + resource_record { + content = "0 issue \"company.org; account=12345\"" + enabled = true + } +} +``` + + +## Schema + +### Required + +- `domain` (String) A domain of DNS Zone Record resource. +- `resource_record` (Block Set, Min: 1) An array of contents with meta of DNS Zone Record resource. (see [below for nested schema](#nestedblock--resource_record)) +- `type` (String) A type of DNS Zone Record resource. +- `zone` (String) A zone of DNS Zone Record resource. + +### Optional + +- `filter` (Block Set) (see [below for nested schema](#nestedblock--filter)) +- `ttl` (Number) A ttl of DNS Zone Record resource. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `resource_record` + +Required: + +- `content` (String) A content of DNS Zone Record resource. (TXT: 'anyString', MX: '50 mail.company.io.', CAA: '0 issue "company.org; account=12345"') + +Optional: + +- `enabled` (Boolean) Manage of public appearing of DNS Zone Record resource. +- `meta` (Block Set, Max: 1) (see [below for nested schema](#nestedblock--resource_record--meta)) + + +### Nested Schema for `resource_record.meta` + +Optional: + +- `asn` (List of Number) An asn meta (e.g. 12345) of DNS Zone Record resource. +- `continents` (List of String) Continents meta (e.g. Asia) of DNS Zone Record resource. +- `countries` (List of String) Countries meta (e.g. USA) of DNS Zone Record resource. +- `default` (Boolean) Fallback meta equals true marks records which are used as a default answer (when nothing was selected by specified meta fields). +- `ip` (List of String) An ip meta (e.g. 127.0.0.0) of DNS Zone Record resource. +- `latlong` (List of Number) A latlong meta (e.g. 27.988056, 86.925278) of DNS Zone Record resource. +- `notes` (List of String) A notes meta (e.g. Miami DC) of DNS Zone Record resource. + + + + +### Nested Schema for `filter` + +Required: + +- `type` (String) A DNS Zone Record filter option that describe a name of filter. + +Optional: + +- `limit` (Number) A DNS Zone Record filter option that describe how many records will be percolated. +- `strict` (Boolean) A DNS Zone Record filter option that describe possibility to return answers if no records were percolated through filter. + +## Import + +Import is supported using the following syntax: + +```shell +# import using zone:domain:type format +terraform import edgecenter_dns_zone_record.example_rrset0 example.com:domain.example.com:A +``` diff --git a/docs/resources/floatingip.md b/docs/resources/floatingip.md index 4d633fa0..70ec4093 100644 --- a/docs/resources/floatingip.md +++ b/docs/resources/floatingip.md @@ -15,38 +15,59 @@ allowing it to have a static public IP address. The floating IP can be re-associ ## Example Usage ```terraform -resource "edgecenter_floatingip" "fip" { - region_id = var.region_id - project_id = var.project_id - metadata = { - "key1" : "value1", - "key2" : "value2", +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_floatingip" "floating_ip" { + project_id = 1 + region_id = 1 + metadata_map = { + tag1 = "tag1_value" } + // fixed_ip_address = "192.168.10.39" // instance`s interface ip + // port_id = "5c992875-f653-4b7b-af5b-1dc3019e5ffa" //instance`s interface port_id } ``` ## Schema -### Required - -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region - ### Optional -- `fixed_ip_address` (String) in case the port has multiple IPs, a specific address can be selected using this field. -if unspecified, the first IP in the list of the port list is used. must be a valid IP address -- `metadata` (Map of String) map containing metadata, for example tags. -- `port_id` (String) network port uuid, if provided, the floating IP will be immediately attached to the specified port +- `fixed_ip_address` (String) The fixed (reserved) IP address that is associated with the floating IP. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `port_id` (String) The ID (uuid) of the network port that the floating IP is associated with. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. ### Read-Only -- `floating_ip_address` (String) floating IP address assigned to the resource +- `created_at` (String) The timestamp when the floating IP was created. +- `floating_ip_address` (String) The floating IP address assigned to the resource. - `id` (String) The ID of this resource. -- `region` (String) name of the region -- `router_id` (String) ID of the router -- `status` (String) current status ('DOWN' or 'ACTIVE') of the floating IP resource -- `subnet_id` (String) ID of the subnet +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `router_id` (String) The ID (uuid) of the router that the floating IP is associated with. +- `status` (String) The current status of the floating IP. Can be 'DOWN' or 'ACTIVE'. +- `updated_at` (String) The timestamp when the floating IP was updated. + + +### Nested Schema for `metadata_read_only` +Read-Only: +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_floatingip.fip1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/instance.md b/docs/resources/instance.md index 1ddd0dde..b7b4dc9a 100644 --- a/docs/resources/instance.md +++ b/docs/resources/instance.md @@ -3,67 +3,148 @@ page_title: "edgecenter_instance Resource - edgecenter" subcategory: "" description: |- - A cloud instance is a virtual machine in a cloud environment + A cloud instance is a virtual machine in a cloud environment. --- # edgecenter_instance (Resource) -A cloud instance is a virtual machine in a cloud environment +A cloud instance is a virtual machine in a cloud environment. ## Example Usage ```terraform -# Example 1 -resource "edgecenter_instance" "instance1" { - region_id = var.region_id - project_id = var.project_id - name = "test-instance" - flavor = "g1-standard-2-4" - keypair_name = "test-keypair" - server_group_id = "00000000-0000-0000-0000-000000000000" - security_groups = ["00000000-0000-0000-0000-000000000000"] - user_data = "#cloud-config\npassword: ваш пароль\nchpasswd: { expire: False }\nssh_pwauth: True" - - metadata = { - "key1" : "value1", - "key2" : "value2", +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_network" "network" { + name = "network_example" + mtu = 1450 + type = "vxlan" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet" { + name = "subnet_example" + cidr = "192.168.10.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = ["8.8.4.4", "1.1.1.1"] + + host_routes { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" } + gateway_ip = "192.168.10.1" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "first_volume" { + name = "boot volume" + type_name = "ssd_hiiops" + size = 5 + image_id = "f4ce3d30-e29c-4cfd-811f-46f383b6081f" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "second_volume" { + name = "second volume" + type_name = "ssd_hiiops" + size = 5 + region_id = 1 + project_id = 1 +} + +resource "edgecenter_instance" "instance" { + flavor_id = "g1-standard-2-4" + name = "test" + volume { - name = "system" - type_name = "ssd_hiiops" - size = 30 - source = "image" - image_id = "00000000-0000-0000-0000-000000000000" - attachment_tag = "tag" - boot_index = 0 - metadata = { - "tag" : "system" - } + source = "existing-volume" + volume_id = edgecenter_volume.first_volume.id + boot_index = 0 } - interface { - type = "any_subnet" - network_id = "00000000-0000-0000-0000-000000000000" - floating_ip_source = "new" + volume { + source = "existing-volume" + volume_id = edgecenter_volume.second_volume.id + boot_index = 1 } interface { - type = "any_subnet" - network_id = "00000000-0000-0000-0000-000000000000" - floating_ip_source = "existing" - floating_ip = "00000000-0000-0000-0000-000000000000" + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet.id + security_groups = ["d75db0b2-58f1-4a11-88c6-a932bb897310"] } - interface { - type = "subnet" - network_id = "00000000-0000-0000-0000-000000000000" - subnet_id = "00000000-0000-0000-0000-000000000000" + metadata_map = { + some_key = "some_value" + stage = "dev" } + + configuration { + key = "some_key" + value = "some_data" + } + + region_id = 1 + project_id = 1 +} + +//*** +// another one example with one interface to private network and fip to internet +//*** + +resource "edgecenter_reservedfixedip" "fixed_ip" { + project_id = 1 + region_id = 1 + type = "ip_address" + network_id = "faf6507b-1ff1-4ebf-b540-befd5c09fe06" + fixed_ip_address = "192.168.13.6" + is_vip = false +} + +resource "edgecenter_volume" "first_volume" { + name = "boot volume" + type_name = "ssd_hiiops" + size = 10 + image_id = "6dc4e061-6fab-41f3-91a3-0ba848fb32d9" + project_id = 1 + region_id = 1 +} + +resource "edgecenter_floatingip" "fip" { + project_id = 1 + region_id = 1 + fixed_ip_address = edgecenter_reservedfixedip.fixed_ip.fixed_ip_address + port_id = edgecenter_reservedfixedip.fixed_ip.port_id } -# Example 2 -# TBD with separate resource + +resource "edgecenter_instance" "v" { + project_id = 1 + region_id = 1 + name = "hello" + flavor_id = "g1-standard-1-2" + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.first_volume.id + boot_index = 0 + } + + interface { + type = "reserved_fixed_ip" + port_id = edgecenter_reservedfixedip.fixed_ip.port_id + fip_source = "existing" + existing_fip_id = edgecenter_floatingip.fip.id + security_groups = ["ada84751-fcca-4491-9249-2dfceb321616"] + } +} ``` @@ -71,52 +152,56 @@ resource "edgecenter_instance" "instance1" { ### Required -- `flavor` (String) ID of the flavor, determining its compute and memory, for example 'g1-standard-2-4'. -- `interface` (Block List, Min: 1) list defining the network interfaces to be attached to the instance (see [below for nested schema](#nestedblock--interface)) -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region -- `volume` (Block List, Min: 1) list of volumes for the instances (see [below for nested schema](#nestedblock--volume)) +- `flavor_id` (String) The ID of the flavor to be used for the instance, determining its compute and memory, for example 'g1-standard-2-4'. +- `interface` (Block List, Min: 1) A list defining the network interfaces to be attached to the instance. (see [below for nested schema](#nestedblock--interface)) +- `volume` (Block Set, Min: 1) A set defining the volumes to be attached to the instance. (see [below for nested schema](#nestedblock--volume)) ### Optional -- `allow_app_ports` (Boolean) if true, application ports will be allowed in the security group for the instances created from the marketplace application template -- `keypair_name` (String) the name of the keypair to inject into new instance(s) -- `metadata` (Map of String) map containing metadata, for example tags. -- `name` (String) the instance name -- `name_templates` (List of String) list of the instance names which will be changed by template: ip_octets, two_ip_octets, one_ip_octet -- `password` (String) this parameter is used to set the password either for the 'Admin' user on a Windows VM or -the default user or a new user on a Linux VM -- `security_groups` (List of String) list of security group (firewall) UUIDs -- `server_group_id` (String) UUID of the anti-affinity or affinity server group (placement groups) -- `user_data` (String) a string in the base64 format. examples of user_data: https://cloudinit.readthedocs.io/en/latest/topics/examples.html -- `username` (String) name of a new user on a Linux VM +- `addresses` (Block List) A list of network addresses associated with the instance, for example "pub_net": [...] (see [below for nested schema](#nestedblock--addresses)) +- `allow_app_ports` (Boolean) A boolean indicating whether to allow application ports on the instance. +- `configuration` (Block List) A list of key-value pairs specifying configuration settings for the instance when created +from a template (marketplace), e.g. {"gitlab_external_url": "https://gitlab/..."} (see [below for nested schema](#nestedblock--configuration)) +- `flavor` (Map of String) A map defining the flavor of the instance, for example, {"flavor_name": "g1-standard-2-4", "ram": 4096, ...}. +- `keypair_name` (String) The name of the key pair to be associated with the instance for SSH access. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata` (Block List, Deprecated) (see [below for nested schema](#nestedblock--metadata)) +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `name` (String) The name of the instance. +- `name_template` (String) A template used to generate the instance name. This field cannot be used with 'name_templates'. +- `name_templates` (List of String, Deprecated) +- `password` (String) The password to be used for accessing the instance. Required with username. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `server_group` (String) The ID (uuid) of the server group to which the instance should belong. +- `status` (String) The current status of the instance. This is computed automatically and can be used to track the instance's state. +- `user_data` (String) A field for specifying user data to be used for configuring the instance at launch time. +- `userdata` (String, Deprecated) **Deprecated** +- `username` (String) The username to be used for accessing the instance. Required with password. +- `vm_state` (String) The current virtual machine state of the instance, +allowing you to start or stop the VM. Possible values are stopped and active. ### Read-Only -- `addresses` (List of Map of String) network addresses associated with the instance - `id` (String) The ID of this resource. -- `keypair_id` (String) uuid of the keypair -- `metadata_detailed` (List of Object) metadata in detailed format with system info (see [below for nested schema](#nestedatt--metadata_detailed)) -- `region` (String) name of the region -- `status` (String) status of the VM -- `vm_state` (String) state of the virtual machine +- `security_group` (List of Object) A list of firewall configurations applied to the instance, defined by their ID and name. (see [below for nested schema](#nestedatt--security_group)) ### Nested Schema for `interface` -Required: - -- `type` (String) available values are 'subnet', 'any_subnet', 'external', 'reserved_fixed_ip' - Optional: -- `floating_ip` (String) floating IP for this subnet attachment -- `floating_ip_source` (String) floating IP type: 'existing' or 'new' +- `existing_fip_id` (String) +- `fip_source` (String) - `ip_address` (String) -- `network_id` (String) ID of the network that the subnet belongs to, required if type is 'subnet' or 'any_subnet' -- `port_id` (String) required if type is 'reserved_fixed_ip' +- `network_id` (String) Required if type is 'subnet' or 'any_subnet'. +- `order` (Number) Order of attaching interface +- `port_id` (String) required if type is 'reserved_fixed_ip' - `security_groups` (List of String) list of security group IDs -- `subnet_id` (String) required if type is 'subnet' +- `subnet_id` (String) Required if type is 'subnet'. +- `type` (String) Available value is 'subnet', 'any_subnet', 'external', 'reserved_fixed_ip' @@ -124,33 +209,72 @@ Optional: Required: -- `size` (Number) size of the volume, specified in gigabytes (GB) -- `source` (String) volume source +- `source` (String) Currently available only 'existing-volume' value Optional: -- `attachment_tag` (String) the block device attachment tag (exposed in the metadata) -- `boot_index` (Number) 0 for the primary boot device. -unique positive values for other bootable devices. negative - the boot is prohibited +- `attachment_tag` (String) +- `boot_index` (Number) If boot_index==0 volumes can not detached - `delete_on_termination` (Boolean) -- `image_id` (String) ID of the image. this field is mandatory if creating a volume from an image -- `metadata` (Map of String) map containing metadata, for example tags. -- `name` (String) name of the volume -- `type_name` (String) volume type with valid values. defaults to 'ssd_hiiops' -- `volume_id` (String) ID of the volume. this field is mandatory if the volume is a pre-existing volume +- `image_id` (String) +- `name` (String) The name assigned to the volume. Defaults to 'system'. +- `size` (Number) The size of the volume, specified in gigabytes (GB). +- `type_name` (String) The type of volume to create. Valid values are 'ssd_hiiops', 'standard', 'cold', and 'ultra'. Defaults to 'standard'. +- `volume_id` (String) Read-Only: - `id` (String) The ID of this resource. - -### Nested Schema for `metadata_detailed` + +### Nested Schema for `addresses` -Read-Only: +Required: + +- `net` (Block List, Min: 1) (see [below for nested schema](#nestedblock--addresses--net)) + + +### Nested Schema for `addresses.net` + +Required: + +- `addr` (String) The net ip address, for example '45.147.163.112'. +- `type` (String) The net type, for example 'fixed'. + + + + +### Nested Schema for `configuration` + +Required: + +- `key` (String) +- `value` (String) + + + +### Nested Schema for `metadata` + +Required: - `key` (String) -- `read_only` (Boolean) - `value` (String) + +### Nested Schema for `security_group` + +Read-Only: + +- `id` (String) +- `name` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_instance.instance1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/k8s.md b/docs/resources/k8s.md new file mode 100644 index 00000000..3e95be07 --- /dev/null +++ b/docs/resources/k8s.md @@ -0,0 +1,122 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_k8s Resource - edgecenter" +subcategory: "" +description: |- + Represent k8s cluster with one default pool. +--- + +# edgecenter_k8s (Resource) + +Represent k8s cluster with one default pool. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_k8s" "v" { + project_id = 1 + region_id = 1 + version = "1.25.11" + name = "tf-k8s" + fixed_network = "6bf878c1-1ce4-47c3-a39b-6b5f1d79bf25" + fixed_subnet = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" + keypair = "tf-keypair" + pool { + name = "tf-pool" + flavor_id = "g1-standard-1-2" + min_node_count = 1 + max_node_count = 2 + node_count = 1 + docker_volume_size = 2 + } +} +``` + + +## Schema + +### Required + +- `fixed_network` (String) Fixed network (uuid) associated with the Kubernetes cluster. +- `fixed_subnet` (String) Subnet (uuid) associated with the fixed network. Ensure there's a router on this subnet. +- `keypair` (String) The name of the keypair +- `name` (String) The name of the Kubernetes cluster. +- `pool` (Block List, Min: 1, Max: 1) Configuration details of the node pool in the Kubernetes cluster. (see [below for nested schema](#nestedblock--pool)) +- `version` (String) The version of the Kubernetes cluster. + +### Optional + +- `auto_healing_enabled` (Boolean) Indicates whether auto-healing is enabled for the Kubernetes cluster. true by default. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `master_lb_floating_ip_enabled` (Boolean) Flag indicating if the master LoadBalancer should have a floating IP. +- `pods_ip_pool` (String) IP pool to be used for pods within the Kubernetes cluster. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `services_ip_pool` (String) IP pool to be used for services within the Kubernetes cluster. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `api_address` (String) API endpoint address for the Kubernetes cluster. +- `cluster_template_id` (String) Template identifier from which the Kubernetes cluster was instantiated. +- `container_version` (String) The container runtime version used in the Kubernetes cluster. +- `created_at` (String) The timestamp when the Kubernetes cluster was created. +- `discovery_url` (String) URL used for node discovery within the Kubernetes cluster. +- `faults` (Map of String) +- `health_status` (String) Overall health status of the Kubernetes cluster. +- `health_status_reason` (Map of String) +- `id` (String) The ID of this resource. +- `master_addresses` (List of String) List of IP addresses for master nodes in the Kubernetes cluster. +- `master_flavor_id` (String) Identifier for the master node flavor in the Kubernetes cluster. +- `node_addresses` (List of String) List of IP addresses for worker nodes in the Kubernetes cluster. +- `node_count` (Number) Total number of nodes in the Kubernetes cluster. +- `status` (String) The current status of the Kubernetes cluster. +- `status_reason` (String) The reason for the current status of the Kubernetes cluster, if ERROR. +- `updated_at` (String) The timestamp when the Kubernetes cluster was updated. +- `user_id` (String) User identifier associated with the Kubernetes cluster. + + +### Nested Schema for `pool` + +Required: + +- `flavor_id` (String) +- `max_node_count` (Number) +- `min_node_count` (Number) +- `name` (String) +- `node_count` (Number) + +Optional: + +- `docker_volume_size` (Number) +- `docker_volume_type` (String) Available value is 'standard', 'ssd_hiiops', 'cold', 'ultra'. + +Read-Only: + +- `created_at` (String) +- `stack_id` (String) +- `uuid` (String) + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_k8s.cluster1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/k8s_pool.md b/docs/resources/k8s_pool.md new file mode 100644 index 00000000..87d5b2df --- /dev/null +++ b/docs/resources/k8s_pool.md @@ -0,0 +1,77 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_k8s_pool Resource - edgecenter" +subcategory: "" +description: |- + Represent k8s cluster's pool. +--- + +# edgecenter_k8s_pool (Resource) + +Represent k8s cluster's pool. + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_k8s_pool" "v" { + project_id = 1 + region_id = 1 + cluster_id = "6bf878c1-1ce4-47c3-a39b-6b5f1d79bf25" + name = "tf-pool" + flavor_id = "g1-standard-1-2" + min_node_count = 1 + max_node_count = 2 + node_count = 1 + docker_volume_size = 2 +} +``` + + +## Schema + +### Required + +- `cluster_id` (String) The uuid of the Kubernetes cluster this pool belongs to. +- `flavor_id` (String) The identifier of the flavor used for nodes in this pool, e.g. g1-standard-2-4. +- `max_node_count` (Number) The maximum number of nodes the pool can scale to. +- `min_node_count` (Number) The minimum number of nodes in the pool. +- `name` (String) The name of the Kubernetes pool. +- `node_count` (Number) The current number of nodes in the pool. + +### Optional + +- `docker_volume_size` (Number) The size of the volume used for Docker containers, in gigabytes. +- `docker_volume_type` (String) The type of volume used for the Docker containers. Available values are 'standard', 'ssd_hiiops', 'cold', and 'ultra'. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `created_at` (String) The timestamp when the Kubernetes pool was created. +- `id` (String) The ID of this resource. +- `stack_id` (String) The identifier of the underlying infrastructure stack used by this pool. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using ::: format +terraform import edgecenter_k8s_pool.k8s_pool1 1:6:a775dd94-4e9c-4da7-9f0e-ffc9ae34446b:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/keypair.md b/docs/resources/keypair.md new file mode 100644 index 00000000..74519a64 --- /dev/null +++ b/docs/resources/keypair.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_keypair Resource - edgecenter" +subcategory: "" +description: |- + Represent a ssh key, do not depends on region +--- + +# edgecenter_keypair (Resource) + +Represent a ssh key, do not depends on region + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_keypair" "kp" { + project_id = 1 + public_key = "your public key here" + sshkey_name = "test" +} + +output "kp" { + value = edgecenter_keypair.kp +} +``` + + +## Schema + +### Required + +- `public_key` (String) The public portion of the SSH key pair. +- `sshkey_name` (String) The name assigned to the SSH key pair, used for identification purposes. + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. + +### Read-Only + +- `fingerprint` (String) A fingerprint of the SSH public key, used to verify the integrity of the key. +- `id` (String) The ID of this resource. +- `sshkey_id` (String) The unique identifier assigned by the provider to the SSH key pair. + + diff --git a/docs/resources/lblistener.md b/docs/resources/lblistener.md index 21ecf782..bf73e05d 100644 --- a/docs/resources/lblistener.md +++ b/docs/resources/lblistener.md @@ -3,33 +3,35 @@ page_title: "edgecenter_lblistener Resource - edgecenter" subcategory: "" description: |- - A listener is a process that checks for connection requests using the protocol and port that you configure. - Can not be created without a load balancer. + Represent a load balancer listener. Can not be created without a load balancer. A listener is a process that checks for connection requests using the protocol and port that you configure. --- # edgecenter_lblistener (Resource) -A listener is a process that checks for connection requests using the protocol and port that you configure. -Can not be created without a load balancer. +Represent a load balancer listener. Can not be created without a load balancer. A listener is a process that checks for connection requests using the protocol and port that you configure. ## Example Usage ```terraform -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other_fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_loadbalancerv2" "lb" { + project_id = 1 + region_id = 1 + name = "test" + flavor = "lb1-1-2" } resource "edgecenter_lblistener" "listener" { - region_id = var.region_id - project_id = var.project_id - name = "test-lblistener" - loadbalancer_id = edgecenter_loadbalancer.lb.id - protocol_port = 80 - protocol = "HTTP" - insert_x_forwarded = true - allowed_cidrs = ["10.10.0.0/24"] + project_id = 1 + region_id = 1 + name = "test" + protocol = "TCP" + protocol_port = 36621 + allowed_cidrs = ["127.0.0.0/24", "192.168.0.0/24"] + loadbalancer_id = edgecenter_loadbalancerv2.lb.id } ``` @@ -38,26 +40,44 @@ resource "edgecenter_lblistener" "listener" { ### Required -- `loadbalancer_id` (String) ID of the load balancer -- `name` (String) listener name -- `project_id` (Number) uuid of the project -- `protocol` (String) available values are 'HTTP', 'HTTPS', 'TCP', 'UDP' and 'TERMINATED_HTTPS' -- `protocol_port` (Number) port on which the protocol is bound -- `region_id` (Number) uuid of the region +- `loadbalancer_id` (String) The uuid for the load balancer. +- `name` (String) The name of the load balancer listener. +- `protocol` (String) Available values are 'TCP', 'UDP', 'HTTP', 'HTTPS' and 'Terminated HTTPS'. +- `protocol_port` (Number) The port on which the protocol is bound. ### Optional -- `allowed_cidrs` (List of String) the allowed CIDRs for listener -- `insert_x_forwarded` (Boolean) add headers X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto to requests. only used with HTTP or TERMINATED_HTTPS protocols -- `secret_id` (String) ID of the secret where PKCS12 file is stored for the TERMINATED_HTTPS load balancer -- `sni_secret_id` (List of String) list of secret identifiers used for Server Name Indication (SNI). +- `allowed_cidrs` (List of String) The allowed CIDRs for listener. +- `insert_x_forwarded` (Boolean) Insert *-forwarded headers +- `last_updated` (String) The timestamp of the last update (use with update context). +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `secret_id` (String) The identifier for the associated secret, typically used for SSL configurations. +- `sni_secret_id` (List of String) List of secret identifiers used for Server Name Indication (SNI). +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) ### Read-Only -- `id` (String) listener uuid -- `insert_headers` (Map of String) dictionary of additional header insertion into the HTTP headers. only used with the HTTP and TERMINATED_HTTPS protocols -- `operating_status` (String) operating status of the listener -- `pool_count` (Number) number of pools -- `provisioning_status` (String) lifecycle status of the listener +- `id` (String) The ID of this resource. +- `operating_status` (String) The current operational status of the load balancer. +- `pool_count` (Number) Number of pools associated with the load balancer. +- `provisioning_status` (String) The current provisioning status of the load balancer. + + +### Nested Schema for `timeouts` + +Optional: +- `create` (String) +- `delete` (String) +## Import + +Import is supported using the following syntax: + +```shell +# import using ::: format +terraform import edgecenter_lblistener.lblistener1 1:6:a775dd94-4e9c-4da7-9f0e-ffc9ae34446b:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/lbmember.md b/docs/resources/lbmember.md index 4e686a12..c3abacff 100644 --- a/docs/resources/lbmember.md +++ b/docs/resources/lbmember.md @@ -3,31 +3,59 @@ page_title: "edgecenter_lbmember Resource - edgecenter" subcategory: "" description: |- - A Member node represents a physical server that acts as a provider of a service available to a load balancer. - Does not support concurrent update of multiple members. Update one at a time + Represent load balancer member --- # edgecenter_lbmember (Resource) -A Member node represents a physical server that acts as a provider of a service available to a load balancer. -Does not support concurrent update of multiple members. Update one at a time +Represent load balancer member ## Example Usage ```terraform -resource "edgecenter_lbpool" "pool" { - region_id = var.region_id - project_id = var.project_id - // other_fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -resource "edgecenter_lbmember" "member" { - region_id = var.region_id - project_id = var.project_id - pool_id = edgecenter_lbpool.pool.id - address = "10.10.0.7" - protocol_port = 9099 - weight = 20 +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test1" + flavor = "lb1-1-2" + listeners { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } +} + +resource "edgecenter_lbpool" "pl" { + project_id = 1 + region_id = 1 + name = "test_pool1" + protocol = "HTTP" + lb_algorithm = "LEAST_CONNECTIONS" + loadbalancer_id = edgecenter_loadbalancer.lb.id + listener_id = edgecenter_loadbalancer.lb.listeners.0.id + health_monitor { + type = "PING" + delay = 60 + max_retries = 5 + timeout = 10 + } + session_persistence { + type = "APP_COOKIE" + cookie_name = "test_new_cookie" + } +} + +resource "edgecenter_lbmember" "lbm" { + project_id = 1 + region_id = 1 + pool_id = edgecenter_lbpool.pl.id + address = "10.10.2.15" + protocol_port = 8081 + weight = 5 } ``` @@ -36,22 +64,40 @@ resource "edgecenter_lbmember" "member" { ### Required -- `address` (String) IP address of the load balancer pool member -- `pool_id` (String) ID of the load balancer pool -- `project_id` (Number) uuid of the project -- `protocol_port` (Number) IP port on which the member listens for requests -- `region_id` (Number) uuid of the region +- `address` (String) The IP address of the load balancer pool member. +- `pool_id` (String) The uuid for the load balancer pool. +- `protocol_port` (Number) The port on which the member listens for requests. ### Optional -- `admin_state_up` (Boolean) true if enabled. Defaults to true -- `instance_id` (String) uuid of the instance (amphora) associated with the pool member. -- `subnet_id` (String) uuid of the subnet in which the pool member is located. -- `weight` (Number) weight value between 0 and 256, determining the distribution of requests among the members of the pool. defaults to 1 +- `instance_id` (String) The uuid of the instance (amphora) associated with the pool member. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `subnet_id` (String) The uuid of the subnet in which the pool member is located. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `weight` (Number) A weight value between 0 and 256, determining the distribution of requests among the members of the pool. ### Read-Only -- `id` (String) ID of the member must be provided if the existing member is being updated -- `operating_status` (String) operating status of the pool +- `id` (String) The ID of this resource. +- `operating_status` (String) The current operating status of the pool member. + + +### Nested Schema for `timeouts` +Optional: +- `create` (String) +- `delete` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using ::: format +terraform import edgecenter_lbmember.lbmember1 1:6:a775dd94-4e9c-4da7-9f0e-ffc9ae34446b:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/lbpool.md b/docs/resources/lbpool.md index a8d20684..79c93f3b 100644 --- a/docs/resources/lbpool.md +++ b/docs/resources/lbpool.md @@ -3,39 +3,49 @@ page_title: "edgecenter_lbpool Resource - edgecenter" subcategory: "" description: |- - A pool is a list of virtual machines to which the listener will redirect incoming traffic + Represent load balancer listener pool. A pool is a list of virtual machines to which the listener will redirect incoming traffic --- # edgecenter_lbpool (Resource) -A pool is a list of virtual machines to which the listener will redirect incoming traffic +Represent load balancer listener pool. A pool is a list of virtual machines to which the listener will redirect incoming traffic ## Example Usage ```terraform -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other_fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -resource "edgecenter_lblistener" "lis" { - region_id = var.region_id - project_id = var.project_id - // other_fields +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test1" + flavor = "lb1-1-2" + listener { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } } -resource "edgecenter_lbpool" "pool" { - region_id = var.region_id - project_id = var.project_id - name = "test-lbpool" - lb_algorithm = "LEAST_CONNECTIONS" +resource "edgecenter_lbpool" "pl" { + project_id = 1 + region_id = 1 + name = "test_pool1" protocol = "HTTP" + lb_algorithm = "LEAST_CONNECTIONS" loadbalancer_id = edgecenter_loadbalancer.lb.id - listener_id = edgecenter_lblistener.lis.id - healthmonitor { - type = "TCP" - delay = 70 + listener_id = edgecenter_loadbalancer.lb.listener.0.id + health_monitor { + type = "PING" + delay = 60 + max_retries = 5 + timeout = 10 + } + session_persistence { + type = "APP_COOKIE" + cookie_name = "test_new_cookie" } } ``` @@ -45,45 +55,44 @@ resource "edgecenter_lbpool" "pool" { ### Required -- `healthmonitor` (Block List, Min: 1, Max: 1) configuration for health checks to test the health and state of the backend members. -it determines how the load balancer identifies whether the backend members are healthy or unhealthy. (see [below for nested schema](#nestedblock--healthmonitor)) -- `lb_algorithm` (String) algorithm of the load balancer. available values are 'ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP', 'SOURCE_IP_PORT' -- `listener_id` (String) ID of the load balancer listener -- `loadbalancer_id` (String) ID of the load balancer -- `name` (String) lb pool name -- `project_id` (Number) uuid of the project -- `protocol` (String) available values are 'HTTP', 'HTTPS', 'TCP', 'UDP' and 'PROXY' -- `region_id` (Number) uuid of the region +- `lb_algorithm` (String) Available values is 'ROUND_ROBIN', 'LEAST_CONNECTIONS', 'SOURCE_IP', 'SOURCE_IP_PORT' +- `name` (String) The name of the load balancer listener pool. +- `protocol` (String) Available values is 'HTTP' (currently work, other do not work on ed-8), 'HTTPS', 'TCP', 'UDP' ### Optional -- `session_persistence` (Block List, Max: 1) configuration that enables the load balancer to bind a user's session to a specific backend member. -this ensures that all requests from the user during the session are sent to the same member. (see [below for nested schema](#nestedblock--session_persistence)) -- `timeout_client_data` (Number) timeout for the frontend client inactivity (in milliseconds) -- `timeout_member_connect` (Number) timeout for the backend member connection (in milliseconds) -- `timeout_member_data` (Number) timeout for the backend member inactivity (in milliseconds) +- `health_monitor` (Block List, Max: 1) Configuration for health checks to test the health and state of the backend members. +It determines how the load balancer identifies whether the backend members are healthy or unhealthy. (see [below for nested schema](#nestedblock--health_monitor)) +- `last_updated` (String) The timestamp of the last update (use with update context). +- `listener_id` (String) The uuid for the load balancer listener. +- `loadbalancer_id` (String) The uuid for the load balancer. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `session_persistence` (Block List, Max: 1) Configuration that enables the load balancer to bind a user's session to a specific backend member. +This ensures that all requests from the user during the session are sent to the same member. (see [below for nested schema](#nestedblock--session_persistence)) +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) ### Read-Only -- `id` (String) lb pool uuid -- `operating_status` (String) operating status of the pool -- `provisioning_status` (String) lifecycle status of the pool +- `id` (String) The ID of this resource. - -### Nested Schema for `healthmonitor` + +### Nested Schema for `health_monitor` Required: -- `type` (String) available values are 'HTTP', 'HTTPS', 'PING', 'TCP', 'TLS-HELLO', 'UDP-CONNECT +- `delay` (Number) +- `max_retries` (Number) +- `timeout` (Number) +- `type` (String) Available values is 'HTTP', 'HTTPS', 'PING', 'TCP', 'TLS-HELLO', 'UDP-CONNECT Optional: -- `delay` (Number) check interval (in sec) - `expected_codes` (String) - `http_method` (String) -- `max_retries` (Number) healthy thresholds -- `max_retries_down` (Number) unhealthy thresholds -- `timeout` (Number) Response time (in sec) +- `max_retries_down` (Number) - `url_path` (String) Read-Only: @@ -105,3 +114,19 @@ Optional: - `persistence_timeout` (Number) + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_lbpool.lbpool1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/lifecyclepolicy.md b/docs/resources/lifecyclepolicy.md new file mode 100644 index 00000000..0df07384 --- /dev/null +++ b/docs/resources/lifecyclepolicy.md @@ -0,0 +1,142 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_lifecyclepolicy Resource - edgecenter" +subcategory: "" +description: |- + Represent lifecycle policy. Use to periodically take snapshots +--- + +# edgecenter_lifecyclepolicy (Resource) + +Represent lifecycle policy. Use to periodically take snapshots + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_lifecyclepolicy" "lp" { + project_id = 1 + region_id = 1 + name = "test" + status = "active" + action = "volume_snapshot" + volume { + id = "fe93bfdd-4ce3-4041-b89b-4f10d0d49498" + } + schedule { + max_quantity = 4 + interval { + weeks = 1 + days = 2 + hours = 3 + minutes = 4 + } + resource_name_template = "reserve snap of the volume {volume_id}" + retention_time { + weeks = 4 + days = 3 + hours = 2 + minutes = 1 + } + } +} +``` + + +## Schema + +### Required + +- `name` (String) + +### Optional + +- `action` (String) +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `schedule` (Block List) (see [below for nested schema](#nestedblock--schedule)) +- `status` (String) +- `volume` (Block Set) List of managed volumes (see [below for nested schema](#nestedblock--volume)) + +### Read-Only + +- `id` (String) The ID of this resource. +- `user_id` (Number) + + +### Nested Schema for `schedule` + +Required: + +- `max_quantity` (Number) Maximum number of stored resources + +Optional: + +- `cron` (Block List, Max: 1) Use for taking actions at specified moments of time. Exactly one of interval and cron blocks should be provided (see [below for nested schema](#nestedblock--schedule--cron)) +- `interval` (Block List, Max: 1) Use for taking actions with equal time intervals between them. Exactly one of interval and cron blocks should be provided (see [below for nested schema](#nestedblock--schedule--interval)) +- `resource_name_template` (String) Used to name snapshots. {volume_id} is substituted with volume.id on creation +- `retention_time` (Block List, Max: 1) If it is set, new resource will be deleted after time (see [below for nested schema](#nestedblock--schedule--retention_time)) + +Read-Only: + +- `id` (String) The ID of this resource. +- `type` (String) + + +### Nested Schema for `schedule.cron` + +Optional: + +- `day` (String) Either single asterisk or comma-separated list of integers (1-31) +- `day_of_week` (String) Either single asterisk or comma-separated list of integers (0-6) +- `hour` (String) Either single asterisk or comma-separated list of integers (0-23) +- `minute` (String) Either single asterisk or comma-separated list of integers (0-59) +- `month` (String) Either single asterisk or comma-separated list of integers (1-12) +- `timezone` (String) +- `week` (String) Either single asterisk or comma-separated list of integers (1-53) + + + +### Nested Schema for `schedule.interval` + +Optional: + +- `days` (Number) Number of days to wait between actions +- `hours` (Number) Number of hours to wait between actions +- `minutes` (Number) Number of minutes to wait between actions +- `weeks` (Number) Number of weeks to wait between actions + + + +### Nested Schema for `schedule.retention_time` + +Optional: + +- `days` (Number) Number of days to wait before deleting snapshot +- `hours` (Number) Number of hours to wait before deleting snapshot +- `minutes` (Number) Number of minutes to wait before deleting snapshot +- `weeks` (Number) Number of weeks to wait before deleting snapshot + + + + +### Nested Schema for `volume` + +Read-Only: + +- `id` (String) The ID of this resource. +- `name` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_lifecyclepolicy.lifecyclepolicy1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/loadbalancer.md b/docs/resources/loadbalancer.md index 2ae3023f..0aec2241 100644 --- a/docs/resources/loadbalancer.md +++ b/docs/resources/loadbalancer.md @@ -3,33 +3,35 @@ page_title: "edgecenter_loadbalancer Resource - edgecenter" subcategory: "" description: |- - A loadbalancer is a software service that distributes incoming network traffic - (e.g., web traffic, application requests) across multiple servers or resources. + Represent load balancer --- # edgecenter_loadbalancer (Resource) -A loadbalancer is a software service that distributes incoming network traffic -(e.g., web traffic, application requests) across multiple servers or resources. +Represent load balancer ## Example Usage ```terraform -# Example 1 +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - name = "test-lb" - flavor_name = "lb1-1-2" - vip_network_id = "00000000-0000-0000-0000-000000000000" - floating_ip_source = "new" - metadata = { - "tag" : "system" + project_id = 1 + region_id = 1 + name = "test" + flavor = "lb1-1-2" + //when upgrading to version 0.2.28 nested listener max length reduced to 1 + //that mean, if you had more than one nested listener and removed them from + //schema they not delete in the cloud. User has to delete it manually and + //recreate as edgecenter_lblistener resource + listener { + name = "test" + protocol = "HTTP" + protocol_port = 80 } } - -# Example 2 -# TBD with separate resource ``` @@ -37,28 +39,74 @@ resource "edgecenter_loadbalancer" "lb" { ### Required -- `flavor_name` (String) flavor name of the load balancer -- `name` (String) name of the load balancer -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region +- `listener` (Block List, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--listener)) +- `name` (String) The name of the load balancer. ### Optional -- `floating_ip` (String) floating IP for this subnet attachment -- `floating_ip_source` (String) floating IP type: 'existing' or 'new' -- `metadata` (Map of String) map containing metadata, for example tags. -- `vip_network_id` (String) ID of the Network. шf not specified, the default external network will be used -- `vip_port_id` (String) ID of the existing reserved fixed IP port for the load balancer -- `vip_subnet_id` (String) ID of the subnet. if not specified, any subnet from vip_network_id will be selected +- `flavor` (String) +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `vip_network_id` (String) +- `vip_subnet_id` (String) ### Read-Only -- `flavor` (Map of String) information about the flavor - `id` (String) The ID of this resource. -- `operating_status` (String) operating status of the load balancer -- `provisioning_status` (String) lifecycle status of the load balancer -- `region` (String) name of the region -- `vip_address` (String) IP address of the load balancer -- `vrrp_ips` (List of String) list of VRRP IP addresses +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `vip_address` (String) Load balancer IP address + + +### Nested Schema for `listener` + +Required: + +- `name` (String) +- `protocol` (String) Available values is 'HTTP' (currently work, other do not work on ed-8), 'HTTPS', 'TCP', 'UDP' +- `protocol_port` (Number) + +Optional: + +- `certificate` (String) +- `certificate_chain` (String) +- `insert_x_forwarded` (Boolean) +- `private_key` (String) +- `secret_id` (String) +- `sni_secret_id` (List of String) + +Read-Only: + +- `id` (String) The ID of this resource. + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) + + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using ::: format, listener_id - nested listener id +terraform import edgecenter_loadbalancer.loadbalancer1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7:a336f28c-fbb0-4256-9545-e905bed9f48f +``` diff --git a/docs/resources/loadbalancerv2.md b/docs/resources/loadbalancerv2.md new file mode 100644 index 00000000..0bec66c0 --- /dev/null +++ b/docs/resources/loadbalancerv2.md @@ -0,0 +1,83 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_loadbalancerv2 Resource - edgecenter" +subcategory: "" +description: |- + Represent load balancer without nested listener +--- + +# edgecenter_loadbalancerv2 (Resource) + +Represent load balancer without nested listener + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_loadbalancerv2" "lb" { + project_id = 1 + region_id = 1 + name = "test" + flavor = "lb1-1-2" + metadata_map = { + tag1 = "tag1_value" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the load balancer. + +### Optional + +- `flavor` (String) The flavor or specification of the load balancer to be created. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) +- `vip_network_id` (String) Attaches the created network. +- `vip_port_id` (String) Attaches the created reserved IP. +- `vip_subnet_id` (String) The ID of the subnet in which to allocate the VIP address for the load balancer. + +### Read-Only + +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) +- `vip_address` (String) Load balancer IP address + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) + + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_loadbalancer.loadbalancer1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/network.md b/docs/resources/network.md new file mode 100644 index 00000000..13dc9f53 --- /dev/null +++ b/docs/resources/network.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_network Resource - edgecenter" +subcategory: "" +description: |- + Represent network. A network is a software-defined network in a cloud computing infrastructure +--- + +# edgecenter_network (Resource) + +Represent network. A network is a software-defined network in a cloud computing infrastructure + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_network" "network" { + name = "network_example" + type = "vxlan" + region_id = 1 + project_id = 1 +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the network. + +### Optional + +- `create_router` (Boolean) Create external router to the network, default true +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `mtu` (Number) Maximum Transmission Unit (MTU) for the network. It determines the maximum packet size that can be transmitted without fragmentation. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `type` (String) 'vlan' or 'vxlan' network type is allowed. Default value is 'vxlan' + +### Read-Only + +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_network.metwork1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/reservedfixedip.md b/docs/resources/reservedfixedip.md new file mode 100644 index 00000000..b98b9215 --- /dev/null +++ b/docs/resources/reservedfixedip.md @@ -0,0 +1,69 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_reservedfixedip Resource - edgecenter" +subcategory: "" +description: |- + Represent reserved ips +--- + +# edgecenter_reservedfixedip (Resource) + +Represent reserved ips + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_reservedfixedip" "fixed_ip" { + project_id = 1 + region_id = 1 + type = "external" + is_vip = false +} +``` + + +## Schema + +### Required + +- `is_vip` (Boolean) Flag to determine if the reserved fixed IP should be treated as a Virtual IP (VIP). +- `type` (String) The type of reserved fixed IP. Valid values are 'external', 'subnet', 'any_subnet', and 'ip_address' + +### Optional + +- `allowed_address_pairs` (Block List) Group of IP addresses that share the current IP as VIP. (see [below for nested schema](#nestedblock--allowed_address_pairs)) +- `fixed_ip_address` (String) The IP address that is associated with the reserved IP. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `network_id` (String) ID of the network to which the reserved fixed IP is associated. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `subnet_id` (String) ID of the subnet from which the fixed IP should be reserved. + +### Read-Only + +- `id` (String) The ID of this resource. +- `port_id` (String) ID of the port_id underlying the reserved fixed IP. +- `status` (String) The current status of the reserved fixed IP. + + +### Nested Schema for `allowed_address_pairs` + +Optional: + +- `ip_address` (String) +- `mac_address` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_reservedfixedip.reservedfixedip1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/router.md b/docs/resources/router.md new file mode 100644 index 00000000..3b9f8315 --- /dev/null +++ b/docs/resources/router.md @@ -0,0 +1,132 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_router Resource - edgecenter" +subcategory: "" +description: |- + Represent router. Router enables you to dynamically exchange routes between networks +--- + +# edgecenter_router (Resource) + +Represent router. Router enables you to dynamically exchange routes between networks + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_router" "router" { + name = "router_example" + + dynamic "external_gateway_info" { + iterator = egi + for_each = var.external_gateway_info + content { + type = egi.value.type + enable_snat = egi.value.enable_snat + network_id = egi.value.network_id + } + } + + dynamic "interfaces" { + iterator = ifaces + for_each = var.interfaces + content { + type = ifaces.value.type + subnet_id = ifaces.value.subnet_id + } + } + + dynamic "routes" { + iterator = rs + for_each = var.routes + content { + destination = rs.value.destination + nexthop = rs.value.nexthop + } + } + + region_id = 1 + project_id = 1 +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the router. + +### Optional + +- `external_gateway_info` (Block List, Max: 1) Information related to the external gateway. (see [below for nested schema](#nestedblock--external_gateway_info)) +- `interfaces` (Block Set) Set of interfaces associated with the router. (see [below for nested schema](#nestedblock--interfaces)) +- `last_updated` (String) The timestamp of the last update (use with update context). +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `routes` (Block List) List of static routes to be applied to the router. (see [below for nested schema](#nestedblock--routes)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `external_gateway_info` + +Optional: + +- `enable_snat` (Boolean) +- `network_id` (String) Id of the external network +- `type` (String) Must be 'manual' or 'default' + +Read-Only: + +- `external_fixed_ips` (List of Object) (see [below for nested schema](#nestedatt--external_gateway_info--external_fixed_ips)) + + +### Nested Schema for `external_gateway_info.external_fixed_ips` + +Read-Only: + +- `ip_address` (String) +- `subnet_id` (String) + + + + +### Nested Schema for `interfaces` + +Required: + +- `subnet_id` (String) Subnet for router interface must have a gateway IP +- `type` (String) must be 'subnet' + +Read-Only: + +- `ip_address` (String) +- `mac_address` (String) +- `network_id` (String) +- `port_id` (String) + + + +### Nested Schema for `routes` + +Required: + +- `destination` (String) +- `nexthop` (String) IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_router.router1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/secret.md b/docs/resources/secret.md new file mode 100644 index 00000000..278cf9cf --- /dev/null +++ b/docs/resources/secret.md @@ -0,0 +1,67 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_secret Resource - edgecenter" +subcategory: "" +description: |- + Represent secret +--- + +# edgecenter_secret (Resource) + +Represent secret + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_secret" "lb_https" { + region_id = 1 + project_id = 1 + + name = "test" + private_key = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQ4E6U0vql4EST\n8o41TlHRz6MKmMhddVUjM2juTKjxv4WuB4T3z/wokznEjQg4H7gfYEKeCJqelrfq\ntdOtbPsznSceMOXB5uA2Sc9WVKwk7owoRJxPd4LQeOcarVOFdIzudzkgSK/oV7Za\nL8Y2hylsB4SX2cfbULtmW/WDePp3YZAL6zYV1fXJSnK+hL2iUSqikiViEGRta+47\nnaTKZnnmSgojdshzsw0wlF/PgRJ/Anf9j9J8ratdJP81yAG5daU3L2NdJ3qx9UbV\ntKnSq2z2u4yx6xdb4t4WFQBKNjC6+YZN/gI5lp96p3FNTNS4PKYxAAUrnCwf0EE3\n7dOR4eWlAgMBAAECggEBALPm3ge0h4li1e4PVYh4AmSRT74KxVgpfMCqwM+uWzyM\nVpkDhPTjwC06UOEHD3M3bqAninkOtA2vhoyzOrP+T4Wu70hDmUAemDJp9BhJKVNN\n2o28Olz/dD4WRAZoDq29Kr0hFqTFtiyJj1eyGihQ1c5j00HuowI0UJPi1Fz+T8uN\nPwukUtTPYwEds6SApii3v9VKjmvbRDmsbHU3KkUoaeqpRnRagyp1vtoLXigezUcK\nrQcoh6wlKtvj0YLR2lxq9Wmj1nn6m3F5Bom54X8o18tcOmFSRudRb+Fxjb0jnqSK\nAsyVlZg4alTBQUmx9gIKv0oSJAIh2nXdclECkGjs8WkCgYEA9xvdDWephsbv+X3k\nndnDG9JTxfrR6HMHPrUrTaZ8/VD+Qw4zuReoNGkcQbV3Cb26egprWQWfYc9+l6mU\nAWgOjFgeGie1uwOwkhv6CfhE/iVvotJ3hOOsC5pLEhz4vRpO75C9wSehjfTYkP1m\nXEAhRTRbgMnvzChWyh5CEjosX5sCgYEA2GRHrG0JVxsYSCugLPKf9fSK4CQDm0bK\nywBwZtAWX0xhiHO/BW6PeK1Mqx2nbiWl1hXNpZKJNS9bnrZWym/yUqOvg2XJKjb6\nhHBvwAD1MOQ8Ysby4JHGCrMBEwlcDpI2wpMpXkKhU3X0XWjkqrhqCH/TETFKkqLt\nfJX/c9PTQ78CgYAEPek0grQJST7zVHLpNsS/pIOloWGbEOZt8CQ3KAV7P7mtov/G\nTJ6pj6hZhGjvtN8Pm0Aufgc3YZ11swaEY6nkRNr3bfkTpcORLoPDSgy9JB1feSdu\nE45vgI2LWQ34CQyT1jM7rpd6XVqeWos4SC2KB5UOh+ji40piG9TchT0fwwKBgA/M\nmpMTTvhGKSqzzLkbaeR6W11sI7tFmu7hdFN9Y/THTeO5l7vcy6ri9FMWEjBvnUEZ\nTG+HWG9CquzWoVWcgNPZ0anFV7+2Teo3j2E0cLKGJ4aKwhb1bcFAOpbaOxdxQ4BH\nYGDaeo7ucM4VJ4TzfAJs2stJjwlPzgknpoQddjJfAoGBAIFfnU8x/SrNhAqZrG9d\n3kpJ5LmbVswOYtj01KHM+KpEwOQVF+s2NOeHqyC7QUIWrue00+1MT88F9cNHDeWk\n0dEOJNWCfzcV85l8A+0p6/4qAW7h7RNiFqeA8GyVKCT8f7fu/7WpYw8D0aq8w5X/\nKZl+AjB+MzYFs71+SC4ohTlI\n-----END PRIVATE KEY-----" + certificate = "-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIJAIUvym0uaBHbMA0GCSqGSIb3DQEBCwUAMD0xCzAJBgNV\nBAYTAlJVMQ8wDQYDVQQIDAZNT1NDT1cxCzAJBgNVBAoMAkNBMRAwDgYDVQQDDAdS\nT09UIENBMB4XDTIxMDczMDE1MTU0NVoXDTMxMDcyODE1MTU0NVowTDELMAkGA1UE\nBhMCQ0ExDTALBgNVBAgMBE5vbmUxCzAJBgNVBAcMAk5CMQ0wCwYDVQQKDAROb25l\nMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQDQ4E6U0vql4EST8o41TlHRz6MKmMhddVUjM2juTKjxv4WuB4T3z/wokznE\njQg4H7gfYEKeCJqelrfqtdOtbPsznSceMOXB5uA2Sc9WVKwk7owoRJxPd4LQeOca\nrVOFdIzudzkgSK/oV7ZaL8Y2hylsB4SX2cfbULtmW/WDePp3YZAL6zYV1fXJSnK+\nhL2iUSqikiViEGRta+47naTKZnnmSgojdshzsw0wlF/PgRJ/Anf9j9J8ratdJP81\nyAG5daU3L2NdJ3qx9UbVtKnSq2z2u4yx6xdb4t4WFQBKNjC6+YZN/gI5lp96p3FN\nTNS4PKYxAAUrnCwf0EE37dOR4eWlAgMBAAGjgZcwgZQwVwYDVR0jBFAwTqFBpD8w\nPTELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1PU0NPVzELMAkGA1UECgwCQ0ExEDAO\nBgNVBAMMB1JPT1QgQ0GCCQCectJTETy4lTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE\n8DAhBgNVHREEGjAYgglsb2NhbGhvc3SCCyoubG9jYWxob3N0MA0GCSqGSIb3DQEB\nCwUAA4IBAQBqzJcwygLsVCTPlReUpcKVn84aFqzfZA0m7hYvH+7PDH/FM8SbX3zg\nteBL/PgQAZw1amO8xjeMc2Pe2kvi9VrpfTeGqNia/9axhGu3q/NEP0tyDFXAE2bR\njBdGhd5gCmg+X4WdHigCgn51cz5r2k3fSOIWP+TQWHqc8Yt+vZXnkwnQkRA1Ki7N\nWOiJjj/ae5RWwma/kJNmShTZn754gbQn06bAjNbPjclsHRLkawmLqikd1rYUhIdk\nOr1Nrl+CWMx3CXg0TVVdJ6rH3dO31uyvb+3qEY7WnL+HhZyr08ay8gJsEKPuPFA2\nxvveXqt9ceU5qh+8T7mHwGALEUw96QcP\n-----END CERTIFICATE-----" + certificate_chain = "-----BEGIN CERTIFICATE-----\nMIIC9jCCAd4CCQCectJTETy4lTANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJS\nVTEPMA0GA1UECAwGTU9TQ09XMQswCQYDVQQKDAJDQTEQMA4GA1UEAwwHUk9PVCBD\nQTAeFw0yMTA3MzAxNTExMzVaFw0yNDA1MTkxNTExMzVaMD0xCzAJBgNVBAYTAlJV\nMQ8wDQYDVQQIDAZNT1NDT1cxCzAJBgNVBAoMAkNBMRAwDgYDVQQDDAdST09UIENB\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo6tZ0NV6QIR/mvsqtAII\nzTTuBMrZR5OTwKvcGnhe4GVDwzJ/OgEWkghLAzOojcJvkfzJOtWwOXqwgphksc+7\n+vwIPTPt3iWjbQUzXK8pFLkjxrO8px/QxPuUrp+U6DTVvvgQesjMZ9jQRUFKOiCc\nu0st1N5Q/CJR4VOJxtYoLy1ZUlsABhwJ+6trkoOFTLRPlMUX1EIG57jYAotHvQFo\nc8UNx3KzvJsJJ56SniXCIkeu61IOt8aOXHU+3TLYhZnPiP311cMbXA0J3vGPRZwz\n25BZjF3IF/ShXlfzz76FjWUTAThc0+HA8lzx53xD4/n8HN+sGubGx9TvLyZimG/U\nGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAnK8Wzw33fR6R6pqV05XI9Yu8J+BwC\nCn2bKxxYwwQWZyX1as+UIlGuvyBRJba9W2UGMj95FQfWVdDyFC98spUur+O/5yL+\nNHH+dxGnkxIRc6RMIy+GXJwPrLiB/t70hSvwgVa249zNJVcwYN/5SGX5wLaJKnim\neY99xm75nr03O/RJK/DR8HvWysH7zxvrMWs0ppfwxkxrwOcg0Cb9xODVkg/wyClw\nLiHWlmH/eyC8nkiLYJKmV7566VWCV+gy+hC/DRstVVjIMG6LsqaPq6ycm7N8EV8s\nBb5uXIVHW6w5a20c40+W9G4EDYiQjdgEaf0FoMAWGDnOEaPsvjQk2/z5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDPDCCAiQCCQDxA75ydLHVoTANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJS\nVTEPMA0GA1UECAwGTU9TQ09XMQ8wDQYDVQQHDAZNT1NDT1cxFTATBgNVBAoMDElO\nVEVSTUVESUFURTEYMBYGA1UEAwwPSU5URVJNRURJQVRFIENBMB4XDTIxMDczMDE1\nMTIyMloXDTI0MDUxOTE1MTIyMlowYDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1P\nU0NPVzEPMA0GA1UEBwwGTU9TQ09XMRUwEwYDVQQKDAxJTlRFUk1FRElBVEUxGDAW\nBgNVBAMMD0lOVEVSTUVESUFURSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAKOrWdDVekCEf5r7KrQCCM007gTK2UeTk8Cr3Bp4XuBlQ8MyfzoBFpII\nSwMzqI3Cb5H8yTrVsDl6sIKYZLHPu/r8CD0z7d4lo20FM1yvKRS5I8azvKcf0MT7\nlK6flOg01b74EHrIzGfY0EVBSjognLtLLdTeUPwiUeFTicbWKC8tWVJbAAYcCfur\na5KDhUy0T5TFF9RCBue42AKLR70BaHPFDcdys7ybCSeekp4lwiJHrutSDrfGjlx1\nPt0y2IWZz4j99dXDG1wNCd7xj0WcM9uQWYxdyBf0oV5X88++hY1lEwE4XNPhwPJc\n8ed8Q+P5/BzfrBrmxsfU7y8mYphv1BsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA\ngOHvrh66+bQoG3Lo8bfp7D1Xvm/Md3gJq2nMotl2BH1TvNzMV93fCXygRX8J8rTL\n7xjUC2SbOrFDWFq2hNJQagdecAeuG+U55BY6Wi8SsHw+fhgxQyl9wtXWwotQPmsD\nuRhR1rL3vEphgPLbxNBzA7Lvj+P89Ar988Qy+o5AiUzHMUuqZbGOqs8UcKCQP7e/\nIX+zqqFwqyI8f90SVySGgs574jo8jQFy3l5fnp6yK0MPWg2cBCjpa5H1A+5DADF+\nnryV6Ie/m/wfxmitZZN+YCJu+8Bmmdl/FCwbmiH+HCLhrO8gonH3K21cQujMyFF5\nc7OFj86hvhqbr4kzz1J8lg==\n-----END CERTIFICATE-----" + expiration = "2025-12-28T19:14:44.213" +} +``` + + +## Schema + +### Required + +- `certificate` (String) SSL certificate in PEM format +- `certificate_chain` (String) SSL certificate chain of intermediates and root certificates in PEM format +- `name` (String) The name of the secret. +- `private_key` (String) SSL private key in PEM format + +### Optional + +- `expiration` (String) Datetime when the secret will expire. The format is 2025-12-28T19:14:44 +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `algorithm` (String) The encryption algorithm used for the secret. +- `bit_length` (Number) The bit length of the encryption algorithm. +- `content_types` (Map of String) The content types associated with the secret's payload. +- `created` (String) Datetime when the secret was created. The format is 2025-12-28T19:14:44.180394 +- `id` (String) The ID of this resource. +- `mode` (String) The mode of the encryption algorithm. +- `status` (String) The current status of the secret. + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_secret.secret_id 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/securitygroup.md b/docs/resources/securitygroup.md new file mode 100644 index 00000000..696e909f --- /dev/null +++ b/docs/resources/securitygroup.md @@ -0,0 +1,111 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_securitygroup Resource - edgecenter" +subcategory: "" +description: |- + Represent SecurityGroups(Firewall) +--- + +# edgecenter_securitygroup (Resource) + +Represent SecurityGroups(Firewall) + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_securitygroup" "sg" { + name = "test sg" + region_id = 1 + project_id = 1 + + security_group_rules { + direction = "egress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 19990 + port_range_max = 19990 + } + + security_group_rules { + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 19990 + port_range_max = 19990 + } + + security_group_rules { + direction = "egress" + ethertype = "IPv4" + protocol = "vrrp" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the security group. +- `security_group_rules` (Block Set, Min: 1) Firewall rules control what inbound(ingress) and outbound(egress) traffic is allowed to enter or leave a Instance. At least one 'egress' rule should be set (see [below for nested schema](#nestedblock--security_group_rules)) + +### Optional + +- `description` (String) A detailed description of the security group. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) + + +### Nested Schema for `security_group_rules` + +Required: + +- `direction` (String) Available value is 'ingress', 'egress' +- `ethertype` (String) Available value is 'IPv4', 'IPv6' +- `protocol` (String) Available value is udp,tcp,any,icmp,ah,dccp,egp,esp,gre,igmp,ospf,pgm,rsvp,sctp,udplite,vrrp,51,50,112,0,4,ipip,ipencap + +Optional: + +- `description` (String) +- `port_range_max` (Number) +- `port_range_min` (Number) +- `remote_ip_prefix` (String) + +Read-Only: + +- `created_at` (String) +- `id` (String) The ID of this resource. +- `updated_at` (String) + + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_securitygroup.securitygroup1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/servergroup.md b/docs/resources/servergroup.md new file mode 100644 index 00000000..714afa5b --- /dev/null +++ b/docs/resources/servergroup.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_servergroup Resource - edgecenter" +subcategory: "" +description: |- + Represent server group resource +--- + +# edgecenter_servergroup (Resource) + +Represent server group resource + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_servergroup" "default" { + name = "default" + policy = "affinity" + region_id = 1 + project_id = 1 +} +``` + + +## Schema + +### Required + +- `name` (String) Displayed server group name +- `policy` (String) Server group policy. Available value is 'affinity', 'anti-affinity' + +### Optional + +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `id` (String) The ID of this resource. +- `instances` (List of Object) Instances in this server group (see [below for nested schema](#nestedatt--instances)) + + +### Nested Schema for `instances` + +Read-Only: + +- `instance_id` (String) +- `instance_name` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_servergroup.servergroup1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/snapshot.md b/docs/resources/snapshot.md new file mode 100644 index 00000000..e15bafab --- /dev/null +++ b/docs/resources/snapshot.md @@ -0,0 +1,63 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_snapshot Resource - edgecenter" +subcategory: "" +description: |- + +--- + +# edgecenter_snapshot (Resource) + + + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_snapshot" "snapshot" { + project_id = 1 + region_id = 1 + name = "snapshot example" + volume_id = "28e9edcb-1593-41fe-971b-da729c6ec301" + description = "snapshot example description" + metadata = { + env = "test" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the snapshot. +- `volume_id` (String) The ID of the volume from which the snapshot was created. + +### Optional + +- `description` (String) A detailed description of the snapshot. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata` (Map of String) +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `id` (String) The ID of this resource. +- `size` (Number) The size of the snapshot in GB. +- `status` (String) The current status of the snapshot. + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_snapshot.snapshot1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/storage_s3.md b/docs/resources/storage_s3.md new file mode 100644 index 00000000..31a9e86d --- /dev/null +++ b/docs/resources/storage_s3.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_storage_s3 Resource - edgecenter" +subcategory: "" +description: |- + Represent s3 storage resource. https://storage.edgecenter.ru/storage/list +--- + +# edgecenter_storage_s3 (Resource) + +Represent s3 storage resource. https://storage.edgecenter.ru/storage/list + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_storage_s3" "example_s3" { + name = "example" + location = "s-ed1" +} +``` + + +## Schema + +### Required + +- `location` (String) A location of new storage resource. One of (s-ed1, s-darz1, s-ws1, s-dt2, s-drc2) +- `name` (String) A name of new storage resource. + +### Optional + +- `client_id` (Number) An client id of new storage resource. +- `generated_access_key` (String) A s3 access key for new storage resource. +- `generated_endpoint` (String) A s3 entry point for new storage resource. +- `generated_http_endpoint` (String) A http s3 entry point for new storage resource. +- `generated_s3_endpoint` (String) A s3 endpoint for new storage resource. +- `generated_secret_key` (String) A s3 secret key for new storage resource. +- `storage_id` (Number) An id of new storage resource. + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/storage_s3_bucket.md b/docs/resources/storage_s3_bucket.md new file mode 100644 index 00000000..436760c2 --- /dev/null +++ b/docs/resources/storage_s3_bucket.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_storage_s3_bucket Resource - edgecenter" +subcategory: "" +description: |- + Represent s3 storage bucket resource. https://storage.edgecenter.ru/storage/list +--- + +# edgecenter_storage_s3_bucket (Resource) + +Represent s3 storage bucket resource. https://storage.edgecenter.ru/storage/list + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_storage_s3_bucket" "example_s3_bucket" { + name = "example1bucket2name" + storage_id = 1 +} +``` + + +## Schema + +### Required + +- `name` (String) A name of new storage bucket resource. +- `storage_id` (Number) An id of existing storage resource. + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/subnet.md b/docs/resources/subnet.md new file mode 100644 index 00000000..a135fb1f --- /dev/null +++ b/docs/resources/subnet.md @@ -0,0 +1,102 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "edgecenter_subnet Resource - edgecenter" +subcategory: "" +description: |- + Represent subnets. Subnetwork is a range of IP addresses in a cloud network. Addresses from this range will be assigned to machines in the cloud +--- + +# edgecenter_subnet (Resource) + +Represent subnets. Subnetwork is a range of IP addresses in a cloud network. Addresses from this range will be assigned to machines in the cloud + +## Example Usage + +```terraform +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_network" "network" { + name = "network_example" + mtu = 1450 + type = "vxlan" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet" { + name = "subnet_example" + cidr = "192.168.10.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = var.dns_nameservers + + dynamic "host_routes" { + iterator = hr + for_each = var.host_routes + content { + destination = hr.value.destination + nexthop = hr.value.nexthop + } + } + + gateway_ip = "192.168.10.1" + region_id = 1 + project_id = 1 +} +``` + + +## Schema + +### Required + +- `cidr` (String) Represents the IP address range of the subnet. +- `name` (String) The name of the subnet. +- `network_id` (String) The ID of the network to which this subnet belongs. + +### Optional + +- `connect_to_network_router` (Boolean) True if the network's router should get a gateway in this subnet. Must be explicitly 'false' when gateway_ip is null. Default true. +- `dns_nameservers` (List of String) List of DNS name servers for the subnet. +- `enable_dhcp` (Boolean) Enable DHCP for this subnet. If true, DHCP will be used to assign IP addresses to instances within this subnet. +- `gateway_ip` (String) The IP address of the gateway for this subnet. +- `host_routes` (Block List) List of additional routes to be added to instances that are part of this subnet. (see [below for nested schema](#nestedblock--host_routes)) +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. + +### Read-Only + +- `id` (String) The ID of this resource. +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) + + +### Nested Schema for `host_routes` + +Required: + +- `destination` (String) +- `nexthop` (String) IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR + + + +### Nested Schema for `metadata_read_only` + +Read-Only: + +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# import using :: format +terraform import edgecenter_subnet.subnet1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/docs/resources/volume.md b/docs/resources/volume.md index 266589f2..26593b66 100644 --- a/docs/resources/volume.md +++ b/docs/resources/volume.md @@ -15,40 +15,19 @@ Volumes can be attached to a virtual machine and manipulated like a physical har ## Example Usage ```terraform -# Example 1 -resource "edgecenter_volume" "volume1" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume" - size = 20 - source = "new-volume" - volume_type = "ssd_hiiops" - instance_id_to_attach_to = "00000000-0000-0000-0000-000000000000" - attachment_tag = "test-tag" - metadata = { - "key1" : "value1", - "key2" : "value2", - } -} - -# Example 2 -resource "edgecenter_volume" "volume_image" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume-image" - size = 20 - source = "image" - image_id = "00000000-0000-0000-0000-000000000000" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 3 -resource "edgecenter_volume" "volume_snapshot" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume-snapshot" - size = 20 - source = "snapshot" - snapshot_id = "00000000-0000-0000-0000-000000000000" +resource "edgecenter_volume" "volume" { + name = "volume_example" + type_name = "standard" + size = 1 + region_id = 1 + project_id = 1 + metadata_map = { + tag1 = "tag1_value" + } } ``` @@ -57,38 +36,40 @@ resource "edgecenter_volume" "volume_snapshot" { ### Required -- `name` (String) name of the volume -- `project_id` (Number) uuid of the project -- `region_id` (Number) uuid of the region -- `size` (Number) size of the volume, specified in gigabytes (GB) -- `source` (String) volume source. valid values are 'new-volume', 'snapshot' or 'image' +- `name` (String) The name of the volume. +- `size` (Number) The size of the volume, specified in gigabytes (GB). ### Optional -- `attachment_tag` (String) the block device attachment tag (exposed in the metadata) -- `image_id` (String) ID of the image. this field is mandatory if creating a volume from an image -- `instance_id_to_attach_to` (String) VM’s instance_id to attach a newly created volume to -- `metadata` (Map of String) map containing metadata, for example tags. -- `snapshot_id` (String) ID of the snapshot. this field is mandatory if creating a volume from a snapshot -- `volume_type` (String) volume type with valid values. defaults to 'standard' +- `image_id` (String) (ForceNew) The ID of the image to create the volume from. This field is mandatory if creating a volume from an image. +- `last_updated` (String) The timestamp of the last update (use with update context). +- `metadata_map` (Map of String) A map containing metadata, for example tags. +- `project_id` (Number) The uuid of the project. Either 'project_id' or 'project_name' must be specified. +- `project_name` (String) The name of the project. Either 'project_id' or 'project_name' must be specified. +- `region_id` (Number) The uuid of the region. Either 'region_id' or 'region_name' must be specified. +- `region_name` (String) The name of the region. Either 'region_id' or 'region_name' must be specified. +- `snapshot_id` (String) (ForceNew) The ID of the snapshot to create the volume from. This field is mandatory if creating a volume from a snapshot. +- `type_name` (String) The type of volume to create. Valid values are 'ssd_hiiops', 'standard', 'cold', and 'ultra'. Defaults to 'standard'. ### Read-Only -- `attachments` (List of Object) the attachment list (see [below for nested schema](#nestedatt--attachments)) -- `bootable` (Boolean) the bootable boolean flag - `id` (String) The ID of this resource. -- `limiter_stats` (Map of Number) the QoS parameters of this volume -- `region` (String) name of the region -- `snapshot_ids` (List of String) snapshots of the volume -- `status` (String) status of the volume +- `metadata_read_only` (List of Object) A list of read-only metadata items, e.g. tags. (see [below for nested schema](#nestedatt--metadata_read_only)) - -### Nested Schema for `attachments` + +### Nested Schema for `metadata_read_only` Read-Only: -- `attachment_id` (String) -- `server_id` (String) -- `volume_id` (String) +- `key` (String) +- `read_only` (Boolean) +- `value` (String) + +## Import +Import is supported using the following syntax: +```shell +# import using :: format +terraform import edgecenter_volume.volume1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 +``` diff --git a/edgecenter/config/config.go b/edgecenter/config/config.go deleted file mode 100644 index 9bafe45d..00000000 --- a/edgecenter/config/config.go +++ /dev/null @@ -1,44 +0,0 @@ -package config - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -type Config struct { - TerraformVersion string - APIKey string - CloudAPIURL string -} - -type CombinedConfig struct { - cloud *edgecloud.Client -} - -func (c *CombinedConfig) EdgeCloudClient() *edgecloud.Client { return c.cloud } - -// Client returns a new client for accessing Edgecenter Cloud client. -func (c *Config) Client() (*CombinedConfig, diag.Diagnostics) { - userAgent := fmt.Sprintf("Terraform/%s", c.TerraformVersion) - - client, err := edgecloud.NewWithRetries(nil, - edgecloud.SetUserAgent(userAgent), - edgecloud.SetAPIKey(c.APIKey), - edgecloud.SetBaseURL(c.CloudAPIURL), - ) - if err != nil { - return nil, diag.FromErr(fmt.Errorf("edgecloud client create error: %w", err)) - } - - clientTransport := logging.NewSubsystemLoggingHTTPTransport("EdgeCenter", client.HTTPClient.Transport) - client.HTTPClient.Transport = clientTransport - - log.Printf("[INFO] EdgeCenter Client configured for URL: %s", client.BaseURL.String()) - - return &CombinedConfig{cloud: client}, nil -} diff --git a/edgecenter/converter/list.go b/edgecenter/converter/list.go deleted file mode 100644 index 9a518852..00000000 --- a/edgecenter/converter/list.go +++ /dev/null @@ -1,138 +0,0 @@ -package converter - -import ( - "net" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func ListInterfaceToListInstanceVolumeCreate(volumes []interface{}) ([]edgecloud.InstanceVolumeCreate, error) { - vols := make([]edgecloud.InstanceVolumeCreate, len(volumes)) - for i, volume := range volumes { - vol := volume.(map[string]interface{}) - var V edgecloud.InstanceVolumeCreate - if err := MapStructureDecoder(&V, &vol, decoderConfig); err != nil { - return nil, err - } - vols[i] = V - } - - return vols, nil -} - -func ListInterfaceToListInstanceInterface(interfaces []interface{}) ([]edgecloud.InstanceInterface, error) { - ifs := make([]edgecloud.InstanceInterface, len(interfaces)) - for idx, i := range interfaces { - inter := i.(map[string]interface{}) - I := edgecloud.InstanceInterface{ - Type: edgecloud.InterfaceType(inter["type"].(string)), - NetworkID: inter["network_id"].(string), - PortID: inter["port_id"].(string), - SubnetID: inter["subnet_id"].(string), - } - - switch inter["floating_ip_source"].(string) { - case "new": - I.FloatingIP = &edgecloud.InterfaceFloatingIP{ - Source: edgecloud.NewFloatingIP, - } - case "existing": - I.FloatingIP = &edgecloud.InterfaceFloatingIP{ - Source: edgecloud.ExistingFloatingIP, - ExistingFloatingID: inter["floating_ip"].(string), - } - default: - I.FloatingIP = nil - } - - sgList := inter["security_groups"].([]interface{}) - if len(sgList) > 0 { - sgs := make([]edgecloud.ID, 0, len(sgList)) - for _, sg := range sgList { - sgs = append(sgs, edgecloud.ID{ID: sg.(string)}) - } - I.SecurityGroups = sgs - } else { - I.SecurityGroups = []edgecloud.ID{} - } - - ifs[idx] = I - } - - return ifs, nil -} - -// ListInterfaceToLoadbalancerSessionPersistence creates a session persistence options struct. -func ListInterfaceToLoadbalancerSessionPersistence(sessionPersistence []interface{}) *edgecloud.LoadbalancerSessionPersistence { - var sp *edgecloud.LoadbalancerSessionPersistence - - if len(sessionPersistence) > 0 { - sessionPersistenceMap := sessionPersistence[0].(map[string]interface{}) - sp = &edgecloud.LoadbalancerSessionPersistence{ - Type: edgecloud.SessionPersistence(sessionPersistenceMap["type"].(string)), - } - - if granularity, ok := sessionPersistenceMap["persistence_granularity"].(string); ok { - sp.PersistenceGranularity = granularity - } - - if timeout, ok := sessionPersistenceMap["persistence_timeout"].(int); ok { - sp.PersistenceTimeout = timeout - } - - if cookieName, ok := sessionPersistenceMap["cookie_name"].(string); ok { - sp.CookieName = cookieName - } - } - - return sp -} - -// ListInterfaceToHealthMonitor creates a heath monitor options struct. -func ListInterfaceToHealthMonitor(healthMonitor []interface{}) edgecloud.HealthMonitorCreateRequest { - var hm edgecloud.HealthMonitorCreateRequest - - if len(healthMonitor) > 0 { - healthMonitorMap := healthMonitor[0].(map[string]interface{}) - hm = edgecloud.HealthMonitorCreateRequest{ - Timeout: healthMonitorMap["timeout"].(int), - Delay: healthMonitorMap["delay"].(int), - Type: edgecloud.HealthMonitorType(healthMonitorMap["type"].(string)), - MaxRetries: healthMonitorMap["max_retries"].(int), - } - - if httpMethod, ok := healthMonitorMap["http_method"].(string); ok { - hm.HTTPMethod = edgecloud.HTTPMethod(httpMethod) - } - - if urlPath, ok := healthMonitorMap["url_path"].(string); ok { - hm.URLPath = urlPath - } - - if maxRetriesDown, ok := healthMonitorMap["max_retries_down"].(int); ok { - hm.MaxRetriesDown = maxRetriesDown - } - - if expectedCodes, ok := healthMonitorMap["expected_codes"].(string); ok { - hm.ExpectedCodes = expectedCodes - } - } - - return hm -} - -func ListInterfaceToListPoolMember(poolMembers []interface{}) ([]edgecloud.PoolMemberCreateRequest, error) { - members := make([]edgecloud.PoolMemberCreateRequest, len(poolMembers)) - for i, member := range poolMembers { - m := member.(map[string]interface{}) - address := m["address"].(string) - m["address"] = net.ParseIP(address) - var M edgecloud.PoolMemberCreateRequest - if err := MapStructureDecoder(&M, &m, decoderConfig); err != nil { - return nil, err - } - members[i] = M - } - - return members, nil -} diff --git a/edgecenter/converter/map.go b/edgecenter/converter/map.go deleted file mode 100644 index aeb02b56..00000000 --- a/edgecenter/converter/map.go +++ /dev/null @@ -1,93 +0,0 @@ -package converter - -import ( - "fmt" - "reflect" - - "github.com/mitchellh/mapstructure" -) - -var decoderConfig = &mapstructure.DecoderConfig{TagName: "json"} - -// MapInterfaceToMapString converts a map[string]interface{} to map[string]string. -func MapInterfaceToMapString(m map[string]interface{}) map[string]string { - mapString := make(map[string]string) - - for key, value := range m { - mapString[key] = fmt.Sprintf("%v", value) - } - - return mapString -} - -// MapStructureDecoder decodes the given map into the provided structure using the specified decoder configuration. -func MapStructureDecoder(strct interface{}, v *map[string]interface{}, config *mapstructure.DecoderConfig) error { - config.Result = strct - decoder, err := mapstructure.NewDecoder(config) - if err != nil { - return fmt.Errorf("failed to create decoder: %w", err) - } - - return decoder.Decode(*v) -} - -// MapLeftDiff returns all elements in Left that are not in Right. -func MapLeftDiff(left, right map[string]struct{}) map[string]struct{} { - out := make(map[string]struct{}) - for l := range left { - if _, ok := right[l]; !ok { - out[l] = struct{}{} - } - } - - return out -} - -// MapsIntersection returns all elements in Left that are in Right. -func MapsIntersection(left, right map[string]struct{}) map[string]struct{} { - out := make(map[string]struct{}) - for l := range left { - if _, ok := right[l]; ok { - out[l] = struct{}{} - } - } - - return out -} - -// contains check if slice contains the element. -func contains[K comparable](slice []K, elm K) bool { - for _, s := range slice { - if s == elm { - return true - } - } - - return false -} - -func MapDifference(iMapOld, iMapNew map[string]interface{}, uncheckedKeys []string) map[string]interface{} { - differentFields := make(map[string]interface{}) - - for oldMapK, oldMapV := range iMapOld { - if contains(uncheckedKeys, oldMapK) { - continue - } - - if newMapV, ok := iMapNew[oldMapK]; !ok || !reflect.DeepEqual(newMapV, oldMapV) { - differentFields[oldMapK] = oldMapV - } - } - - for newMapK, newMapV := range iMapNew { - if contains(uncheckedKeys, newMapK) { - continue - } - - if _, ok := iMapOld[newMapK]; !ok { - differentFields[newMapK] = newMapV - } - } - - return differentFields -} diff --git a/edgecenter/data_source_edgecenter_floatingip.go b/edgecenter/data_source_edgecenter_floatingip.go new file mode 100644 index 00000000..868bc6a4 --- /dev/null +++ b/edgecenter/data_source_edgecenter_floatingip.go @@ -0,0 +1,187 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "net" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/floatingip/v1/floatingips" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" +) + +func dataSourceFloatingIP() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceFloatingIPRead, + Description: `A floating IP is a static IP address that can be associated with one of your instances or loadbalancers, +allowing it to have a static public IP address. The floating IP can be re-associated to any other instance in the same datacenter.`, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "floating_ip_address": { + Type: schema.TypeString, + Required: true, + Description: "The floating IP address assigned to the resource. It must be a valid IP address.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + ip := net.ParseIP(v) + if ip != nil { + return diag.Diagnostics{} + } + + return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) + }, + }, + "port_id": { + Type: schema.TypeString, + Optional: true, + Description: "The ID (uuid) of the network port that the floating IP is associated with.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the floating IP resource. Can be 'DOWN' or 'ACTIVE'.", + }, + "fixed_ip_address": { + Type: schema.TypeString, + Computed: true, + Description: "The fixed (reserved) IP address that is associated with the floating IP.", + }, + "router_id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID (uuid) of the router that the floating IP is associated with.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceFloatingIPRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start FloatingIP reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, FloatingIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + ipAddr := d.Get("floating_ip_address").(string) + metaOpts := &floatingips.ListOpts{} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + metaOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + metaOpts.MetadataKV = meta + } + + ips, err := floatingips.ListAll(client, *metaOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var floatingIP floatingips.FloatingIPDetail + for _, ip := range ips { + if ip.FloatingIPAddress.String() == ipAddr { + floatingIP = ip + found = true + break + } + } + + if !found { + return diag.Errorf("floatingIP %s not found", ipAddr) + } + + d.SetId(floatingIP.ID) + if floatingIP.FixedIPAddress != nil { + d.Set("fixed_ip_address", floatingIP.FixedIPAddress.String()) + } else { + d.Set("fixed_ip_address", "") + } + + d.Set("project_id", floatingIP.ProjectID) + d.Set("region_id", floatingIP.RegionID) + d.Set("status", floatingIP.Status) + d.Set("port_id", floatingIP.PortID) + d.Set("router_id", floatingIP.RouterID) + d.Set("floating_ip_address", floatingIP.FloatingIPAddress.String()) + + metadataReadOnly := PrepareMetadataReadonly(floatingIP.Metadata) + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish FloatingIP reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_image.go b/edgecenter/data_source_edgecenter_image.go new file mode 100644 index 00000000..82e81d05 --- /dev/null +++ b/edgecenter/data_source_edgecenter_image.go @@ -0,0 +1,187 @@ +package edgecenter + +import ( + "context" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/image/v1/images" +) + +const ( + ImagesPoint = "images" + bmImagesPoint = "bmimages" +) + +func dataSourceImage() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceImageRead, + Description: "A cloud image is a pre-configured virtual machine template that you can use to create new instances.", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Description: "The name of the image. Use 'os-version', for example 'ubuntu-20.04'.", + Required: true, + }, + "is_baremetal": { + Type: schema.TypeBool, + Description: "Set to true if need to get the baremetal image.", + Optional: true, + }, + "min_disk": { + Type: schema.TypeInt, + Computed: true, + Description: "Minimum disk space (in GB) required to launch an instance using this image.", + }, + "min_ram": { + Type: schema.TypeInt, + Computed: true, + Description: "Minimum VM RAM (in MB) required to launch an instance using this image.", + }, + "os_distro": { + Type: schema.TypeString, + Computed: true, + Description: "The distribution of the OS present in the image, e.g. Debian, CentOS, Ubuntu etc.", + }, + "os_version": { + Type: schema.TypeString, + Computed: true, + Description: "The version of the OS present in the image. e.g. 19.04 (for Ubuntu) or 9.4 for Debian.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "A detailed description of the image.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}.`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceImageRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Image reading") + name := d.Get("name").(string) + + config := m.(*Config) + provider := config.Provider + + point := ImagesPoint + if isBm, _ := d.Get("is_baremetal").(bool); isBm { + point = bmImagesPoint + } + client, err := CreateClient(provider, d, point, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + listOpts := &images.ListOpts{} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + listOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + typedMetadataKV := make(map[string]string, len(metadataRaw.(map[string]interface{}))) + for k, v := range metadataRaw.(map[string]interface{}) { + typedMetadataKV[k] = v.(string) + } + listOpts.MetadataKV = typedMetadataKV + } + + allImages, err := images.ListAll(client, *listOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var image images.Image + for _, img := range allImages { + if strings.HasPrefix(strings.ToLower(img.Name), strings.ToLower(name)) { + image = img + found = true + break + } + } + + if !found { + return diag.Errorf("image with name %s not found", name) + } + + d.SetId(image.ID) + d.Set("project_id", d.Get("project_id").(int)) + d.Set("region_id", d.Get("region_id").(int)) + d.Set("min_disk", image.MinDisk) + d.Set("min_ram", image.MinRAM) + d.Set("os_distro", image.OsDistro) + d.Set("os_version", image.OsVersion) + d.Set("description", image.Description) + + metadataReadOnly := PrepareMetadataReadonly(image.Metadata) + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish Image reading") + + return nil +} diff --git a/edgecenter/data_source_edgecenter_instance.go b/edgecenter/data_source_edgecenter_instance.go new file mode 100644 index 00000000..558f7ba5 --- /dev/null +++ b/edgecenter/data_source_edgecenter_instance.go @@ -0,0 +1,297 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/instances" +) + +func dataSourceInstance() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceInstanceRead, + Description: `A cloud instance is a virtual machine in a cloud environment. Could be used with baremetal also.`, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the instance.", + }, + "flavor_id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the flavor to be used for the instance, determining its compute and memory, for example 'g1-standard-2-4'.", + }, + "volume": { + Type: schema.TypeSet, + Computed: true, + Set: volumeUniqueID, + Description: "A set defining the volumes to be attached to the instance.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "volume_id": { + Type: schema.TypeString, + Computed: true, + }, + "delete_on_termination": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "interface": { + Type: schema.TypeList, + Computed: true, + Description: "A list defining the network interfaces to be attached to the instance.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "network_id": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + "port_id": { + Type: schema.TypeString, + Computed: true, + }, + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "security_group": { + Type: schema.TypeList, + Computed: true, + Description: "A list of firewall configurations applied to the instance, defined by their id and name.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "metadata": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "flavor": { + Type: schema.TypeMap, + Computed: true, + Description: `A map defining the flavor of the instance, for example, {"flavor_name": "g1-standard-2-4", "ram": 4096, ...}.`, + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the instance. This is computed automatically and can be used to track the instance's state.", + }, + "vm_state": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf(`The current virtual machine state of the instance, +allowing you to start or stop the VM. Possible values are %s and %s.`, InstanceVMStateStopped, InstanceVMStateActive), + }, + "addresses": { + Type: schema.TypeList, + Computed: true, + Description: `A list of network addresses associated with the instance, for example "pub_net": [...].`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "net": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "addr": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func dataSourceInstanceRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Instance reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + insts, err := instances.ListAll(client, instances.ListOpts{Name: name}) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var instance instances.Instance + for _, l := range insts { + if l.Name == name { + instance = l + found = true + break + } + } + + if !found { + return diag.Errorf("instance with name %s not found", name) + } + + d.SetId(instance.ID) + d.Set("name", instance.Name) + + d.Set("flavor_id", instance.Flavor.FlavorID) + d.Set("status", instance.Status) + d.Set("vm_state", instance.VMState) + + flavor := make(map[string]interface{}, 4) + flavor["flavor_id"] = instance.Flavor.FlavorID + flavor["flavor_name"] = instance.Flavor.FlavorName + flavor["ram"] = strconv.Itoa(instance.Flavor.RAM) + flavor["vcpus"] = strconv.Itoa(instance.Flavor.VCPUS) + d.Set("flavor", flavor) + + extVolumes := make([]interface{}, 0, len(instance.Volumes)) + for _, vol := range instance.Volumes { + v := make(map[string]interface{}) + v["volume_id"] = vol.ID + v["delete_on_termination"] = vol.DeleteOnTermination + extVolumes = append(extVolumes, v) + } + + if err := d.Set("volume", schema.NewSet(volumeUniqueID, extVolumes)); err != nil { + return diag.FromErr(err) + } + + ifs, err := instances.ListInterfacesAll(client, instance.ID) + log.Printf("instance data source interfaces: %+v", ifs) + if err != nil { + return diag.FromErr(err) + } + var cleanInterfaces []interface{} + for _, iface := range ifs { + if len(iface.IPAssignments) == 0 { + continue + } + + for _, assignment := range iface.IPAssignments { + subnetID := assignment.SubnetID + + i := make(map[string]interface{}) + + i["network_id"] = iface.NetworkID + i["subnet_id"] = subnetID + i["port_id"] = iface.PortID + i["ip_address"] = iface.IPAssignments[0].IPAddress.String() + + cleanInterfaces = append(cleanInterfaces, i) + } + } + if err := d.Set("interface", cleanInterfaces); err != nil { + return diag.FromErr(err) + } + + sliced := make([]map[string]interface{}, 0, len(instance.Metadata)) + for k, data := range instance.Metadata { + mdata := make(map[string]interface{}, 2) + mdata["key"] = k + mdata["value"] = data + sliced = append(sliced, mdata) + } + if err := d.Set("metadata", sliced); err != nil { + return diag.FromErr(err) + } + + secGrps := make([]map[string]interface{}, 0, len(instance.SecurityGroups)) + for _, sg := range instance.SecurityGroups { + i := make(map[string]interface{}) + i["name"] = sg.Name + secGrps = append(secGrps, i) + } + if err := d.Set("security_group", secGrps); err != nil { + return diag.FromErr(err) + } + + addresses := []map[string][]map[string]string{} + for _, data := range instance.Addresses { + d := map[string][]map[string]string{} + netd := make([]map[string]string, len(data)) + for i, iaddr := range data { + ndata := make(map[string]string, 2) + ndata["type"] = iaddr.Type.String() + ndata["addr"] = iaddr.Address.String() + netd[i] = ndata + } + d["net"] = netd + addresses = append(addresses, d) + } + if err := d.Set("addresses", addresses); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish Instance reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_k8s.go b/edgecenter/data_source_edgecenter_k8s.go new file mode 100644 index 00000000..11b392d2 --- /dev/null +++ b/edgecenter/data_source_edgecenter_k8s.go @@ -0,0 +1,330 @@ +package edgecenter + +import ( + "context" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/pools" +) + +func dataSourceK8s() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceK8sRead, + Description: "Represent k8s cluster with one default pool.", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "cluster_id": { + Type: schema.TypeString, + Required: true, + Description: "The uuid of the Kubernetes cluster.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the Kubernetes cluster.", + }, + "fixed_network": { + Type: schema.TypeString, + Computed: true, + Description: "Fixed network (uuid) associated with the Kubernetes cluster.", + }, + "fixed_subnet": { + Type: schema.TypeString, + Computed: true, + Description: "Subnet (uuid) associated with the fixed network.", + }, + "auto_healing_enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether auto-healing is enabled for the Kubernetes cluster.", + }, + "master_lb_floating_ip_enabled": { + Type: schema.TypeBool, + Computed: true, + Description: "Flag indicating if the master LoadBalancer should have a floating IP.", + }, + "keypair": { + Type: schema.TypeString, + Computed: true, + }, + "pool": { + Type: schema.TypeList, + Computed: true, + Description: "Configuration details of the node pool in the Kubernetes cluster.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "flavor_id": { + Type: schema.TypeString, + Computed: true, + }, + "min_node_count": { + Type: schema.TypeInt, + Computed: true, + }, + "max_node_count": { + Type: schema.TypeInt, + Computed: true, + }, + "node_count": { + Type: schema.TypeInt, + Computed: true, + }, + "docker_volume_type": { + Type: schema.TypeString, + Computed: true, + }, + "docker_volume_size": { + Type: schema.TypeInt, + Computed: true, + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + }, + "stack_id": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "node_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Total number of nodes in the Kubernetes cluster.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the Kubernetes cluster.", + }, + "status_reason": { + Type: schema.TypeString, + Computed: true, + Description: "The reason for the current status of the Kubernetes cluster, if ERROR.", + }, + "master_addresses": { + Type: schema.TypeList, + Computed: true, + Description: "List of IP addresses for master nodes in the Kubernetes cluster.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "node_addresses": { + Type: schema.TypeList, + Computed: true, + Description: "List of IP addresses for worker nodes in the Kubernetes cluster.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "container_version": { + Type: schema.TypeString, + Computed: true, + Description: "The container runtime version used in the Kubernetes cluster.", + }, + "api_address": { + Type: schema.TypeString, + Computed: true, + Description: "API endpoint address for the Kubernetes cluster.", + }, + "user_id": { + Type: schema.TypeString, + Computed: true, + Description: "User identifier associated with the Kubernetes cluster.", + }, + "discovery_url": { + Type: schema.TypeString, + Computed: true, + Description: "URL used for node discovery within the Kubernetes cluster.", + }, + "health_status": { + Type: schema.TypeString, + Computed: true, + Description: "Overall health status of the Kubernetes cluster.", + }, + "health_status_reason": { + Type: schema.TypeMap, + Computed: true, + }, + "faults": { + Type: schema.TypeMap, + Computed: true, + }, + "master_flavor_id": { + Type: schema.TypeString, + Computed: true, + Description: "Identifier for the master node flavor in the Kubernetes cluster.", + }, + "cluster_template_id": { + Type: schema.TypeString, + Computed: true, + Description: "Template identifier from which the Kubernetes cluster was instantiated.", + }, + "version": { + Type: schema.TypeString, + Computed: true, + Description: "The version of the Kubernetes cluster.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the Kubernetes cluster was updated.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the Kubernetes cluster was created.", + }, + "certificate_authority_data": { + Type: schema.TypeString, + Computed: true, + Description: "The certificate_authority_data field from the Kubernetes cluster config.", + }, + }, + } +} + +func dataSourceK8sRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clusterID := d.Get("cluster_id").(string) + cluster, err := clusters.Get(client, clusterID).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(cluster.UUID) + + d.Set("name", cluster.Name) + d.Set("fixed_network", cluster.FixedNetwork) + d.Set("fixed_subnet", cluster.FixedSubnet) + d.Set("master_lb_floating_ip_enabled", cluster.FloatingIPEnabled) + d.Set("keypair", cluster.KeyPair) + d.Set("node_count", cluster.NodeCount) + d.Set("status", cluster.Status) + d.Set("status_reason", cluster.StatusReason) + + masterAddresses := make([]string, len(cluster.MasterAddresses)) + for i, addr := range cluster.MasterAddresses { + masterAddresses[i] = addr.String() + } + if err := d.Set("master_addresses", masterAddresses); err != nil { + return diag.FromErr(err) + } + + nodeAddresses := make([]string, len(cluster.NodeAddresses)) + for i, addr := range cluster.NodeAddresses { + nodeAddresses[i] = addr.String() + } + if err := d.Set("node_addresses", nodeAddresses); err != nil { + return diag.FromErr(err) + } + + d.Set("container_version", cluster.ContainerVersion) + d.Set("api_address", cluster.APIAddress.String()) + d.Set("user_id", cluster.UserID) + d.Set("discovery_url", cluster.DiscoveryURL.String()) + + d.Set("health_status", cluster.HealthStatus) + if err := d.Set("health_status_reason", cluster.HealthStatusReason); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("faults", cluster.Faults); err != nil { + return diag.FromErr(err) + } + + d.Set("master_flavor_id", cluster.MasterFlavorID) + d.Set("cluster_template_id", cluster.ClusterTemplateID) + d.Set("version", cluster.Version) + d.Set("updated_at", cluster.UpdatedAt.Format(time.RFC850)) + d.Set("created_at", cluster.CreatedAt.Format(time.RFC850)) + + var pool pools.ClusterPool + for _, p := range cluster.Pools { + if p.IsDefault { + pool = p + } + } + + p := make(map[string]interface{}) + p["uuid"] = pool.UUID + p["name"] = pool.Name + p["flavor_id"] = pool.FlavorID + p["min_node_count"] = pool.MinNodeCount + p["max_node_count"] = pool.MaxNodeCount + p["node_count"] = pool.NodeCount + p["docker_volume_type"] = pool.DockerVolumeType.String() + p["docker_volume_size"] = pool.DockerVolumeSize + p["stack_id"] = pool.StackID + p["created_at"] = pool.CreatedAt.Format(time.RFC850) + + if err := d.Set("pool", []interface{}{p}); err != nil { + return diag.FromErr(err) + } + + getConfigResult, err := clusters.GetConfig(client, clusterID).Extract() + if err != nil { + return diag.FromErr(err) + } + + clusterConfig, err := parseK8sConfig(getConfigResult.Config) + if err != nil { + return diag.Errorf("failed to parse k8s config: %s", err) + } + + certificateAuthorityData := clusterConfig.Clusters[0].Cluster.CertificateAuthorityData + if err := d.Set("certificate_authority_data", certificateAuthorityData); err != nil { + return diag.Errorf("couldn't get certificate_authority_data: %s", err) + } + + log.Println("[DEBUG] Finish K8s reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_k8s_client_config.go b/edgecenter/data_source_edgecenter_k8s_client_config.go new file mode 100644 index 00000000..83aa7dcb --- /dev/null +++ b/edgecenter/data_source_edgecenter_k8s_client_config.go @@ -0,0 +1,99 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" +) + +func dataSourceK8sClientConfig() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceK8sReadClientConfig, + Description: "Represent k8s cluster with one default pool.", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "cluster_id": { + Type: schema.TypeString, + Required: true, + Description: "The uuid of the Kubernetes cluster.", + }, + "client_certificate_data": { + Type: schema.TypeString, + Computed: true, + Description: "The client_certificate_data field from k8s config.", + }, + "client_key_data": { + Type: schema.TypeString, + Computed: true, + Description: "The client_key_data field from k8s config.", + }, + }, + } +} + +func dataSourceK8sReadClientConfig(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s client config reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clusterID := d.Get("cluster_id").(string) + + d.SetId(clusterID) + + getConfigResult, err := clusters.GetConfig(client, clusterID).Extract() + if err != nil { + return diag.FromErr(err) + } + + clusterConfig, err := parseK8sConfig(getConfigResult.Config) + if err != nil { + return diag.Errorf("failed to parse k8s config: %s", err) + } + + clientCertificateData := clusterConfig.Users[0].User.ClientCertificateData + if err := d.Set("client_certificate_data", clientCertificateData); err != nil { + return diag.Errorf("couldn't get client_certificate_data: %s", err) + } + + clientKeyData := clusterConfig.Users[0].User.ClientKeyData + if err := d.Set("client_key_data", clientKeyData); err != nil { + return diag.Errorf("couldn't get client_key_data: %s", err) + } + + log.Println("[DEBUG] Finish K8s client config reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_k8s_pool.go b/edgecenter/data_source_edgecenter_k8s_pool.go new file mode 100644 index 00000000..9ac6255b --- /dev/null +++ b/edgecenter/data_source_edgecenter_k8s_pool.go @@ -0,0 +1,175 @@ +package edgecenter + +import ( + "context" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/pools" +) + +func dataSourceK8sPool() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceK8sPoolRead, + Description: "Represent k8s cluster's pool.", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "pool_id": { + Type: schema.TypeString, + Required: true, + Description: "The uuid of the Kubernetes pool within the cluster.", + }, + "cluster_id": { + Type: schema.TypeString, + Required: true, + Description: "The uuid of the Kubernetes cluster this pool belongs to.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the Kubernetes pool.", + }, + "is_default": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether this pool is the default pool in the cluster.", + }, + "flavor_id": { + Type: schema.TypeString, + Computed: true, + Description: "The identifier of the flavor used for nodes in this pool.", + }, + "min_node_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The minimum number of nodes in the pool.", + }, + "max_node_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The maximum number of nodes the pool can scale to.", + }, + "node_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The current number of nodes in the pool.", + }, + "docker_volume_type": { + Type: schema.TypeString, + Computed: true, + Description: "The type of volume used for the Docker containers. Available values are 'standard', 'ssd_hiiops', 'cold', and 'ultra'.", + }, + "docker_volume_size": { + Type: schema.TypeInt, + Computed: true, + Description: "The size of the volume used for Docker containers, in gigabytes.", + }, + "stack_id": { + Type: schema.TypeString, + Computed: true, + Description: "The identifier of the underlying infrastructure stack used by this pool.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the Kubernetes pool was created.", + }, + "node_addresses": { + Type: schema.TypeList, + Computed: true, + Description: "A list of IP addresses of nodes within the pool.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "node_names": { + Type: schema.TypeList, + Computed: true, + Description: "A list of names of nodes within the pool.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} + +func dataSourceK8sPoolRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s pool reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clusterID := d.Get("cluster_id").(string) + poolID := d.Get("pool_id").(string) + + pool, err := pools.Get(client, clusterID, poolID).Extract() + if err != nil { + return diag.FromErr(err) + } + d.SetId(pool.UUID) + + d.Set("name", pool.Name) + d.Set("cluster_id", clusterID) + d.Set("is_default", pool.IsDefault) + d.Set("flavor_id", pool.FlavorID) + d.Set("min_node_count", pool.MinNodeCount) + d.Set("max_node_count", pool.MaxNodeCount) + d.Set("node_count", pool.NodeCount) + d.Set("docker_volume_type", pool.DockerVolumeType.String()) + d.Set("docker_volume_size", pool.DockerVolumeSize) + d.Set("stack_id", pool.StackID) + d.Set("created_at", pool.CreatedAt.Format(time.RFC850)) + + nodeAddresses := make([]string, len(pool.NodeAddresses)) + for i, na := range pool.NodeAddresses { + nodeAddresses[i] = na.String() + } + d.Set("node_addresses", nodeAddresses) + + poolInstances, err := pools.InstancesAll(client, clusterID, poolID) + if err != nil { + return diag.FromErr(err) + } + + nodeNames := make([]string, len(poolInstances)) + for j, instance := range poolInstances { + nodeNames[j] = instance.Name + } + d.Set("node_names", nodeNames) + + log.Println("[DEBUG] Finish K8s pool reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_lblistener.go b/edgecenter/data_source_edgecenter_lblistener.go new file mode 100644 index 00000000..98239763 --- /dev/null +++ b/edgecenter/data_source_edgecenter_lblistener.go @@ -0,0 +1,139 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" +) + +func dataSourceLBListener() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceLBListenerRead, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer listener.", + }, + "loadbalancer_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The uuid for the load balancer.", + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + Description: "Available values is 'HTTP', 'HTTPS', 'TCP', 'UDP'", + }, + "protocol_port": { + Type: schema.TypeInt, + Computed: true, + Description: "The port on which the protocol is bound.", + }, + "pool_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of pools associated with the load balancer.", + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "The current operational status of the load balancer.", + }, + "provisioning_status": { + Type: schema.TypeString, + Computed: true, + Description: "The current provisioning status of the load balancer.", + }, + "allowed_cidrs": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Description: "The allowed CIDRs for listener.", + }, + }, + } +} + +func dataSourceLBListenerRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBListener reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var opts listeners.ListOpts + name := d.Get("name").(string) + lbID := d.Get("loadbalancer_id").(string) + if lbID != "" { + opts.LoadBalancerID = &lbID + } + + ls, err := listeners.ListAll(client, opts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var listener listeners.Listener + for _, l := range ls { + if l.Name == name { + listener = l + found = true + break + } + } + + if !found { + return diag.Errorf("lb listener with name %s not found", name) + } + + d.SetId(listener.ID) + d.Set("name", listener.Name) + d.Set("protocol", listener.Protocol.String()) + d.Set("protocol_port", listener.ProtocolPort) + d.Set("pool_count", listener.PoolCount) + d.Set("operating_status", listener.OperationStatus.String()) + d.Set("provisioning_status", listener.ProvisioningStatus.String()) + d.Set("loadbalancer_id", lbID) + d.Set("project_id", d.Get("project_id").(int)) + d.Set("region_id", d.Get("region_id").(int)) + d.Set("allowed_cidrs", listener.AllowedCIDRs) + + log.Println("[DEBUG] Finish LBListener reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_lbpool.go b/edgecenter/data_source_edgecenter_lbpool.go new file mode 100644 index 00000000..50e83634 --- /dev/null +++ b/edgecenter/data_source_edgecenter_lbpool.go @@ -0,0 +1,240 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/lbpools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" +) + +func dataSourceLBPool() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceLBPoolRead, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer pool.", + }, + "lb_algorithm": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available values is '%s', '%s', '%s', '%s'", types.LoadBalancerAlgorithmRoundRobin, types.LoadBalancerAlgorithmLeastConnections, types.LoadBalancerAlgorithmSourceIP, types.LoadBalancerAlgorithmSourceIPPort), + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available values is '%s' (currently work, other do not work on ed-8), '%s', '%s', '%s'", types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP), + }, + "loadbalancer_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The uuid for the load balancer.", + }, + "listener_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The uuid for the load balancer listener.", + }, + "health_monitor": { + Type: schema.TypeList, + Computed: true, + Description: `Configuration for health checks to test the health and state of the backend members. +It determines how the load balancer identifies whether the backend members are healthy or unhealthy.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available values is '%s', '%s', '%s', '%s', '%s', '%s", types.HealthMonitorTypeHTTP, types.HealthMonitorTypeHTTPS, types.HealthMonitorTypePING, types.HealthMonitorTypeTCP, types.HealthMonitorTypeTLSHello, types.HealthMonitorTypeUDPConnect), + }, + "delay": { + Type: schema.TypeInt, + Computed: true, + }, + "max_retries": { + Type: schema.TypeInt, + Computed: true, + }, + "timeout": { + Type: schema.TypeInt, + Computed: true, + }, + "max_retries_down": { + Type: schema.TypeInt, + Computed: true, + }, + "http_method": { + Type: schema.TypeString, + Computed: true, + }, + "url_path": { + Type: schema.TypeString, + Computed: true, + }, + "expected_codes": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "session_persistence": { + Type: schema.TypeList, + Computed: true, + Description: `Configuration that enables the load balancer to bind a user's session to a specific backend member. +This ensures that all requests from the user during the session are sent to the same member.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Computed: true, + }, + "cookie_name": { + Type: schema.TypeString, + Computed: true, + }, + "persistence_granularity": { + Type: schema.TypeString, + Computed: true, + }, + "persistence_timeout": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceLBPoolRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBPool reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var opts lbpools.ListOpts + name := d.Get("name").(string) + lbID := d.Get("loadbalancer_id").(string) + if lbID != "" { + opts.LoadBalancerID = &lbID + } + lID := d.Get("listener_id").(string) + if lbID != "" { + opts.ListenerID = &lID + } + + pools, err := lbpools.ListAll(client, opts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var lb lbpools.Pool + for _, p := range pools { + if p.Name == name { + lb = p + found = true + break + } + } + + if !found { + return diag.Errorf("lb listener with name %s not found", name) + } + + d.SetId(lb.ID) + d.Set("name", lb.Name) + d.Set("lb_algorithm", lb.LoadBalancerAlgorithm.String()) + d.Set("protocol", lb.Protocol.String()) + + if len(lb.LoadBalancers) > 0 { + d.Set("loadbalancer_id", lb.LoadBalancers[0].ID) + } + + if len(lb.Listeners) > 0 { + d.Set("listener_id", lb.Listeners[0].ID) + } + + if lb.HealthMonitor != nil { + healthMonitor := map[string]interface{}{ + "id": lb.HealthMonitor.ID, + "type": lb.HealthMonitor.Type.String(), + "delay": lb.HealthMonitor.Delay, + "timeout": lb.HealthMonitor.Timeout, + "max_retries": lb.HealthMonitor.MaxRetries, + "max_retries_down": lb.HealthMonitor.MaxRetriesDown, + "url_path": lb.HealthMonitor.URLPath, + "expected_codes": lb.HealthMonitor.ExpectedCodes, + } + if lb.HealthMonitor.HTTPMethod != nil { + healthMonitor["http_method"] = lb.HealthMonitor.HTTPMethod.String() + } + + if err := d.Set("health_monitor", []interface{}{healthMonitor}); err != nil { + return diag.FromErr(err) + } + } + + if lb.SessionPersistence != nil { + sessionPersistence := map[string]interface{}{ + "type": lb.SessionPersistence.Type.String(), + "cookie_name": lb.SessionPersistence.CookieName, + "persistence_granularity": lb.SessionPersistence.PersistenceGranularity, + "persistence_timeout": lb.SessionPersistence.PersistenceTimeout, + } + + if err := d.Set("session_persistence", []interface{}{sessionPersistence}); err != nil { + return diag.FromErr(err) + } + } + + d.Set("project_id", d.Get("project_id").(int)) + d.Set("region_id", d.Get("region_id").(int)) + + log.Println("[DEBUG] Finish LBPool reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_loadbalancer.go b/edgecenter/data_source_edgecenter_loadbalancer.go new file mode 100644 index 00000000..7c1c3195 --- /dev/null +++ b/edgecenter/data_source_edgecenter_loadbalancer.go @@ -0,0 +1,205 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" +) + +func dataSourceLoadBalancer() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceLoadBalancerRead, + DeprecationMessage: "!> **WARNING:** This data-source is deprecated and will be removed in the next major version. Use edgecenter_loadbalancerv2 data-source instead", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the router.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "vip_address": { + Type: schema.TypeString, + Computed: true, + }, + "vip_port_id": { + Type: schema.TypeString, + Computed: true, + }, + "listener": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available values is '%s' (currently work, other do not work on ed-8), '%s', '%s', '%s'", types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP), + }, + "protocol_port": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceLoadBalancerRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + metaOpts := &loadbalancers.ListOpts{} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + metaOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + metaOpts.MetadataKV = meta + } + + lbs, err := loadbalancers.ListAll(client, *metaOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var lb loadbalancers.LoadBalancer + for _, l := range lbs { + if l.Name == name { + lb = l + found = true + break + } + } + + if !found { + return diag.Errorf("load balancer with name %s not found", name) + } + + d.SetId(lb.ID) + d.Set("project_id", lb.ProjectID) + d.Set("region_id", lb.RegionID) + d.Set("name", lb.Name) + d.Set("vip_address", lb.VipAddress.String()) + d.Set("vip_port_id", lb.VipPortID) + + metadataReadOnly := PrepareMetadataReadonly(lb.Metadata) + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + listenersClient, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + newListeners := make([]map[string]interface{}, len(lb.Listeners)) + for i, l := range lb.Listeners { + listener, err := listeners.Get(listenersClient, l.ID).Extract() + if err != nil { + return diag.FromErr(err) + } + + newListeners[i] = map[string]interface{}{ + "id": listener.ID, + "name": listener.Name, + "protocol": listener.Protocol.String(), + "protocol_port": listener.ProtocolPort, + } + } + if err := d.Set("listener", newListeners); err != nil { + diag.FromErr(err) + } + + log.Println("[DEBUG] Finish LoadBalancer reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_loadbalancerv2.go b/edgecenter/data_source_edgecenter_loadbalancerv2.go new file mode 100644 index 00000000..7515fefa --- /dev/null +++ b/edgecenter/data_source_edgecenter_loadbalancerv2.go @@ -0,0 +1,172 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +func dataSourceLoadBalancerV2() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceLoadBalancerV2Read, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "vip_address": { + Type: schema.TypeString, + Computed: true, + Description: "Load balancer IP address", + }, + "vip_port_id": { + Type: schema.TypeString, + Computed: true, + Description: "Attached reserved IP.", + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceLoadBalancerV2Read(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + + metaOpts := &loadbalancers.ListOpts{} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + metaOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + metaOpts.MetadataKV = meta + } + + lbs, err := loadbalancers.ListAll(client, *metaOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var lb loadbalancers.LoadBalancer + for _, l := range lbs { + if l.Name == name { + lb = l + found = true + break + } + } + + if !found { + return diag.Errorf("load balancer with name %s not found", name) + } + + d.SetId(lb.ID) + d.Set("project_id", lb.ProjectID) + d.Set("region_id", lb.RegionID) + d.Set("name", lb.Name) + d.Set("vip_address", lb.VipAddress.String()) + d.Set("vip_port_id", lb.VipPortID) + + metadataList, err := metadata.ResourceMetadataListAll(client, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + metadataReadOnly := make([]map[string]interface{}, 0, len(metadataList)) + if len(metadataList) > 0 { + for _, metadataItem := range metadataList { + metadataReadOnly = append(metadataReadOnly, map[string]interface{}{ + "key": metadataItem.Key, + "value": metadataItem.Value, + "read_only": metadataItem.ReadOnly, + }) + } + } + + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish LoadBalancer reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_network.go b/edgecenter/data_source_edgecenter_network.go new file mode 100644 index 00000000..63f4e216 --- /dev/null +++ b/edgecenter/data_source_edgecenter_network.go @@ -0,0 +1,278 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/availablenetworks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +func dataSourceNetwork() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceNetworkRead, + Description: "Represent network. A network is a software-defined network in a cloud computing infrastructure", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the network.", + }, + "shared_with_subnets": { + Type: schema.TypeBool, + Optional: true, + Description: "Get shared networks with details of subnets.", + }, + "mtu": { + Type: schema.TypeInt, + Computed: true, + Description: "Maximum Transmission Unit (MTU) for the network. It determines the maximum packet size that can be transmitted without fragmentation.", + }, + "type": { + Type: schema.TypeString, + Computed: true, + Description: "'vlan' or 'vxlan' network type is allowed. Default value is 'vxlan'", + }, + "external": { + Type: schema.TypeBool, + Computed: true, + }, + "shared": { + Type: schema.TypeBool, + Computed: true, + }, + "subnets": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the subnet.", + }, + "name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the subnet.", + }, + "available_ips": { + Type: schema.TypeInt, + Computed: true, + Description: "The number of available IPs in the subnet.", + }, + "total_ips": { + Type: schema.TypeInt, + Computed: true, + Description: "The total number of IPs in the subnet.", + }, + "enable_dhcp": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable DHCP for this subnet. If true, DHCP will be used to assign IP addresses to instances within this subnet.", + }, + "has_router": { + Type: schema.TypeBool, + Computed: true, + Description: "Indicates whether the subnet has a router attached to it.", + }, + "cidr": { + Type: schema.TypeString, + Computed: true, + Description: "Represents the IP address range of the subnet.", + }, + "dns_nameservers": { + Type: schema.TypeList, + Computed: true, + Description: "List of DNS name servers for the subnet.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "host_routes": { + Type: schema.TypeList, + Computed: true, + Description: "List of additional routes to be added to instances that are part of this subnet.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination": { + Type: schema.TypeString, + Computed: true, + }, + "nexthop": { + Type: schema.TypeString, + Computed: true, + Description: "IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR", + }, + }, + }, + }, + "gateway_ip": { + Type: schema.TypeString, + Computed: true, + Description: "The IP address of the gateway for this subnet.", + }, + }, + }, + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceNetworkRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Network reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, NetworksPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + clientShared, err := CreateClient(provider, d, SharedNetworksPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + metaOpts := &networks.ListOpts{} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + metaOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + typedMetadataKV := make(map[string]string, len(metadataRaw.(map[string]interface{}))) + for k, v := range metadataRaw.(map[string]interface{}) { + typedMetadataKV[k] = v.(string) + } + metaOpts.MetadataKV = typedMetadataKV + } + + var ( + withDetails = d.Get("shared_with_subnets").(bool) + rawNetwork map[string]interface{} + subs []subnets.Subnet + meta []metadata.Metadata + ) + + if !withDetails { + nets, err := networks.ListAll(client, *metaOpts) + if err != nil { + return diag.FromErr(err) + } + network, found := findNetworkByName(name, nets) + if !found { + return diag.Errorf("network with name %s not found. you can try to set 'shared_with_subnets' parameter", name) + } + meta = network.Metadata + rawNetwork, err = StructToMap(network) + if err != nil { + return diag.FromErr(err) + } + } else { + nets, err := availablenetworks.ListAll(clientShared, nil) + if err != nil { + return diag.FromErr(err) + } + sharedNetwork, found := findSharedNetworkByName(name, nets) + if !found { + return diag.Errorf("shared network with name %s not found", name) + } + subs = sharedNetwork.Subnets + rawNetwork, err = StructToMap(sharedNetwork) + if err != nil { + return diag.FromErr(err) + } + } + + d.SetId(rawNetwork["id"].(string)) + d.Set("name", rawNetwork["name"]) + d.Set("mtu", rawNetwork["mtu"]) + d.Set("type", rawNetwork["type"]) + d.Set("region_id", rawNetwork["region_id"]) + d.Set("project_id", rawNetwork["project_id"]) + d.Set("external", rawNetwork["external"]) + d.Set("shared", rawNetwork["shared"]) + if withDetails { + if len(subs) > 0 { + if err := d.Set("subnets", prepareSubnets(subs)); err != nil { + return diag.FromErr(err) + } + } + } else { + metadataReadOnly := PrepareMetadataReadonly(meta) + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + } + + log.Println("[DEBUG] Finish Network reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_project.go b/edgecenter/data_source_edgecenter_project.go new file mode 100644 index 00000000..faac615d --- /dev/null +++ b/edgecenter/data_source_edgecenter_project.go @@ -0,0 +1,42 @@ +package edgecenter + +import ( + "context" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceProject() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceProjectRead, + Description: "Represent project data", + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "Displayed project name", + Required: true, + }, + }, + } +} + +func dataSourceProjectRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Project reading") + name := d.Get("name").(string) + config := m.(*Config) + provider := config.Provider + projectID, err := GetProject(provider, 0, name) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(projectID)) + d.Set("name", name) + + log.Println("[DEBUG] Finish Project reading") + + return nil +} diff --git a/edgecenter/data_source_edgecenter_region.go b/edgecenter/data_source_edgecenter_region.go new file mode 100644 index 00000000..dd6494cb --- /dev/null +++ b/edgecenter/data_source_edgecenter_region.go @@ -0,0 +1,43 @@ +package edgecenter + +import ( + "context" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceRegion() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceRegionRead, + Description: "Represent region data", + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "Displayed region name", + Required: true, + }, + }, + } +} + +func dataSourceRegionRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Region reading") + + name := d.Get("name").(string) + config := m.(*Config) + provider := config.Provider + regionID, err := GetRegion(provider, 0, name) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(strconv.Itoa(regionID)) + d.Set("name", name) + + log.Println("[DEBUG] Finish Region reading") + + return nil +} diff --git a/edgecenter/data_source_edgecenter_reservedfixedip.go b/edgecenter/data_source_edgecenter_reservedfixedip.go new file mode 100644 index 00000000..d425b806 --- /dev/null +++ b/edgecenter/data_source_edgecenter_reservedfixedip.go @@ -0,0 +1,163 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "net" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/reservedfixedip/v1/reservedfixedips" +) + +func dataSourceReservedFixedIP() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceReservedFixedIPRead, + Description: "Represent reserved ips", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "fixed_ip_address": { + Type: schema.TypeString, + Required: true, + Description: "The IP address that is associated with the reserved IP.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + ip := net.ParseIP(v) + if ip != nil { + return diag.Diagnostics{} + } + + return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) + }, + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the reserved fixed IP.", + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the subnet from which the fixed IP should be reserved.", + }, + "network_id": { + Type: schema.TypeString, + Computed: true, + Description: "ID of the network to which the reserved fixed IP is associated.", + }, + "is_vip": { + Type: schema.TypeBool, + Computed: true, + Description: "Flag to determine if the reserved fixed IP should be treated as a Virtual IP (VIP).", + }, + "port_id": { + Type: schema.TypeString, + Description: "ID of the port_id underlying the reserved fixed IP", + Computed: true, + }, + "allowed_address_pairs": { + Type: schema.TypeList, + Computed: true, + Description: "Group of IP addresses that share the current IP as VIP.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "mac_address": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceReservedFixedIPRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ReservedFixedIP reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ReservedFixedIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + ipAddr := d.Get("fixed_ip_address").(string) + ips, err := reservedfixedips.ListAll(client, reservedfixedips.ListOpts{}) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var reservedFixedIP reservedfixedips.ReservedFixedIP + for _, ip := range ips { + if ip.FixedIPAddress.String() == ipAddr { + reservedFixedIP = ip + found = true + break + } + } + + if !found { + return diag.Errorf("reserved fixed ip %s not found", ipAddr) + } + + // should we use PortID as id? + d.SetId(reservedFixedIP.PortID) + d.Set("project_id", reservedFixedIP.ProjectID) + d.Set("region_id", reservedFixedIP.RegionID) + d.Set("status", reservedFixedIP.Status) + d.Set("fixed_ip_address", reservedFixedIP.FixedIPAddress.String()) + d.Set("subnet_id", reservedFixedIP.SubnetID) + d.Set("network_id", reservedFixedIP.NetworkID) + d.Set("is_vip", reservedFixedIP.IsVip) + d.Set("port_id", reservedFixedIP.PortID) + + allowedPairs := make([]map[string]interface{}, len(reservedFixedIP.AllowedAddressPairs)) + for i, p := range reservedFixedIP.AllowedAddressPairs { + pair := make(map[string]interface{}) + + pair["ip_address"] = p.IPAddress + pair["mac_address"] = p.MacAddress + + allowedPairs[i] = pair + } + if err := d.Set("allowed_address_pairs", allowedPairs); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish ReservedFixedIP reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_router.go b/edgecenter/data_source_edgecenter_router.go new file mode 100644 index 00000000..f91f9db0 --- /dev/null +++ b/edgecenter/data_source_edgecenter_router.go @@ -0,0 +1,220 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/router/v1/routers" +) + +func dataSourceRouter() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceRouterRead, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load router.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the router resource.", + }, + "external_gateway_info": { + Type: schema.TypeList, + Computed: true, + Description: "Information related to the external gateway.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enable_snat": { + Type: schema.TypeBool, + Computed: true, + }, + "network_id": { + Type: schema.TypeString, + Computed: true, + }, + "external_fixed_ips": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "interfaces": { + Type: schema.TypeList, + Computed: true, + Description: "Set of interfaces associated with the router.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port_id": { + Type: schema.TypeString, + Computed: true, + }, + "network_id": { + Type: schema.TypeString, + Computed: true, + }, + "mac_address": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "routes": { + Type: schema.TypeList, + Computed: true, + Description: "List of static routes to be applied to the router.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination": { + Type: schema.TypeString, + Computed: true, + }, + "nexthop": { + Type: schema.TypeString, + Computed: true, + Description: "IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR", + }, + }, + }, + }, + }, + } +} + +func dataSourceRouterRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Router reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, RouterPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + rs, err := routers.ListAll(client, routers.ListOpts{}) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var router routers.Router + for _, r := range rs { + if r.Name == name { + router = r + found = true + break + } + } + + if !found { + return diag.Errorf("router with name %s not found", name) + } + + d.SetId(router.ID) + d.Set("name", router.Name) + d.Set("status", router.Status) + + if len(router.ExternalGatewayInfo.ExternalFixedIPs) > 0 { + egi := make(map[string]interface{}, 4) + egilst := make([]map[string]interface{}, 1) + egi["enable_snat"] = router.ExternalGatewayInfo.EnableSNat + egi["network_id"] = router.ExternalGatewayInfo.NetworkID + + efip := make([]map[string]string, len(router.ExternalGatewayInfo.ExternalFixedIPs)) + for i, fip := range router.ExternalGatewayInfo.ExternalFixedIPs { + tmpfip := make(map[string]string, 1) + tmpfip["ip_address"] = fip.IPAddress + tmpfip["subnet_id"] = fip.SubnetID + efip[i] = tmpfip + } + egi["external_fixed_ips"] = efip + + egilst[0] = egi + d.Set("external_gateway_info", egilst) + } + + ifs := make([]map[string]interface{}, 0, len(router.Interfaces)) + for _, iface := range router.Interfaces { + for _, subnet := range iface.IPAssignments { + smap := make(map[string]interface{}, 6) + smap["port_id"] = iface.PortID + smap["network_id"] = iface.NetworkID + smap["mac_address"] = iface.MacAddress.String() + smap["type"] = "subnet" + smap["subnet_id"] = subnet.SubnetID + smap["ip_address"] = subnet.IPAddress.String() + ifs = append(ifs, smap) + } + } + d.Set("interfaces", ifs) + + rss := make([]map[string]string, len(router.Routes)) + for i, r := range router.Routes { + rmap := make(map[string]string, 2) + rmap["destination"] = r.Destination.String() + rmap["nexthop"] = r.NextHop.String() + rss[i] = rmap + } + d.Set("routes", rss) + + log.Println("[DEBUG] Finish router reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_secret.go b/edgecenter/data_source_edgecenter_secret.go new file mode 100644 index 00000000..55ea4484 --- /dev/null +++ b/edgecenter/data_source_edgecenter_secret.go @@ -0,0 +1,136 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/secret/v1/secrets" +) + +func dataSourceSecret() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceSecretRead, + Description: "Represent secret", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the secret.", + }, + "algorithm": { + Type: schema.TypeString, + Computed: true, + Description: "The encryption algorithm used for the secret.", + }, + "bit_length": { + Type: schema.TypeInt, + Computed: true, + Description: "The bit length of the encryption algorithm.", + }, + "mode": { + Type: schema.TypeString, + Computed: true, + Description: "The mode of the encryption algorithm.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the secret.", + }, + "content_types": { + Type: schema.TypeMap, + Computed: true, + Description: "The content types associated with the secret's payload.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "expiration": { + Type: schema.TypeString, + Description: "Datetime when the secret will expire. The format is 2025-12-28T19:14:44.180394", + Computed: true, + }, + "created": { + Type: schema.TypeString, + Description: "Datetime when the secret was created. The format is 2025-12-28T19:14:44.180394", + Computed: true, + }, + }, + } +} + +func dataSourceSecretRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start secret reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + secretID := d.Id() + log.Printf("[DEBUG] Secret id = %s", secretID) + + client, err := CreateClient(provider, d, SecretPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + allSecrets, err := secrets.ListAll(client) + if err != nil { + return diag.Errorf("cannot get secrets. Error: %s", err.Error()) + } + + var found bool + name := d.Get("name").(string) + for _, secret := range allSecrets { + if name == secret.Name { + d.SetId(secret.ID) + d.Set("name", name) + d.Set("algorithm", secret.Algorithm) + d.Set("bit_length", secret.BitLength) + d.Set("mode", secret.Mode) + d.Set("status", secret.Status) + d.Set("expiration", secret.Expiration.Format(edgecloud.RFC3339ZColon)) + d.Set("created", secret.CreatedAt.Format(edgecloud.RFC3339ZColon)) + if err := d.Set("content_types", secret.ContentTypes); err != nil { + return diag.FromErr(err) + } + found = true + + break + } + } + + if !found { + return diag.Errorf("secret with name %s does not exit", name) + } + + log.Println("[DEBUG] Finish secret reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_securitygroup.go b/edgecenter/data_source_edgecenter_securitygroup.go new file mode 100644 index 00000000..47ee12d2 --- /dev/null +++ b/edgecenter/data_source_edgecenter_securitygroup.go @@ -0,0 +1,259 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygroups" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/types" +) + +func dataSourceSecurityGroup() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceSecurityGroupRead, + Description: "Represent SecurityGroups(Firewall)", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the security group.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "A detailed description of the security group.", + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "security_group_rules": { + Type: schema.TypeSet, + Computed: true, + Description: "Firewall rules control what inbound(ingress) and outbound(egress) traffic is allowed to enter or leave a Instance. At least one 'egress' rule should be set", + Set: secGroupUniqueID, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "direction": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available value is '%s', '%s'", types.RuleDirectionIngress, types.RuleDirectionEgress), + }, + "ethertype": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available value is '%s', '%s'", types.EtherTypeIPv4, types.EtherTypeIPv6), + }, + "protocol": { + Type: schema.TypeString, + Computed: true, + Description: fmt.Sprintf("Available value is %s", strings.Join(types.Protocol("").StringList(), ",")), + }, + "port_range_min": { + Type: schema.TypeInt, + Computed: true, + }, + "port_range_max": { + Type: schema.TypeInt, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "remote_ip_prefix": { + Type: schema.TypeString, + Computed: true, + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceSecurityGroupRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start SecurityGroup reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + metaOpts := &securitygroups.ListOpts{} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + metaOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + typedMetadataKV := make(map[string]string, len(metadataRaw.(map[string]interface{}))) + for k, v := range metadataRaw.(map[string]interface{}) { + typedMetadataKV[k] = v.(string) + } + metaOpts.MetadataKV = typedMetadataKV + } + sgs, err := securitygroups.ListAll(client, *metaOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var sg securitygroups.SecurityGroup + for _, s := range sgs { + if s.Name == name { + sg = s + found = true + break + } + } + + if !found { + return diag.Errorf("security group with name %s not found", name) + } + + d.SetId(sg.ID) + d.Set("project_id", sg.ProjectID) + d.Set("region_id", sg.RegionID) + d.Set("name", sg.Name) + d.Set("description", sg.Description) + + metadataReadOnly := make([]map[string]interface{}, 0, len(sg.Metadata)) + if len(sg.Metadata) > 0 { + for _, metadataItem := range sg.Metadata { + metadataReadOnly = append(metadataReadOnly, map[string]interface{}{ + "key": metadataItem.Key, + "value": metadataItem.Value, + "read_only": metadataItem.ReadOnly, + }) + } + } + + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + newSgRules := make([]interface{}, len(sg.SecurityGroupRules)) + for i, sgr := range sg.SecurityGroupRules { + r := make(map[string]interface{}) + r["id"] = sgr.ID + r["direction"] = sgr.Direction.String() + + r["ethertype"] = "" + if sgr.EtherType != nil { + r["ethertype"] = sgr.EtherType.String() + } + + r["protocol"] = types.ProtocolAny.String() + if sgr.Protocol != nil { + r["protocol"] = sgr.Protocol.String() + } + + r["port_range_max"] = 65535 + if sgr.PortRangeMax != nil { + r["port_range_max"] = *sgr.PortRangeMax + } + + r["port_range_min"] = 1 + if sgr.PortRangeMin != nil { + r["port_range_min"] = *sgr.PortRangeMin + } + + r["description"] = "" + if sgr.Description != nil { + r["description"] = *sgr.Description + } + + r["remote_ip_prefix"] = "" + if sgr.RemoteIPPrefix != nil { + r["remote_ip_prefix"] = *sgr.RemoteIPPrefix + } + + r["updated_at"] = sgr.UpdatedAt.String() + r["created_at"] = sgr.CreatedAt.String() + + newSgRules[i] = r + } + + if err := d.Set("security_group_rules", schema.NewSet(secGroupUniqueID, newSgRules)); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish SecurityGroup reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_servergroup.go b/edgecenter/data_source_edgecenter_servergroup.go new file mode 100644 index 00000000..f09547c0 --- /dev/null +++ b/edgecenter/data_source_edgecenter_servergroup.go @@ -0,0 +1,123 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/servergroup/v1/servergroups" +) + +func dataSourceServerGroup() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceServerGroupRead, + Description: "Represent server group data", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Description: "The name of the server group.", + Required: true, + }, + "policy": { + Type: schema.TypeString, + Description: "Server group policy. Available value is 'affinity', 'anti-affinity'", + Computed: true, + }, + "instances": { + Type: schema.TypeList, + Description: "Instances in this server group", + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "instance_id": { + Type: schema.TypeString, + Computed: true, + }, + "instance_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceServerGroupRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ServerGroup reading") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ServerGroupsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var serverGroup servergroups.ServerGroup + serverGroups, err := servergroups.ListAll(client) + if err != nil { + return diag.FromErr(err) + } + + var found bool + name := d.Get("name").(string) + for _, sg := range serverGroups { + if sg.Name == name { + serverGroup = sg + found = true + break + } + } + + if !found { + return diag.Errorf("server group with name %s not found", name) + } + + d.SetId(serverGroup.ServerGroupID) + d.Set("name", name) + d.Set("project_id", serverGroup.ProjectID) + d.Set("region_id", serverGroup.RegionID) + d.Set("policy", serverGroup.Policy.String()) + + instances := make([]map[string]string, len(serverGroup.Instances)) + for i, instance := range serverGroup.Instances { + rawInstance := make(map[string]string) + rawInstance["instance_id"] = instance.InstanceID + rawInstance["instance_name"] = instance.InstanceName + instances[i] = rawInstance + } + if err := d.Set("instances", instances); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish ServerGroup reading") + + return nil +} diff --git a/edgecenter/data_source_edgecenter_storage_s3.go b/edgecenter/data_source_edgecenter_storage_s3.go new file mode 100644 index 00000000..1e722527 --- /dev/null +++ b/edgecenter/data_source_edgecenter_storage_s3.go @@ -0,0 +1,68 @@ +package edgecenter + +import ( + "regexp" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceStorageS3() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + StorageSchemaID: { + Type: schema.TypeInt, + Optional: true, + AtLeastOneOf: []string{ + StorageSchemaID, + StorageSchemaName, + }, + Description: "An id of new storage resource.", + }, + StorageSchemaClientID: { + Type: schema.TypeInt, + Computed: true, + Description: "An client id of new storage resource.", + }, + StorageSchemaName: { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + storageName := i.(string) + if !regexp.MustCompile(`^[\w\-]+$`).MatchString(storageName) || len(storageName) > 255 { + return diag.Errorf("storage name can't be empty and can have only letters, numbers, dashes and underscores, it also should be less than 256 symbols") + } + return nil + }, + AtLeastOneOf: []string{ + StorageSchemaID, + StorageSchemaName, + }, + Description: "A name of new storage resource.", + }, + StorageSchemaLocation: { + Type: schema.TypeString, + Computed: true, + Description: "A location of new storage resource. One of (s-dt2)", + }, + StorageSchemaGenerateHTTPEndpoint: { + Type: schema.TypeString, + Computed: true, + Description: "A http s3 entry point for new storage resource.", + }, + StorageSchemaGenerateS3Endpoint: { + Type: schema.TypeString, + Computed: true, + Description: "A s3 endpoint for new storage resource.", + }, + StorageSchemaGenerateEndpoint: { + Type: schema.TypeString, + Computed: true, + Description: "A s3 entry point for new storage resource.", + }, + }, + ReadContext: resourceStorageS3Read, + Description: "Represent s3 storage resource. https://storage.edgecenter.ru/storage/list", + } +} diff --git a/edgecenter/data_source_edgecenter_storage_s3_bucket.go b/edgecenter/data_source_edgecenter_storage_s3_bucket.go new file mode 100644 index 00000000..ca7847eb --- /dev/null +++ b/edgecenter/data_source_edgecenter_storage_s3_bucket.go @@ -0,0 +1,37 @@ +package edgecenter + +import ( + "regexp" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceStorageS3Bucket() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + StorageS3BucketSchemaStorageID: { + Type: schema.TypeInt, + Required: true, + Description: "An id of existing storage resource.", + }, + StorageS3BucketSchemaName: { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + storageName := i.(string) + if !regexp.MustCompile(`^[\w\-]+$`).MatchString(storageName) || + len(storageName) > 63 || + len(storageName) < 3 { + return diag.Errorf("bucket name can't be empty and can have only letters & numbers. it also should be less than 63 symbols") + } + return nil + }, + Description: "A name of storage bucket resource.", + }, + }, + ReadContext: resourceStorageS3BucketRead, + Description: "Represent storage s3 bucket resource.", + } +} diff --git a/edgecenter/data_source_edgecenter_subnet.go b/edgecenter/data_source_edgecenter_subnet.go new file mode 100644 index 00000000..005e76be --- /dev/null +++ b/edgecenter/data_source_edgecenter_subnet.go @@ -0,0 +1,207 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" +) + +func dataSourceSubnet() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceSubnetRead, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the subnet.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "network_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Description: "The ID of the network to which this subnet belongs.", + }, + "enable_dhcp": { + Type: schema.TypeBool, + Computed: true, + Description: "Enable DHCP for this subnet. If true, DHCP will be used to assign IP addresses to instances within this subnet.", + }, + "cidr": { + Type: schema.TypeString, + Computed: true, + Description: "Represents the IP address range of the subnet.", + }, + "connect_to_network_router": { + Type: schema.TypeBool, + Computed: true, + Description: "True if the network's router should get a gateway in this subnet. Must be explicitly 'false' when gateway_ip is null.", + }, + "dns_nameservers": { + Type: schema.TypeList, + Computed: true, + Description: "List of DNS name servers for the subnet.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "host_routes": { + Type: schema.TypeList, + Computed: true, + Description: "List of additional routes to be added to instances that are part of this subnet.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination": { + Type: schema.TypeString, + Computed: true, + }, + "nexthop": { + Type: schema.TypeString, + Computed: true, + Description: "IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR", + }, + }, + }, + }, + "gateway_ip": { + Type: schema.TypeString, + Computed: true, + Description: "The IP address of the gateway for this subnet.", + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceSubnetRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Subnet reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SubnetPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + networkID := d.Get("network_id").(string) + subnetsOpts := &subnets.ListOpts{NetworkID: networkID} + + if metadataK, ok := d.GetOk("metadata_k"); ok { + subnetsOpts.MetadataK = metadataK.(string) + } + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + typedMetadataKV := make(map[string]string, len(metadataRaw.(map[string]interface{}))) + for k, v := range metadataRaw.(map[string]interface{}) { + typedMetadataKV[k] = v.(string) + } + subnetsOpts.MetadataKV = typedMetadataKV + } + + snets, err := subnets.ListAll(client, *subnetsOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var subnet subnets.Subnet + for _, sn := range snets { + if sn.Name == name { + subnet = sn + found = true + break + } + } + + if !found { + return diag.Errorf("subnet with name %s not found", name) + } + + d.SetId(subnet.ID) + d.Set("name", subnet.Name) + d.Set("enable_dhcp", subnet.EnableDHCP) + d.Set("cidr", subnet.CIDR.String()) + d.Set("network_id", subnet.NetworkID) + + metadataReadOnly := PrepareMetadataReadonly(subnet.Metadata) + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + d.Set("dns_nameservers", dnsNameserversToStringList(subnet.DNSNameservers)) + d.Set("host_routes", hostRoutesToListOfMaps(subnet.HostRoutes)) + d.Set("region_id", subnet.RegionID) + d.Set("project_id", subnet.ProjectID) + d.Set("gateway_ip", subnet.GatewayIP.String()) + + d.Set("connect_to_network_router", true) + if subnet.GatewayIP == nil { + d.Set("connect_to_network_router", false) + d.Set("gateway_ip", "disable") + } + + log.Println("[DEBUG] Finish Subnet reading") + + return diags +} diff --git a/edgecenter/data_source_edgecenter_volume.go b/edgecenter/data_source_edgecenter_volume.go new file mode 100644 index 00000000..c612a005 --- /dev/null +++ b/edgecenter/data_source_edgecenter_volume.go @@ -0,0 +1,156 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" +) + +func dataSourceVolume() *schema.Resource { + return &schema.Resource{ + ReadContext: dataSourceVolumeRead, + Description: `A volume is a detachable block storage device akin to a USB hard drive or SSD, but located remotely in the cloud. +Volumes can be attached to a virtual machine and manipulated like a physical hard drive.`, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the volume.", + }, + "metadata_k": { + Type: schema.TypeString, + Optional: true, + Description: "Filtration query opts (only key).", + }, + "metadata_kv": { + Type: schema.TypeMap, + Optional: true, + Description: `Filtration query opts, for example, {offset = "10", limit = "10"}`, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "size": { + Type: schema.TypeInt, + Computed: true, + Description: "The size of the volume, specified in gigabytes (GB).", + }, + "type_name": { + Type: schema.TypeString, + Computed: true, + Description: "The type of volume to create. Valid values are 'ssd_hiiops', 'standard', 'cold', and 'ultra'. Defaults to 'standard'.", + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceVolumeRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Volume reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + name := d.Get("name").(string) + volumeOpts := &volumes.ListOpts{} + if metadataK, ok := d.GetOk("metadata_k"); ok { + volumeOpts.MetadataK = metadataK.(string) + } + + if metadataRaw, ok := d.GetOk("metadata_kv"); ok { + typedMetadataKV := make(map[string]string, len(metadataRaw.(map[string]interface{}))) + for k, v := range metadataRaw.(map[string]interface{}) { + typedMetadataKV[k] = v.(string) + } + volumeOpts.MetadataKV = typedMetadataKV + } + + vols, err := volumes.ListAll(client, volumeOpts) + if err != nil { + return diag.FromErr(err) + } + + var found bool + var volume volumes.Volume + for _, v := range vols { + if v.Name == name { + volume = v + found = true + break + } + } + + if !found { + return diag.Errorf("volume with name %s not found", name) + } + + d.SetId(volume.ID) + d.Set("name", volume.Name) + d.Set("size", volume.Size) + d.Set("type_name", volume.VolumeType) + d.Set("region_id", volume.RegionID) + d.Set("project_id", volume.ProjectID) + + metadataReadOnly := PrepareMetadataReadonly(volume.Metadata) + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish Volume reading") + + return diags +} diff --git a/edgecenter/floatingip/datasource_floatingip.go b/edgecenter/floatingip/datasource_floatingip.go deleted file mode 100644 index 795deb5c..00000000 --- a/edgecenter/floatingip/datasource_floatingip.go +++ /dev/null @@ -1,200 +0,0 @@ -package floatingip - -import ( - "context" - "fmt" - "net" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func DataSourceEdgeCenterFloatingIP() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceEdgeCenterFloatingIPRead, - Description: `A floating IP is a static IP address that can be associated with one of your instances or loadbalancers, -allowing it to have a static public IP address. The floating IP can be re-associated to any other instance in the same datacenter.`, - - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "id": { - Type: schema.TypeString, - Optional: true, - Description: "floating IP uuid", - ValidateFunc: validation.IsUUID, - ExactlyOneOf: []string{"id", "floating_ip_address"}, - }, - "floating_ip_address": { - Type: schema.TypeString, - Optional: true, - Description: "floating IP address assigned to the resource, must be a valid IP address", - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - ip := net.ParseIP(v) - if ip != nil { - return diag.Diagnostics{} - } - - return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) - }, - ExactlyOneOf: []string{"id", "floating_ip_address"}, - }, - // computed attributes - "status": { - Type: schema.TypeString, - Computed: true, - Description: "current status ('DOWN' or 'ACTIVE') of the floating IP resource", - }, - "port_id": { - Type: schema.TypeString, - Computed: true, - Description: "network port uuid that the floating IP is associated with", - }, - "router_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the router", - }, - "subnet_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the subnet", - }, - "fixed_ip_address": { - Type: schema.TypeString, - Computed: true, - Description: "fixed IP address", - }, - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "instance": { - Type: schema.TypeMap, - Computed: true, - Description: "instance that the floating IP is attached to", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "loadbalancer": { - Type: schema.TypeMap, - Computed: true, - Description: "load balancer that the floating IP is attached to", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "metadata": { - Type: schema.TypeList, - Computed: true, - Description: "metadata in detailed format", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - "read_only": { - Type: schema.TypeBool, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func dataSourceEdgeCenterFloatingIPRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - var foundFloatingIP *edgecloud.FloatingIP - - if id, ok := d.GetOk("id"); ok { - floatingIP, err := util.FloatingIPDetailedByID(ctx, client, id.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundFloatingIP = floatingIP - } else if floatingIPAddress, ok := d.GetOk("floating_ip_address"); ok { - floatingIP, err := util.FloatingIPDetailedByIPAddress(ctx, client, floatingIPAddress.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundFloatingIP = floatingIP - } else { - return diag.Errorf("Error: specify either a floating_ip_address or id to lookup the floating ip") - } - - d.SetId(foundFloatingIP.ID) - d.Set("floating_ip_address", foundFloatingIP.FloatingIPAddress) - d.Set("status", foundFloatingIP.Status) - d.Set("port_id", foundFloatingIP.PortID) - - d.Set("router_id", foundFloatingIP.RouterID) - d.Set("subnet_id", foundFloatingIP.SubnetID) - d.Set("fixed_ip_address", foundFloatingIP.FixedIPAddress.String()) - d.Set("region", foundFloatingIP.Region) - - if len(foundFloatingIP.Metadata) > 0 { - metadata := make([]map[string]interface{}, 0, len(foundFloatingIP.Metadata)) - for _, metadataItem := range foundFloatingIP.Metadata { - metadata = append(metadata, map[string]interface{}{ - "key": metadataItem.Key, - "value": metadataItem.Value, - "read_only": metadataItem.ReadOnly, - }) - } - d.Set("metadata", metadata) - } - - if foundFloatingIP.Instance.ID != "" { - instance := map[string]string{ - "instance_id": foundFloatingIP.Instance.ID, - "instance_name": foundFloatingIP.Instance.Name, - "status": foundFloatingIP.Instance.Status, - "vm_state": foundFloatingIP.Instance.VMState, - } - d.Set("instance", instance) - } - - if foundFloatingIP.Loadbalancer.ID != "" { - loadbalancer := map[string]string{ - "id": foundFloatingIP.Loadbalancer.ID, - "provisioning_status": string(foundFloatingIP.Loadbalancer.ProvisioningStatus), - "operating_status": string(foundFloatingIP.Loadbalancer.OperatingStatus), - "name": foundFloatingIP.Loadbalancer.Name, - "vip_address": foundFloatingIP.Loadbalancer.VipAddress.String(), - "vip_port_id": foundFloatingIP.Loadbalancer.VipPortID, - "vip_network_id": foundFloatingIP.Loadbalancer.VipNetworkID, - } - d.Set("loadbalancer", loadbalancer) - } - - return nil -} diff --git a/edgecenter/floatingip/floatingips.go b/edgecenter/floatingip/floatingips.go deleted file mode 100644 index b0c6e326..00000000 --- a/edgecenter/floatingip/floatingips.go +++ /dev/null @@ -1,85 +0,0 @@ -package floatingip - -import ( - "fmt" - "net" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" -) - -func floatingIPSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the region", - }, - "fixed_ip_address": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: `in case the port has multiple IPs, a specific address can be selected using this field. -if unspecified, the first IP in the list of the port list is used. must be a valid IP address`, - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - ip := net.ParseIP(v) - if ip != nil { - return diag.Diagnostics{} - } - - return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) - }, - RequiredWith: []string{"port_id"}, - }, - "port_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "network port uuid, if provided, the floating IP will be immediately attached to the specified port", - RequiredWith: []string{"fixed_ip_address"}, - }, - "metadata": { - Type: schema.TypeMap, - Optional: true, - Description: "map containing metadata, for example tags.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - // computed attributes - "floating_ip_address": { - Type: schema.TypeString, - Computed: true, - Description: "floating IP address assigned to the resource", - }, - "status": { - Type: schema.TypeString, - Computed: true, - Description: "current status ('DOWN' or 'ACTIVE') of the floating IP resource", - }, - "router_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the router", - }, - "subnet_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the subnet", - }, - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - } -} diff --git a/edgecenter/floatingip/resource_floatingip.go b/edgecenter/floatingip/resource_floatingip.go deleted file mode 100644 index 14a2b567..00000000 --- a/edgecenter/floatingip/resource_floatingip.go +++ /dev/null @@ -1,140 +0,0 @@ -package floatingip - -import ( - "context" - "log" - "net" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" -) - -func ResourceEdgeCenterFloatingIP() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterFloatingIPCreate, - ReadContext: resourceEdgeCenterFloatingIPRead, - UpdateContext: resourceEdgeCenterFloatingIPUpdate, - DeleteContext: resourceEdgeCenterFloatingIPDelete, - Description: `A floating IP is a static IP address that can be associated with one of your instances or loadbalancers, -allowing it to have a static public IP address. The floating IP can be re-associated to any other instance in the same datacenter.`, - Schema: floatingIPSchema(), - } -} - -func resourceEdgeCenterFloatingIPCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - opts := &edgecloud.FloatingIPCreateRequest{} - - if v, ok := d.GetOk("port_id"); ok { - opts.PortID = v.(string) - } - - if v, ok := d.GetOk("fixed_ip_address"); ok { - opts.FixedIPAddress = net.ParseIP(v.(string)) - } - - if v, ok := d.GetOk("metadata"); ok { - metadata := converter.MapInterfaceToMapString(v.(map[string]interface{})) - opts.Metadata = metadata - } - - log.Printf("[DEBUG] Floating IP create configuration: %#v", opts) - - taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Floatingips.Create, opts, client) - if err != nil { - return diag.Errorf("error creating floating IP: %s", err) - } - - d.SetId(taskResult.FloatingIPs[0]) - - log.Printf("[INFO] Floating IP: %s", d.Id()) - - return resourceEdgeCenterFloatingIPRead(ctx, d, meta) -} - -func resourceEdgeCenterFloatingIPRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - // Retrieve the floating ip properties for updating the state - floatingIP, resp, err := client.Floatingips.Get(ctx, d.Id()) - if err != nil { - // check if the floating ip no longer exists. - if resp != nil && resp.StatusCode == 404 { - log.Printf("[WARN] EdgeCenter FloatingIP (%s) not found", d.Id()) - d.SetId("") - return nil - } - - return diag.Errorf("Error retrieving floating ip: %s", err) - } - - d.Set("floating_ip_address", floatingIP.FloatingIPAddress) - d.Set("status", floatingIP.Status) - d.Set("router_id", floatingIP.RouterID) - d.Set("subnet_id", floatingIP.SubnetID) - d.Set("region", floatingIP.Region) - - return nil -} - -func resourceEdgeCenterFloatingIPUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - if d.HasChanges("fixed_ip_address", "port_id") { - oldFixedIPAddress, newFixedIPAddress := d.GetChange("fixed_ip_address") - oldPortID, newPortID := d.GetChange("port_id") - if oldPortID.(string) != "" || oldFixedIPAddress.(string) != "" { - _, _, err := client.Floatingips.UnAssign(ctx, d.Id()) - if err != nil { - return diag.FromErr(err) - } - } - - if newPortID.(string) != "" || newFixedIPAddress.(string) != "" { - assignFloatingIPRequest := &edgecloud.AssignFloatingIPRequest{ - PortID: newPortID.(string), - FixedIPAddress: net.ParseIP(newFixedIPAddress.(string)), - } - - if _, _, err := client.Floatingips.Assign(ctx, d.Id(), assignFloatingIPRequest); err != nil { - return diag.FromErr(err) - } - } - } - - if d.HasChange("metadata") { - metadata := edgecloud.Metadata(converter.MapInterfaceToMapString(d.Get("metadata").(map[string]interface{}))) - - if _, err := client.Floatingips.MetadataUpdate(ctx, d.Id(), &metadata); err != nil { - return diag.Errorf("cannot update metadata. Error: %s", err) - } - } - - return resourceEdgeCenterFloatingIPRead(ctx, d, meta) -} - -func resourceEdgeCenterFloatingIPDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - log.Printf("[INFO] Deleting floating ip: %s", d.Id()) - if err := util.DeleteResourceIfExist(ctx, client, client.Floatingips, d.Id()); err != nil { - return diag.Errorf("Error deleting firewall: %s", err) - } - d.SetId("") - - return nil -} diff --git a/edgecenter/instance/change.go b/edgecenter/instance/change.go deleted file mode 100644 index 1be3df3f..00000000 --- a/edgecenter/instance/change.go +++ /dev/null @@ -1,204 +0,0 @@ -package instance - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" -) - -func changeServerGroup(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client) error { - oldSgRaw, newSgRaw := d.GetChange("server_group_id") - oldSg, newSg := oldSgRaw.(string), newSgRaw.(string) - - // delete old server group - if oldSg != "" { - task, _, err := client.Instances.RemoveFromServerGroup(ctx, d.Id()) - if err != nil { - return fmt.Errorf("error when remove the instance from server group: %w", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return fmt.Errorf("error while waiting for instance remove from server group: %w", err) - } - } - - // add new server group if needed - if newSg != "" { - instancePutIntoServerGroupRequest := &edgecloud.InstancePutIntoServerGroupRequest{ServerGroupID: newSg} - task, _, err := client.Instances.PutIntoServerGroup(ctx, d.Id(), instancePutIntoServerGroupRequest) - if err != nil { - return fmt.Errorf("error when put the instance to new server group: %w", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return fmt.Errorf("error while waiting for instance put to new server group: %w", err) - } - } - - return nil -} - -func changeVolumes(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client) error { - oldVolumesRaw, newVolumesRaw := d.GetChange("volume") - oldVolumes, newVolumes := oldVolumesRaw.([]interface{}), newVolumesRaw.([]interface{}) - - oldIDs := getVolumeIDsSet(oldVolumes) - newIDs := getVolumeIDsSet(newVolumes) - - // detach volumes - for volumeID := range converter.MapLeftDiff(oldIDs, newIDs) { - volume := getVolumeInfoByID(volumeID, oldVolumes) - if volume["boot_index"].(int) == 0 { - return fmt.Errorf("cannot detach primary boot device with boot_index=0. id: %s", volumeID) - } - - volumeDetachRequest := &edgecloud.VolumeDetachRequest{InstanceID: d.Id()} - if _, _, err := client.Volumes.Detach(ctx, volumeID, volumeDetachRequest); err != nil { - return fmt.Errorf("еrror while detaching the volume: %w", err) - } - } - - // attach volumes - for volumeID := range converter.MapLeftDiff(newIDs, oldIDs) { - volume := getVolumeInfoByID(volumeID, newVolumes) - attachmentTag := volume["attachment_tag"].(string) - - switch volume["source"].(string) { - case "image": - return fmt.Errorf("cannot attach image-source volume, required 'existing-volume' or 'new-volume' source") - case "existing-volume": - volumeAttachRequest := &edgecloud.VolumeAttachRequest{ - InstanceID: d.Id(), - AttachmentTag: attachmentTag, - } - if _, _, err := client.Volumes.Attach(ctx, volume["volume_id"].(string), volumeAttachRequest); err != nil { - return fmt.Errorf("cannot attach existing-volume: %s. error: %w", volumeID, err) - } - case "new-volume": - volumeCreateRequest := &edgecloud.VolumeCreateRequest{ - AttachmentTag: attachmentTag, - Source: "new-volume", - InstanceIDToAttachTo: d.Id(), - Name: volume["name"].(string), - Size: volume["size"].(int), - TypeName: edgecloud.VolumeType(volume["type_name"].(string)), - } - task, _, err := client.Volumes.Create(ctx, volumeCreateRequest) - if err != nil { - return fmt.Errorf("error when creating a new instance volume: %w", err) - } - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return fmt.Errorf("error while waiting for instance volume create: %w", err) - } - } - } - - // resize the same volume - for volumeID := range converter.MapsIntersection(newIDs, oldIDs) { - volumeOld := getVolumeInfoByID(volumeID, oldVolumes) - volumeNew := getVolumeInfoByID(volumeID, newVolumes) - - if volumeOld["size"].(int) != volumeNew["size"].(int) { - volumeExtendSizeRequest := &edgecloud.VolumeExtendSizeRequest{Size: volumeNew["size"].(int)} - task, _, err := client.Volumes.Extend(ctx, volumeID, volumeExtendSizeRequest) - if err != nil { - return fmt.Errorf("error when extending instance volume: %w", err) - } - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return fmt.Errorf("error while waiting for instance volume extend: %w", err) - } - } - } - - return nil -} - -func updateInterfaceSecurityGroups(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client, portID string, unAssignSGs, assignSGs []edgecloud.ID) error { - for _, sg := range unAssignSGs { - sgInfo, _, err := client.SecurityGroups.Get(ctx, sg.ID) - if err != nil { - return fmt.Errorf("cannot get security group with ID: %s, err: %w", sg.ID, err) - } - - unAssignSecurityGroupRequest := &edgecloud.AssignSecurityGroupRequest{ - PortsSecurityGroupNames: []edgecloud.PortsSecurityGroupNames{ - { - PortID: portID, - SecurityGroupNames: []string{sgInfo.Name}, - }, - }, - } - if _, err = client.Instances.SecurityGroupUnAssign(ctx, d.Id(), unAssignSecurityGroupRequest); err != nil { - return fmt.Errorf("cannot Unassign security group from Instance. SecGroup ID: %s, err: %w", sg.ID, err) - } - } - - for _, sg := range assignSGs { - sgInfo, _, err := client.SecurityGroups.Get(ctx, sg.ID) - if err != nil { - return fmt.Errorf("cannot get security group with ID: %s, err: %w", sg.ID, err) - } - - assignSecurityGroupRequest := &edgecloud.AssignSecurityGroupRequest{ - PortsSecurityGroupNames: []edgecloud.PortsSecurityGroupNames{ - { - PortID: portID, - SecurityGroupNames: []string{sgInfo.Name}, - }, - }, - } - if _, err := client.Instances.SecurityGroupAssign(ctx, d.Id(), assignSecurityGroupRequest); err != nil { - return fmt.Errorf("cannot Assign security group to Instance. SecGroup ID: %s, err: %w", sg.ID, err) - } - } - - return nil -} - -func detachInterface(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client, ifs map[string]interface{}) error { - detachInterfaceRequest := &edgecloud.InstanceDetachInterfaceRequest{ - PortID: ifs["port_id"].(string), - IPAddress: ifs["ip_address"].(string), - } - task, _, err := client.Instances.DetachInterface(ctx, d.Id(), detachInterfaceRequest) - if err != nil { - return fmt.Errorf("error detaching the instance interface: %w", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return fmt.Errorf("error waiting for the instance interface detach: %w", err) - } - - return nil -} - -func attachInterface(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client, ifs map[string]interface{}) error { - iType := edgecloud.InterfaceType(ifs["type"].(string)) - attachInterfaceRequest := &edgecloud.InstanceAttachInterfaceRequest{Type: iType} - - switch iType { //nolint: exhaustive - case edgecloud.InterfaceTypeSubnet: - attachInterfaceRequest.SubnetID = ifs["subnet_id"].(string) - case edgecloud.InterfaceTypeAnySubnet: - attachInterfaceRequest.NetworkID = ifs["network_id"].(string) - case edgecloud.InterfaceTypeReservedFixedIP: - attachInterfaceRequest.PortID = ifs["port_id"].(string) - } - attachInterfaceRequest.SecurityGroups = getSecurityGroupsIDs(ifs["security_groups"].([]interface{})) - - task, _, err := client.Instances.AttachInterface(ctx, d.Id(), attachInterfaceRequest) - if err != nil { - return fmt.Errorf("error attaching the instance interface: %w", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return fmt.Errorf("error waiting for the instance interface atach: %w", err) - } - - return nil -} diff --git a/edgecenter/instance/datasource_instance.go b/edgecenter/instance/datasource_instance.go deleted file mode 100644 index 5f9a9e6e..00000000 --- a/edgecenter/instance/datasource_instance.go +++ /dev/null @@ -1,240 +0,0 @@ -package instance - -import ( - "context" - "errors" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func DataSourceEdgeCenterInstance() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceEdgeCenterInstanceRead, - Description: `A cloud instance is a virtual machine in a cloud environment`, - - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "id": { - Type: schema.TypeString, - Optional: true, - Description: "instance uuid", - ValidateFunc: validation.IsUUID, - ExactlyOneOf: []string{"id", "name"}, - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: `instance name. this parameter is not unique, if there is more than one instance with the same name, -then the first one will be used. it is recommended to use "id"`, - ExactlyOneOf: []string{"id", "name"}, - }, - // computed attributes - "status": { - Type: schema.TypeString, - Computed: true, - Description: "current status of the instance resource", - }, - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "vm_state": { - Type: schema.TypeString, - Computed: true, - Description: "state of the virtual machine", - }, - "keypair_name": { - Type: schema.TypeString, - Computed: true, - Description: "name of the keypair", - }, - "metadata_detailed": { - Type: schema.TypeList, - Computed: true, - Description: "metadata in detailed format", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - "read_only": { - Type: schema.TypeBool, - Computed: true, - }, - }, - }, - }, - "volumes": { - Type: schema.TypeList, - Computed: true, - Description: "list of volumes ID's", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "security_groups": { - Type: schema.TypeList, - Computed: true, - Description: "list of security groups names", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "flavor": { - Type: schema.TypeMap, - Computed: true, - Description: "information about the flavor", - }, - "addresses": { - Type: schema.TypeList, - Computed: true, - Description: "network addresses associated with the instance", - Elem: &schema.Schema{Type: schema.TypeMap}, - }, - "interface": { - Type: schema.TypeList, - Computed: true, - Description: "network interfaces attached to the instance", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "network_id": { - Type: schema.TypeString, - Computed: true, - }, - "subnet_id": { - Type: schema.TypeString, - Computed: true, - }, - "port_id": { - Type: schema.TypeString, - Computed: true, - }, - "ip_address": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - "server_group_id": { - Type: schema.TypeString, - Computed: true, - Description: "UUID of the anti-affinity or affinity server group (placement groups)", - }, - }, - } -} - -func dataSourceEdgeCenterInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - var foundInstance *edgecloud.Instance - - if id, ok := d.GetOk("id"); ok { - instance, _, err := client.Instances.Get(ctx, id.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundInstance = instance - } else if instanceName, ok := d.GetOk("name"); ok { - instList, _, err := client.Instances.List(ctx, &edgecloud.InstanceListOptions{Name: instanceName.(string)}) - if err != nil { - return diag.FromErr(err) - } - - foundInstance = &instList[0] - } else { - return diag.Errorf("Error: specify either id or a name to lookup the instance") - } - - d.SetId(foundInstance.ID) - d.Set("name", foundInstance.Name) - d.Set("status", foundInstance.Status) - d.Set("region", foundInstance.Region) - d.Set("vm_state", foundInstance.VMState) - d.Set("keypair_name", foundInstance.KeypairName) - - if err := setSecurityGroups(ctx, d, foundInstance); err != nil { - return diag.FromErr(err) - } - - if err := setMetadataDetailed(ctx, d, foundInstance); err != nil { - return diag.FromErr(err) - } - - if err := setFlavor(ctx, d, foundInstance); err != nil { - return diag.FromErr(err) - } - - if err := setAddresses(ctx, d, foundInstance); err != nil { - return diag.FromErr(err) - } - - if len(foundInstance.Volumes) > 0 { - volumes := make([]string, 0, len(foundInstance.Volumes)) - for _, v := range foundInstance.Volumes { - volumes = append(volumes, v.ID) - } - if err := d.Set("volumes", volumes); err != nil { - return diag.FromErr(err) - } - } - - ifs, _, err := client.Instances.InterfaceList(ctx, d.Id()) - if err != nil { - return diag.FromErr(err) - } - - var cleanInterfaces []interface{} - for _, iface := range ifs { - if len(iface.IPAssignments) == 0 { - continue - } - - for _, assignment := range iface.IPAssignments { - i := map[string]interface{}{ - "network_id": iface.NetworkID, - "subnet_id": assignment.SubnetID, - "port_id": iface.PortID, - "ip_address": iface.IPAssignments[0].IPAddress.String(), - } - cleanInterfaces = append(cleanInterfaces, i) - } - } - if err = d.Set("interface", cleanInterfaces); err != nil { - return diag.FromErr(err) - } - - sg, err := util.ServerGroupGetByInstance(ctx, client, d.Id()) - if err != nil { - if !errors.Is(err, util.ErrServerGroupNotFound) { - return diag.Errorf("Error retrieving instance server groups: %s", err) - } - } - - if sg != nil { - d.Set("server_group_id", sg.ID) - } - - return nil -} diff --git a/edgecenter/instance/get.go b/edgecenter/instance/get.go deleted file mode 100644 index 36104d96..00000000 --- a/edgecenter/instance/get.go +++ /dev/null @@ -1,69 +0,0 @@ -package instance - -import edgecloud "github.com/Edge-Center/edgecentercloud-go" - -func getVolumeIDsSet(volumes []interface{}) map[string]struct{} { - ids := make(map[string]struct{}, len(volumes)) - for _, volumeRaw := range volumes { - volume := volumeRaw.(map[string]interface{}) - ids[volume["id"].(string)] = struct{}{} - } - - return ids -} - -func getVolumeInfoByID(id string, volumeList []interface{}) map[string]interface{} { - for _, volumeRaw := range volumeList { - volume := volumeRaw.(map[string]interface{}) - if volume["id"].(string) == id { - return volume - } - } - - return nil -} - -func getVolumeIDByName(name string, volumeList []edgecloud.Volume) string { - for _, volume := range volumeList { - if volume.Name == name { - return volume.ID - } - } - - return "" -} - -func getVolumesBootIndexList(volumes []interface{}) []int { - idxList := make([]int, 0, len(volumes)) - for _, volumeRaw := range volumes { - volume := volumeRaw.(map[string]interface{}) - idxList = append(idxList, volume["boot_index"].(int)) - } - - return idxList -} - -// getSecurityGroupsIDs converts a slice of raw security group IDs to a slice of edgecloud.ItemID. -func getSecurityGroupsIDs(sgsRaw []interface{}) []edgecloud.ID { - sgs := make([]edgecloud.ID, len(sgsRaw)) - for i, sgID := range sgsRaw { - sgs[i] = edgecloud.ID{ID: sgID.(string)} - } - return sgs -} - -// getSecurityGroupsDifference finds the difference between two slices of edgecloud.ID. -func getSecurityGroupsDifference(sl1, sl2 []edgecloud.ID) (diff []edgecloud.ID) { //nolint: nonamedreturns - set := make(map[string]bool) - for _, item := range sl1 { - set[item.ID] = true - } - - for _, item := range sl2 { - if !set[item.ID] { - diff = append(diff, item) - } - } - - return diff -} diff --git a/edgecenter/instance/instances.go b/edgecenter/instance/instances.go deleted file mode 100644 index a40a9308..00000000 --- a/edgecenter/instance/instances.go +++ /dev/null @@ -1,294 +0,0 @@ -package instance - -import ( - "fmt" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func instanceSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the region", - }, - "name": { - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"name_templates"}, - Description: "the instance name", - }, - "name_templates": { - Type: schema.TypeList, - Optional: true, - ConflictsWith: []string{"name"}, - Description: "list of the instance names which will be changed by template: ip_octets, two_ip_octets, one_ip_octet", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "flavor": { - Type: schema.TypeString, - Required: true, - Description: "ID of the flavor, determining its compute and memory, for example 'g1-standard-2-4'.", - }, - "interface": { - Type: schema.TypeList, - Required: true, - Description: "list defining the network interfaces to be attached to the instance", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - Description: fmt.Sprintf( - "available values are '%s', '%s', '%s', '%s'", - edgecloud.InterfaceTypeSubnet, edgecloud.InterfaceTypeAnySubnet, - edgecloud.InterfaceTypeExternal, edgecloud.InterfaceTypeReservedFixedIP, - ), - }, - "security_groups": { - Type: schema.TypeList, - Optional: true, - Computed: true, - Description: "list of security group IDs", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "network_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validation.IsUUID, - Description: fmt.Sprintf( - "ID of the network that the subnet belongs to, required if type is '%s' or '%s'", - edgecloud.InterfaceTypeSubnet, - edgecloud.InterfaceTypeAnySubnet, - ), - }, - "subnet_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validation.IsUUID, - Description: fmt.Sprintf("required if type is '%s'", edgecloud.InterfaceTypeSubnet), - }, - "floating_ip_source": { - Type: schema.TypeString, - Optional: true, - Description: "floating IP type: 'existing' or 'new'", - }, - "floating_ip": { - Type: schema.TypeString, - ValidateFunc: validation.IsUUID, - Optional: true, - Computed: true, - Description: "floating IP for this subnet attachment", - }, - "port_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateFunc: validation.IsUUID, - Description: fmt.Sprintf("required if type is '%s'", edgecloud.InterfaceTypeReservedFixedIP), - }, - "ip_address": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - }, - }, - }, - "volume": { - Type: schema.TypeList, - Required: true, - Description: "list of volumes for the instances", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "source": { - Type: schema.TypeString, - Required: true, - Description: "volume source", - ValidateFunc: validation.StringInSlice( - []string{"new-volume", "existing-volume", "image"}, false, - ), - }, - "size": { - Type: schema.TypeInt, - Required: true, - Description: "size of the volume, specified in gigabytes (GB)", - ValidateFunc: validation.IntAtLeast(1), - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: "name of the volume", - }, - "type_name": { - Type: schema.TypeString, - Optional: true, - Description: "volume type with valid values. defaults to 'ssd_hiiops'", - ValidateFunc: validation.StringInSlice([]string{ - string(edgecloud.VolumeTypeSsdHiIops), - string(edgecloud.VolumeTypeSsdLocal), - string(edgecloud.VolumeTypeUltra), - string(edgecloud.VolumeTypeCold), - string(edgecloud.VolumeTypeStandard), - }, false), - Default: string(edgecloud.VolumeTypeSsdHiIops), - }, - "attachment_tag": { - Type: schema.TypeString, - Optional: true, - Description: "the block device attachment tag (exposed in the metadata)", - }, - "boot_index": { - Type: schema.TypeInt, - Description: `0 for the primary boot device. -unique positive values for other bootable devices. negative - the boot is prohibited`, - Optional: true, - }, - "image_id": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.IsUUID, - Description: "ID of the image. this field is mandatory if creating a volume from an image", - }, - "volume_id": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.IsUUID, - Description: "ID of the volume. this field is mandatory if the volume is a pre-existing volume", - }, - "metadata": { - Type: schema.TypeMap, - Optional: true, - Description: "map containing metadata, for example tags.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - // computed attributes - "id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "delete_on_termination": { - Type: schema.TypeBool, - Optional: true, - Computed: true, - }, - }, - }, - }, - "metadata": { - Type: schema.TypeMap, - Optional: true, - Description: "map containing metadata, for example tags.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "keypair_name": { - Type: schema.TypeString, - Optional: true, - Description: "the name of the keypair to inject into new instance(s)", - }, - "server_group_id": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.IsUUID, - Description: "UUID of the anti-affinity or affinity server group (placement groups)", - }, - "security_groups": { - Type: schema.TypeList, - Optional: true, - Description: "list of security group (firewall) UUIDs", - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validation.IsUUID, - }, - }, - "user_data": { - Type: schema.TypeString, - Optional: true, - Description: "a string in the base64 format. examples of user_data: https://cloudinit.readthedocs.io/en/latest/topics/examples.html", - }, - "username": { - Type: schema.TypeString, - Optional: true, - RequiredWith: []string{"password"}, - Description: "name of a new user on a Linux VM", - }, - "password": { - Type: schema.TypeString, - Optional: true, - RequiredWith: []string{"username"}, - Description: `this parameter is used to set the password either for the 'Admin' user on a Windows VM or -the default user or a new user on a Linux VM`, - }, - "allow_app_ports": { - Type: schema.TypeBool, - Optional: true, - Description: "if true, application ports will be allowed in the security group for the instances created from the marketplace application template", - }, - // computed attributes - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "status": { - Type: schema.TypeString, - Computed: true, - Description: "status of the VM", - }, - "vm_state": { - Type: schema.TypeString, - Computed: true, - Description: "state of the virtual machine", - }, - "addresses": { - Type: schema.TypeList, - Computed: true, - Description: "network addresses associated with the instance", - Elem: &schema.Schema{Type: schema.TypeMap}, - }, - "keypair_id": { - Type: schema.TypeString, - Computed: true, - Description: "uuid of the keypair", - }, - "metadata_detailed": { - Type: schema.TypeList, - Computed: true, - Description: "metadata in detailed format with system info", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - "read_only": { - Type: schema.TypeBool, - Computed: true, - }, - }, - }, - }, - } -} diff --git a/edgecenter/instance/resource_instance.go b/edgecenter/instance/resource_instance.go deleted file mode 100644 index 01c48a02..00000000 --- a/edgecenter/instance/resource_instance.go +++ /dev/null @@ -1,369 +0,0 @@ -package instance - -import ( - "context" - "encoding/base64" - "fmt" - "log" - "slices" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" -) - -func ResourceEdgeCenterInstance() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterInstanceCreate, - ReadContext: resourceEdgeCenterInstanceRead, - UpdateContext: resourceEdgeCenterInstanceUpdate, - DeleteContext: resourceEdgeCenterInstanceDelete, - Description: `A cloud instance is a virtual machine in a cloud environment`, - Schema: instanceSchema(), - - CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - oldVolumesRaw, newVolumesRaw := diff.GetChange("volume") - oldVolumes, newVolumes := oldVolumesRaw.([]interface{}), newVolumesRaw.([]interface{}) - - newVolumesBootIndexes := getVolumesBootIndexList(newVolumes) - - if !slices.Contains(newVolumesBootIndexes, 0) { - return fmt.Errorf("one of volumes should be with boot_index = 0") - } - - // sequential means 0, 1, 2, 3 but not 0, 2, 3, 1 - if len(newVolumesBootIndexes) > 1 { - for i := 1; i < len(newVolumesBootIndexes); i++ { - if newVolumesBootIndexes[i]-newVolumesBootIndexes[i-1] != 1 { - return fmt.Errorf("volume boot_index order must be sequential") - } - } - } - - // check same volume changed - for _, v := range newVolumes { - volume := v.(map[string]interface{}) - oldVolumeWithSameID := getVolumeInfoByID(volume["id"].(string), oldVolumes) - - if oldVolumeWithSameID != nil { - if oldVolumeWithSameID["size"].(int) > volume["size"].(int) { - return fmt.Errorf("volumes `size` can only be expanded and not shrunk") - } - - if oldVolumeWithSameID["name"].(string) != volume["name"].(string) { - return fmt.Errorf("volume cannot be renamed. create a new one with the name you want") - } - - if oldVolumeWithSameID["type_name"].(string) != volume["type_name"].(string) { - return fmt.Errorf("volume type cannot changed. create a new one with the type you want") - } - } - } - - return nil - }, - } -} - -func resourceEdgeCenterInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - opts := &edgecloud.InstanceCreateRequest{ - Flavor: d.Get("flavor").(string), - KeypairName: d.Get("keypair_name").(string), - ServerGroupID: d.Get("server_group_id").(string), - Username: d.Get("username").(string), - Password: d.Get("password").(string), - AllowAppPorts: d.Get("allow_app_ports").(bool), - } - - if userData, ok := d.GetOk("user_data"); ok { - opts.UserData = base64.StdEncoding.EncodeToString([]byte(userData.(string))) - } - - if v, ok := d.GetOk("name"); ok { - opts.Names = []string{v.(string)} - } else if v, ok := d.GetOk("name_templates"); ok { - nameTemplates := v.([]string) - opts.NameTemplates = nameTemplates - } - - if v, ok := d.GetOk("security_groups"); ok { - securityGroups := v.([]interface{}) - sgsList := make([]edgecloud.ID, 0, len(securityGroups)) - for _, sg := range securityGroups { - sgsList = append(sgsList, edgecloud.ID{ID: sg.(string)}) - } - opts.SecurityGroups = sgsList - } - - volumes := d.Get("volume").([]interface{}) - instanceVolumeCreateList, err := converter.ListInterfaceToListInstanceVolumeCreate(volumes) - if err != nil { - return diag.Errorf("error creating instance volume config: %s", err) - } - opts.Volumes = instanceVolumeCreateList - - if v, ok := d.GetOk("metadata"); ok { - metadata := converter.MapInterfaceToMapString(v.(map[string]interface{})) - opts.Metadata = metadata - } - - ifs := d.Get("interface").([]interface{}) - interfaceInstanceCreateOptsList, err := converter.ListInterfaceToListInstanceInterface(ifs) - if err != nil { - return diag.Errorf("error creating instance interface config: %s", err) - } - - if v, ok := d.GetOk("security_groups"); ok { - securityGroups := v.([]interface{}) - sgsList := make([]edgecloud.ID, 0, len(securityGroups)) - for _, sg := range securityGroups { - sgsList = append(sgsList, edgecloud.ID{ID: sg.(string)}) - } - opts.SecurityGroups = sgsList - } - - opts.Interfaces = interfaceInstanceCreateOptsList - - log.Printf("[DEBUG] Instance create configuration: %#v", opts) - - taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Instances.Create, opts, client) - if err != nil { - return diag.Errorf("error creating instance: %s", err) - } - - d.SetId(taskResult.Instances[0]) - - log.Printf("[INFO] Instance: %s", d.Id()) - - return resourceEdgeCenterInstanceRead(ctx, d, meta) -} - -func resourceEdgeCenterInstanceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - // Retrieve the instance properties for updating the state - foundInstance, resp, err := client.Instances.Get(ctx, d.Id()) - if err != nil { - // check if instance no longer exists. - if resp != nil && resp.StatusCode == 404 { - log.Printf("[WARN] EdgeCenter Instance (%s) not found", d.Id()) - d.SetId("") - return nil - } - - return diag.Errorf("Error retrieving instance: %s", err) - } - - d.Set("status", foundInstance.Status) - d.Set("region", foundInstance.Region) - d.Set("vm_state", foundInstance.VMState) - d.Set("keypair_id", foundInstance.KeypairName) - - if err = setVolumes(ctx, d, client); err != nil { - return diag.FromErr(err) - } - - if err = setInterfaces(ctx, d, client); err != nil { - return diag.FromErr(err) - } - - if err = setAddresses(ctx, d, foundInstance); err != nil { - return diag.FromErr(err) - } - - if err = setMetadataDetailed(ctx, d, foundInstance); err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceEdgeCenterInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { //nolint: gocognit, gocyclo - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - if d.HasChange("name") { - newName := d.Get("name").(string) - if _, _, err := client.Instances.Rename(ctx, d.Id(), &edgecloud.Name{Name: newName}); err != nil { - return diag.Errorf("Error when renaming the instance: %s", err) - } - } - - if d.HasChange("flavor") { - newFlavor := d.Get("flavor").(string) - instanceFlavorUpdateRequest := &edgecloud.InstanceFlavorUpdateRequest{FlavorID: newFlavor} - task, _, err := client.Instances.UpdateFlavor(ctx, d.Id(), instanceFlavorUpdateRequest) - if err != nil { - return diag.Errorf("Error when changing the instance flavor: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Error while waiting for flavor change: %s", err) - } - } - - if d.HasChange("metadata") { - metadata := edgecloud.Metadata(converter.MapInterfaceToMapString(d.Get("metadata").(map[string]interface{}))) - - if _, err := client.Instances.MetadataUpdate(ctx, d.Id(), &metadata); err != nil { - return diag.Errorf("cannot update metadata. error: %s", err) - } - } - - if d.HasChange("server_group_id") { - if err := changeServerGroup(ctx, d, client); err != nil { - return diag.FromErr(err) - } - } - - if d.HasChange("volume") { - if err := changeVolumes(ctx, d, client); err != nil { - return diag.FromErr(err) - } - } - - if d.HasChange("interface") { - iOldRaw, iNewRaw := d.GetChange("interface") - ifsOldSlice, ifsNewSlice := iOldRaw.([]interface{}), iNewRaw.([]interface{}) - - switch { - // the same number of interfaces - case len(ifsOldSlice) == len(ifsNewSlice): - for idx, item := range ifsOldSlice { - iOld := item.(map[string]interface{}) - iNew := ifsNewSlice[idx].(map[string]interface{}) - - sgsIDsOld := getSecurityGroupsIDs(iOld["security_groups"].([]interface{})) - sgsIDsNew := getSecurityGroupsIDs(iNew["security_groups"].([]interface{})) - if len(sgsIDsOld) > 0 || len(sgsIDsNew) > 0 { - portID := iOld["port_id"].(string) - unAssignSGs := getSecurityGroupsDifference(sgsIDsNew, sgsIDsOld) - assignSGs := getSecurityGroupsDifference(sgsIDsOld, sgsIDsNew) - if err := updateInterfaceSecurityGroups(ctx, d, client, portID, unAssignSGs, assignSGs); err != nil { - return diag.FromErr(err) - } - } - - differentFields := converter.MapDifference(iOld, iNew, []string{"security_groups"}) - if len(differentFields) > 0 { - if err := detachInterface(ctx, d, client, iOld); err != nil { - return diag.FromErr(err) - } - - if err := attachInterface(ctx, d, client, iNew); err != nil { - return diag.FromErr(err) - } - } - } - - // new interfaces > old interfaces - need to attach new - case len(ifsOldSlice) < len(ifsNewSlice): - for idx, item := range ifsOldSlice { - iOld := item.(map[string]interface{}) - iNew := ifsNewSlice[idx].(map[string]interface{}) - - sgsIDsOld := getSecurityGroupsIDs(iOld["security_groups"].([]interface{})) - sgsIDsNew := getSecurityGroupsIDs(iNew["security_groups"].([]interface{})) - if len(sgsIDsOld) > 0 || len(sgsIDsNew) > 0 { - portID := iOld["port_id"].(string) - unAssignSGs := getSecurityGroupsDifference(sgsIDsNew, sgsIDsOld) - assignSGs := getSecurityGroupsDifference(sgsIDsOld, sgsIDsNew) - if err := updateInterfaceSecurityGroups(ctx, d, client, portID, unAssignSGs, assignSGs); err != nil { - return diag.FromErr(err) - } - } - - differentFields := converter.MapDifference(iOld, iNew, []string{"security_groups"}) - if len(differentFields) > 0 { - if err := detachInterface(ctx, d, client, iOld); err != nil { - return diag.FromErr(err) - } - - if err := attachInterface(ctx, d, client, iNew); err != nil { - return diag.FromErr(err) - } - } - } - - for _, item := range ifsNewSlice[len(ifsOldSlice):] { - iNew := item.(map[string]interface{}) - if err := attachInterface(ctx, d, client, iNew); err != nil { - return diag.FromErr(err) - } - } - - // old interfaces > new interfaces - need to detach old - case len(ifsOldSlice) > len(ifsNewSlice): - for idx, item := range ifsOldSlice[:len(ifsNewSlice)] { - iOld := item.(map[string]interface{}) - iNew := ifsNewSlice[idx].(map[string]interface{}) - - sgsIDsOld := getSecurityGroupsIDs(iOld["security_groups"].([]interface{})) - sgsIDsNew := getSecurityGroupsIDs(iNew["security_groups"].([]interface{})) - if len(sgsIDsOld) > 0 || len(sgsIDsNew) > 0 { - portID := iOld["port_id"].(string) - unAssignSGs := getSecurityGroupsDifference(sgsIDsNew, sgsIDsOld) - assignSGs := getSecurityGroupsDifference(sgsIDsOld, sgsIDsNew) - if err := updateInterfaceSecurityGroups(ctx, d, client, portID, unAssignSGs, assignSGs); err != nil { - return diag.FromErr(err) - } - } - - differentFields := converter.MapDifference(iOld, iNew, []string{"security_groups"}) - if len(differentFields) > 0 { - if err := detachInterface(ctx, d, client, iOld); err != nil { - return diag.FromErr(err) - } - - if err := attachInterface(ctx, d, client, iNew); err != nil { - return diag.FromErr(err) - } - } - } - - for _, item := range ifsOldSlice[len(ifsNewSlice):] { - iOld := item.(map[string]interface{}) - if err := detachInterface(ctx, d, client, iOld); err != nil { - return diag.FromErr(err) - } - } - } - } - - return resourceEdgeCenterInstanceRead(ctx, d, meta) -} - -func resourceEdgeCenterInstanceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - log.Printf("[INFO] Deleting instance: %s", d.Id()) - task, _, err := client.Instances.Delete(ctx, d.Id(), nil) - if err != nil { - return diag.Errorf("Error deleting instance: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Delete instance task failed with error: %s", err) - } - - if err = util.ResourceIsDeleted(ctx, client.Instances.Get, d.Id()); err != nil { - return diag.Errorf("Instance with id %s was not deleted: %s", d.Id(), err) - } - - d.SetId("") - - return nil -} diff --git a/edgecenter/instance/set.go b/edgecenter/instance/set.go deleted file mode 100644 index e8bad4c2..00000000 --- a/edgecenter/instance/set.go +++ /dev/null @@ -1,138 +0,0 @@ -package instance - -import ( - "context" - "fmt" - "strconv" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func setVolumes(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client) error { - // desc sorting: first volume is the last added - volumes, _, err := client.Volumes.List(ctx, &edgecloud.VolumeListOptions{InstanceID: d.Id()}) - if err != nil { - return fmt.Errorf("error retrieving instance volumes: %w", err) - } - - // asc sorting: first volume is the first added - currentVolumes := d.Get("volume").([]interface{}) - for i, v := range currentVolumes { - volume := v.(map[string]interface{}) - volumeID := getVolumeIDByName(volume["name"].(string), volumes) - if volumeID == "" { - return fmt.Errorf("error during get volume id") - } - currentVolumes[i].(map[string]interface{})["id"] = volumeID - } - - return d.Set("volume", currentVolumes) -} - -func setInterfaces(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client) error { - instancePorts, _, err := client.Instances.PortsList(ctx, d.Id()) - if err != nil { - return fmt.Errorf("error retrieving instance ports: %w", err) - } - - routerInterfaces, _, err := client.Instances.InterfaceList(ctx, d.Id()) - if err != nil { - return fmt.Errorf("error retrieving instance interfaces: %w", err) - } - - currentInterfaces := d.Get("interface").([]interface{}) - for i, v := range currentInterfaces { - portID := routerInterfaces[i].PortID - - var sgList []string - for _, port := range instancePorts { - if port.ID == portID { - for _, sg := range port.SecurityGroups { - sgList = append(sgList, sg.ID) - } - } - } - - ipAssignments := routerInterfaces[i].IPAssignments - if len(ipAssignments) == 0 { - continue - } - - fip := routerInterfaces[i].FloatingIPDetails - - ifs := v.(map[string]interface{}) - ifsType := edgecloud.InterfaceType(ifs["type"].(string)) - switch ifsType { //nolint: exhaustive - case edgecloud.InterfaceTypeSubnet, edgecloud.InterfaceTypeAnySubnet: - currentInterfaces[i].(map[string]interface{})["subnet_id"] = ipAssignments[0].SubnetID - } - currentInterfaces[i].(map[string]interface{})["port_id"] = portID - currentInterfaces[i].(map[string]interface{})["ip_address"] = ipAssignments[0].IPAddress.String() - if len(fip) > 0 { - currentInterfaces[i].(map[string]interface{})["floating_ip"] = fip[0].FloatingIPAddress - } - - currentInterfaces[i].(map[string]interface{})["security_groups"] = sgList - } - - return d.Set("interface", currentInterfaces) -} - -func setAddresses(_ context.Context, d *schema.ResourceData, instance *edgecloud.Instance) error { - addresses := make([]map[string]string, 0, len(instance.Addresses)) - for networkName, networkInfo := range instance.Addresses { - net := networkInfo[0] - address := map[string]string{ - "network_name": networkName, - "type": net.Type, - "addr": net.Address.String(), - "subnet_id": net.SubnetID, - "subnet_name": net.SubnetName, - } - addresses = append(addresses, address) - } - - return d.Set("addresses", addresses) -} - -func setFlavor(_ context.Context, d *schema.ResourceData, instance *edgecloud.Instance) error { - flavor := map[string]interface{}{ - "flavor_name": instance.Flavor.FlavorName, - "vcpus": strconv.Itoa(instance.Flavor.VCPUS), - "ram": strconv.Itoa(instance.Flavor.RAM), - "flavor_id": instance.Flavor.FlavorID, - } - - return d.Set("flavor", flavor) -} - -func setSecurityGroups(_ context.Context, d *schema.ResourceData, instance *edgecloud.Instance) error { - if len(instance.SecurityGroups) > 0 { - securityGroups := make([]string, 0, len(instance.SecurityGroups)) - for _, sg := range instance.SecurityGroups { - securityGroups = append(securityGroups, sg.Name) - } - return d.Set("security_groups", securityGroups) - } - - return nil -} - -func setMetadataDetailed(_ context.Context, d *schema.ResourceData, instance *edgecloud.Instance) error { - if len(instance.MetadataDetailed) > 0 { - metadata := make([]map[string]interface{}, 0, len(instance.MetadataDetailed)) - for _, metadataItem := range instance.MetadataDetailed { - metadata = append(metadata, map[string]interface{}{ - "key": metadataItem.Key, - "value": metadataItem.Value, - "read_only": metadataItem.ReadOnly, - }) - } - - return d.Set("metadata_detailed", metadata) - } - - return nil -} diff --git a/edgecenter/lblistener/datasource_lblistener.go b/edgecenter/lblistener/datasource_lblistener.go deleted file mode 100644 index fe1bb71b..00000000 --- a/edgecenter/lblistener/datasource_lblistener.go +++ /dev/null @@ -1,142 +0,0 @@ -package lblistener - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func DataSourceEdgeCenterLbListener() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceEdgeCenterLbListenerRead, - Description: `A listener is a process that checks for connection requests using the protocol and port that you configure.`, - - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "id": { - Type: schema.TypeString, - Optional: true, - Description: "listener uuid", - ValidateFunc: validation.IsUUID, - ExactlyOneOf: []string{"id", "name"}, - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: `listener name. this parameter is not unique, if there is more than one listener with the same name, -then the first one will be used. it is recommended to use "id"`, - ExactlyOneOf: []string{"id", "name"}, - }, - "loadbalancer_id": { - Type: schema.TypeString, - Required: true, - Description: "ID of the load balancer", - }, - // computed attributes - "protocol": { - Type: schema.TypeString, - Computed: true, - Description: "protocol of the load balancer", - }, - "protocol_port": { - Type: schema.TypeInt, - Computed: true, - Description: "protocol port number of the resource", - }, - "secret_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the secret where PKCS12 file is stored for the TERMINATED_HTTPS load balancer", - }, - "provisioning_status": { - Type: schema.TypeString, - Computed: true, - Description: "lifecycle status of the listener", - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the listener", - }, - "pool_count": { - Type: schema.TypeInt, - Computed: true, - Description: "number of pools", - }, - "insert_headers": { - Type: schema.TypeMap, - Computed: true, - Description: "dictionary of additional header insertion into the HTTP headers. only used with the HTTP and TERMINATED_HTTPS protocols", - }, - "allowed_cidrs": { - Type: schema.TypeList, - Computed: true, - Description: "allowed CIDRs for listener", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - }, - } -} - -func dataSourceEdgeCenterLbListenerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - loadbalancerID := d.Get("loadbalancer_id").(string) - - var foundListener *edgecloud.Listener - - if id, ok := d.GetOk("id"); ok { - listener, _, err := client.Loadbalancers.ListenerGet(ctx, id.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundListener = listener - } else if listenerName, ok := d.GetOk("name"); ok { - listener, err := util.LBListenerGetByName(ctx, client, listenerName.(string), loadbalancerID) - if err != nil { - return diag.FromErr(err) - } - - foundListener = listener - } else { - return diag.Errorf("Error: specify either id or a name to lookup the listener") - } - - d.SetId(foundListener.ID) - d.Set("name", foundListener.Name) - d.Set("loadbalancer_id", loadbalancerID) - d.Set("provisioning_status", foundListener.ProvisioningStatus) - d.Set("operating_status", foundListener.OperatingStatus) - d.Set("protocol", foundListener.Protocol) - d.Set("protocol_port", foundListener.ProtocolPort) - d.Set("pool_count", foundListener.PoolCount) - d.Set("secret_id", foundListener.SecretID) - - if err := setAllowedCIDRs(ctx, d, foundListener); err != nil { - return diag.FromErr(err) - } - - if err := setInsertHeaders(ctx, d, foundListener); err != nil { - return diag.FromErr(err) - } - - return nil -} diff --git a/edgecenter/lblistener/lblistener.go b/edgecenter/lblistener/lblistener.go deleted file mode 100644 index 31abed91..00000000 --- a/edgecenter/lblistener/lblistener.go +++ /dev/null @@ -1,119 +0,0 @@ -package lblistener - -import ( - "fmt" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func lblistenerSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "name": { - Type: schema.TypeString, - Required: true, - Description: `listener name`, - }, - "loadbalancer_id": { - Type: schema.TypeString, - Required: true, - Description: "ID of the load balancer", - ValidateFunc: validation.IsUUID, - }, - "protocol": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: fmt.Sprintf( - "available values are '%s', '%s', '%s', '%s' and '%s'", - edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS, - edgecloud.ListenerProtocolTCP, edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolTerminatedHTTPS, - ), - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - switch edgecloud.LoadbalancerListenerProtocol(v) { - case edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS, edgecloud.ListenerProtocolTCP, - edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolTerminatedHTTPS: - return diag.Diagnostics{} - default: - return diag.Errorf( - "wrong protocol %s, available values are '%s', '%s', '%s', '%s', '%s'", v, - edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS, - edgecloud.ListenerProtocolTCP, edgecloud.ListenerProtocolUDP, - edgecloud.ListenerProtocolTerminatedHTTPS, - ) - } - }, - }, - "protocol_port": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "port on which the protocol is bound", - }, - "insert_x_forwarded": { - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - Description: "add headers X-Forwarded-For, X-Forwarded-Port, X-Forwarded-Proto to requests. only used with HTTP or TERMINATED_HTTPS protocols", - }, - "secret_id": { - Type: schema.TypeString, - Optional: true, - Description: "ID of the secret where PKCS12 file is stored for the TERMINATED_HTTPS load balancer", - ValidateFunc: validation.IsUUID, - }, - "sni_secret_id": { - Type: schema.TypeList, - Optional: true, - Description: "list of secret identifiers used for Server Name Indication (SNI).", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "allowed_cidrs": { - Type: schema.TypeList, - Optional: true, - Description: "the allowed CIDRs for listener", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - // computed attributes - "id": { - Type: schema.TypeString, - Computed: true, - Description: "listener uuid", - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the listener", - }, - "provisioning_status": { - Type: schema.TypeString, - Computed: true, - Description: "lifecycle status of the listener", - }, - "pool_count": { - Type: schema.TypeInt, - Computed: true, - Description: "number of pools", - }, - "insert_headers": { - Type: schema.TypeMap, - Computed: true, - Description: "dictionary of additional header insertion into the HTTP headers. only used with the HTTP and TERMINATED_HTTPS protocols", - }, - } -} diff --git a/edgecenter/lblistener/resource_lblistener.go b/edgecenter/lblistener/resource_lblistener.go deleted file mode 100644 index 123b8c50..00000000 --- a/edgecenter/lblistener/resource_lblistener.go +++ /dev/null @@ -1,232 +0,0 @@ -package lblistener - -import ( - "context" - "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func ResourceEdgeCenterLbListener() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterLbListenerCreate, - ReadContext: resourceEdgeCenterLbListenerRead, - UpdateContext: resourceEdgeCenterLbListenerUpdate, - DeleteContext: resourceEdgeCenterLbListenerDelete, - Description: `A listener is a process that checks for connection requests using the protocol and port that you configure. -Can not be created without a load balancer.`, - Schema: lblistenerSchema(), - - CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - protocol := edgecloud.LoadbalancerListenerProtocol(diff.Get("protocol").(string)) - - if diff.HasChange("secret_id") { - if protocol != edgecloud.ListenerProtocolTerminatedHTTPS { - return fmt.Errorf( - "secret_id parameter can only be used with %s listener protocol type", - edgecloud.ListenerProtocolTerminatedHTTPS, - ) - } - } - - if diff.HasChange("sni_secret_id") { - if protocol != edgecloud.ListenerProtocolTerminatedHTTPS { - return fmt.Errorf( - "sni_secret_id parameter can only be used with %s listener protocol type", - edgecloud.ListenerProtocolTerminatedHTTPS, - ) - } - } - - return nil - }, - } -} - -func resourceEdgeCenterLbListenerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - opts := &edgecloud.ListenerCreateRequest{ - Name: d.Get("name").(string), - LoadbalancerID: d.Get("loadbalancer_id").(string), - Protocol: edgecloud.LoadbalancerListenerProtocol(d.Get("protocol").(string)), - ProtocolPort: d.Get("protocol_port").(int), - InsertXForwarded: d.Get("insert_x_forwarded").(bool), - } - - secretID := d.Get("secret_id").(string) - sniSecretIDRaw := d.Get("sni_secret_id").([]interface{}) - - switch opts.Protocol { - case edgecloud.ListenerProtocolTCP, edgecloud.ListenerProtocolUDP, edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolHTTPS: - if secretID != "" { - return diag.Errorf("secret_id parameter can only be used with %s listener protocol type", edgecloud.ListenerProtocolTerminatedHTTPS) - } - - if len(sniSecretIDRaw) > 0 { - return diag.Errorf("sni_secret_id parameter can only be used with %s listener protocol type", edgecloud.ListenerProtocolTerminatedHTTPS) - } - - if opts.InsertXForwarded && (opts.Protocol == edgecloud.ListenerProtocolTCP || opts.Protocol == edgecloud.ListenerProtocolUDP || opts.Protocol == edgecloud.ListenerProtocolHTTPS) { - return diag.Errorf( - "X-Forwarded headers can only be used with %s or %s listener protocol type", - edgecloud.ListenerProtocolHTTP, edgecloud.ListenerProtocolTerminatedHTTPS, - ) - } - case edgecloud.ListenerProtocolTerminatedHTTPS: - if secretID == "" { - return diag.Errorf("secret_id parameter is required with %s listener protocol type", edgecloud.ListenerProtocolTerminatedHTTPS) - } - opts.SecretID = secretID - if len(sniSecretIDRaw) > 0 { - opts.SNISecretID = make([]string, len(sniSecretIDRaw)) - for i, s := range sniSecretIDRaw { - opts.SNISecretID[i] = s.(string) - } - } - default: - return diag.Errorf("wrong protocol") - } - - allowedCIRDsRaw := d.Get("allowed_cidrs").([]interface{}) - if len(allowedCIRDsRaw) > 0 { - opts.AllowedCIDRs = make([]string, len(allowedCIRDsRaw)) - for i, s := range allowedCIRDsRaw { - opts.AllowedCIDRs[i] = s.(string) - } - } - - log.Printf("[DEBUG] Loadbalancer listener create configuration: %#v", opts) - - taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Loadbalancers.ListenerCreate, opts, client) - if err != nil { - return diag.Errorf("error creating loadbalancer listener: %s", err) - } - - d.SetId(taskResult.Listeners[0]) - - log.Printf("[INFO] Listener: %s", d.Id()) - - return resourceEdgeCenterLbListenerRead(ctx, d, meta) -} - -func resourceEdgeCenterLbListenerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - // Retrieve the loadbalancer listener properties for updating the state - listener, resp, err := client.Loadbalancers.ListenerGet(ctx, d.Id()) - if err != nil { - // check if the loadbalancer listener no longer exists. - if resp != nil && resp.StatusCode == 404 { - log.Printf("[WARN] EdgeCenter Listener (%s) not found", d.Id()) - d.SetId("") - return nil - } - - return diag.Errorf("Error retrieving loadbalancer listener: %s", err) - } - - d.Set("name", listener.Name) - d.Set("protocol", listener.Protocol) - d.Set("protocol_port", listener.ProtocolPort) - d.Set("secret_id", listener.SecretID) - d.Set("sni_secret_id", listener.SNISecretID) - d.Set("operating_status", listener.OperatingStatus) - d.Set("provisioning_status", listener.ProvisioningStatus) - d.Set("pool_count", listener.PoolCount) - - if err := setAllowedCIDRs(ctx, d, listener); err != nil { - return diag.FromErr(err) - } - - if err := setInsertHeaders(ctx, d, listener); err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceEdgeCenterLbListenerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - var changed bool - opts := &edgecloud.ListenerUpdateRequest{Name: d.Get("name").(string)} - - if d.HasChange("name") { - changed = true - } - - if d.HasChange("secret_id") { - opts.SecretID = d.Get("secret_id").(string) - changed = true - } - - if d.HasChange("sni_secret_id") { - sniSecretIDRaw := d.Get("sni_secret_id").([]interface{}) - sniSecretID := make([]string, len(sniSecretIDRaw)) - for i, s := range sniSecretIDRaw { - sniSecretID[i] = s.(string) - } - opts.SNISecretID = sniSecretID - changed = true - } - - if d.HasChange("allowed_cidrs") { - allowedCIDRsRaw := d.Get("allowed_cidrs").([]interface{}) - allowedCIDRs := make([]string, len(allowedCIDRsRaw)) - for i, s := range allowedCIDRsRaw { - allowedCIDRs[i] = s.(string) - } - opts.AllowedCIDRs = allowedCIDRs - changed = true - } - - if changed { - task, _, err := client.Loadbalancers.ListenerUpdate(ctx, d.Id(), opts) - if err != nil { - return diag.Errorf("Error when changing the loadbalancer listener: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Error while waiting for loadbalancer listener update: %s", err) - } - } - - return resourceEdgeCenterLbListenerRead(ctx, d, meta) -} - -func resourceEdgeCenterLbListenerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - log.Printf("[INFO] Deleting loadbalancer listener: %s", d.Id()) - task, _, err := client.Loadbalancers.ListenerDelete(ctx, d.Id()) - if err != nil { - return diag.Errorf("Error deleting loadbalancer listener: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Delete loadbalancer listener task failed with error: %s", err) - } - - if err = util.ResourceIsDeleted(ctx, client.Loadbalancers.ListenerGet, d.Id()); err != nil { - return diag.Errorf("Loadbalancer listener with id %s was not deleted: %s", d.Id(), err) - } - - d.SetId("") - - return nil -} diff --git a/edgecenter/lblistener/set.go b/edgecenter/lblistener/set.go deleted file mode 100644 index 2378ecd5..00000000 --- a/edgecenter/lblistener/set.go +++ /dev/null @@ -1,31 +0,0 @@ -package lblistener - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func setAllowedCIDRs(_ context.Context, d *schema.ResourceData, listener *edgecloud.Listener) error { - if len(listener.AllowedCIDRs) > 0 { - allowedCIDRs := make([]string, 0, len(listener.AllowedCIDRs)) - allowedCIDRs = append(allowedCIDRs, listener.AllowedCIDRs...) - return d.Set("allowed_cidrs", allowedCIDRs) - } - - return nil -} - -func setInsertHeaders(_ context.Context, d *schema.ResourceData, listener *edgecloud.Listener) error { - if len(listener.InsertHeaders) > 0 { - return d.Set("insert_headers", map[string]interface{}{ - "X-Forwarded-For": "true", - "X-Forwarded-Port": "true", - "X-Forwarded-Proto": "true", - }) - } - - return nil -} diff --git a/edgecenter/lbmember/lbmember.go b/edgecenter/lbmember/lbmember.go deleted file mode 100644 index d1dc301d..00000000 --- a/edgecenter/lbmember/lbmember.go +++ /dev/null @@ -1,96 +0,0 @@ -package lbmember - -import ( - "fmt" - "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 ( - minWeight = 0 - maxWeight = 256 -) - -func lbmemberSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "pool_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "ID of the load balancer pool", - }, - "address": { - Type: schema.TypeString, - Required: true, - Description: "IP address of the load balancer pool member", - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - ip := net.ParseIP(v) - if ip != nil { - return diag.Diagnostics{} - } - - return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) - }, - }, - "protocol_port": { - Type: schema.TypeInt, - Required: true, - Description: "IP port on which the member listens for requests", - }, - "subnet_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - Description: "uuid of the subnet in which the pool member is located.", - }, - "instance_id": { - Type: schema.TypeString, - Optional: true, - Description: "uuid of the instance (amphora) associated with the pool member.", - }, - "weight": { - Type: schema.TypeInt, - Optional: true, - Default: 1, - Description: "weight value between 0 and 256, determining the distribution of requests among the members of the pool. defaults to 1", - ValidateDiagFunc: func(val interface{}, path cty.Path) diag.Diagnostics { - v := val.(int) - if v >= minWeight && v <= maxWeight { - return nil - } - return diag.Errorf("valid values: %d to %d got: %d", minWeight, maxWeight, v) - }, - }, - "admin_state_up": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "true if enabled. Defaults to true", - }, - // computed attributes - "id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the member must be provided if the existing member is being updated", - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the pool", - }, - } -} diff --git a/edgecenter/lbmember/resource_lbmember.go b/edgecenter/lbmember/resource_lbmember.go deleted file mode 100644 index 13671a52..00000000 --- a/edgecenter/lbmember/resource_lbmember.go +++ /dev/null @@ -1,198 +0,0 @@ -package lbmember - -import ( - "context" - "errors" - "log" - "net" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func ResourceEdgeCenterLbMember() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterLbMemberCreate, - ReadContext: resourceEdgeCenterLbMemberRead, - UpdateContext: resourceEdgeCenterLbMemberUpdate, - DeleteContext: resourceEdgeCenterLbMemberDelete, - Description: `A Member node represents a physical server that acts as a provider of a service available to a load balancer. -Does not support concurrent update of multiple members. Update one at a time`, - Schema: lbmemberSchema(), - } -} - -func resourceEdgeCenterLbMemberCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - opts := &edgecloud.PoolMemberCreateRequest{ - Address: net.ParseIP(d.Get("address").(string)), - ProtocolPort: d.Get("protocol_port").(int), - Weight: d.Get("weight").(int), - SubnetID: d.Get("subnet_id").(string), - InstanceID: d.Get("instance_id").(string), - AdminStateUP: d.Get("admin_state_up").(bool), - } - - log.Printf("[DEBUG] Loadbalancer pool member create configuration: %#v", opts) - - task, _, err := client.Loadbalancers.PoolMemberCreate(ctx, d.Get("pool_id").(string), opts) - if err != nil { - return diag.Errorf("error creating loadbalancer pool member: %s", err) - } - - taskInfo, err := util.WaitAndGetTaskInfo(ctx, client, task.Tasks[0]) - if err != nil { - return diag.Errorf("error waiting for pool member create: %s", err) - } - - taskResult, err := util.ExtractTaskResultFromTask(taskInfo) - if err != nil { - return diag.Errorf("error while extract task result: %s", err) - } - - d.SetId(taskResult.Members[0]) - - log.Printf("[INFO] Pool member: %s", d.Id()) - - return resourceEdgeCenterLbMemberRead(ctx, d, meta) -} - -func resourceEdgeCenterLbMemberRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - member, err := util.PoolMemberGetByID(ctx, client, d.Get("pool_id").(string), d.Id()) - if errors.Is(err, util.ErrLoadbalancerPoolsMemberNotFound) { - log.Printf("[WARN] EdgeCenter Pool member (%s) not found", d.Id()) - d.SetId("") - return nil - } - - d.Set("address", member.Address.String()) - d.Set("protocol_port", member.ProtocolPort) - d.Set("weight", member.Weight) - d.Set("subnet_id", member.SubnetID) - d.Set("instance_id", member.InstanceID) - d.Set("operating_status", member.OperatingStatus) - - return nil -} - -func resourceEdgeCenterLbMemberUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - pool, _, err := client.Loadbalancers.PoolGet(ctx, d.Get("pool_id").(string)) - if err != nil { - return diag.FromErr(err) - } - - members := make([]edgecloud.PoolMemberCreateRequest, len(pool.Members)) - for i, pm := range pool.Members { - if pm.ID != d.Id() { - members[i] = edgecloud.PoolMemberCreateRequest{ - ID: pm.ID, - Address: pm.Address, - ProtocolPort: pm.ProtocolPort, - Weight: pm.Weight, - SubnetID: pm.SubnetID, - InstanceID: pm.InstanceID, - AdminStateUP: pm.AdminStateUP, - } - - continue - } - - members[i] = edgecloud.PoolMemberCreateRequest{ - ID: d.Id(), - Address: net.ParseIP(d.Get("address").(string)), - ProtocolPort: d.Get("protocol_port").(int), - Weight: d.Get("weight").(int), - SubnetID: d.Get("subnet_id").(string), - InstanceID: d.Get("instance_id").(string), - AdminStateUP: d.Get("admin_state_up").(bool), - } - } - - opts := &edgecloud.PoolUpdateRequest{ - ID: pool.ID, - Name: pool.Name, - LoadbalancerAlgorithm: pool.LoadbalancerAlgorithm, - Members: members, - TimeoutMemberData: pool.TimeoutMemberData, - TimeoutClientData: pool.TimeoutClientData, - TimeoutMemberConnect: pool.TimeoutMemberConnect, - HealthMonitor: edgecloud.HealthMonitorCreateRequest{ - Type: pool.HealthMonitor.Type, - MaxRetries: pool.HealthMonitor.MaxRetries, - Delay: pool.HealthMonitor.Delay, - Timeout: pool.HealthMonitor.Timeout, - ID: pool.HealthMonitor.ID, - ExpectedCodes: pool.HealthMonitor.ExpectedCodes, - MaxRetriesDown: pool.HealthMonitor.MaxRetriesDown, - HTTPMethod: pool.HealthMonitor.HTTPMethod, - URLPath: pool.HealthMonitor.URLPath, - }, - } - task, _, err := client.Loadbalancers.PoolUpdate(ctx, pool.ID, opts) - if err != nil { - return diag.Errorf("Error when changing the loadbalancer pool: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Error while waiting for loadbalancer pool update: %s", err) - } - - poolAfterUpdate, _, err := client.Loadbalancers.PoolGet(ctx, d.Get("pool_id").(string)) - if err != nil { - return diag.Errorf("Error when get the loadbalancer pool info after update: %s", err) - } - - for _, pm := range poolAfterUpdate.Members { - if pm.Address == nil { - continue - } - - if net.IP.Equal(pm.Address, net.ParseIP(d.Get("address").(string))) && - pm.ProtocolPort == d.Get("protocol_port").(int) && pm.SubnetID == d.Get("subnet_id").(string) { - d.SetId(pm.ID) - break - } - } - - return resourceEdgeCenterLbMemberRead(ctx, d, meta) -} - -func resourceEdgeCenterLbMemberDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - log.Printf("[INFO] Deleting loadbalancer pool member: %s", d.Id()) - task, _, err := client.Loadbalancers.PoolMemberDelete(ctx, d.Get("pool_id").(string), d.Id()) - if err != nil { - return diag.Errorf("Error deleting pool member: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Delete pool member task failed with error: %s", err) - } - - _, err = util.PoolMemberGetByID(ctx, client, d.Get("pool_id").(string), d.Id()) - if !errors.Is(err, util.ErrLoadbalancerPoolsMemberNotFound) { - return diag.Errorf("Pool member with id %s was not deleted: %s", d.Id(), err) - } - - d.SetId("") - - return nil -} diff --git a/edgecenter/lbpool/datasource_lbpool.go b/edgecenter/lbpool/datasource_lbpool.go deleted file mode 100644 index 307b3785..00000000 --- a/edgecenter/lbpool/datasource_lbpool.go +++ /dev/null @@ -1,259 +0,0 @@ -package lbpool - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func DataSourceEdgeCenterLbPool() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceEdgeCenterLbPoolRead, - Description: `A pool is a list of virtual machines to which the listener will redirect incoming traffic`, - - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "id": { - Type: schema.TypeString, - Optional: true, - Description: "lb pool uuid", - ValidateFunc: validation.IsUUID, - ExactlyOneOf: []string{"id", "name"}, - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: `lb pool name. this parameter is not unique, if there is more than one lb pool with the same name, -then the first one will be used. it is recommended to use "id"`, - ExactlyOneOf: []string{"id", "name"}, - }, - "loadbalancer_id": { - Type: schema.TypeString, - Required: true, - Description: "ID of the load balancer", - }, - // computed attributes - "listener_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the load balancer listener", - }, - "lb_algorithm": { - Type: schema.TypeString, - Computed: true, - Description: "algorithm of the load balancer", - }, - "provisioning_status": { - Type: schema.TypeString, - Computed: true, - Description: "lifecycle status of the pool", - }, - "session_persistence": { - Type: schema.TypeList, - Computed: true, - Description: `configuration that enables the load balancer to bind a user's session to a specific backend member. -this ensures that all requests from the user during the session are sent to the same member.`, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Computed: true, - }, - "cookie_name": { - Type: schema.TypeString, - Computed: true, - }, - "persistence_granularity": { - Type: schema.TypeString, - Computed: true, - }, - "persistence_timeout": { - Type: schema.TypeInt, - Computed: true, - }, - }, - }, - }, - "timeout_member_connect": { - Type: schema.TypeInt, - Computed: true, - Description: "timeout for the backend member connection (in milliseconds)", - }, - "timeout_member_data": { - Type: schema.TypeInt, - Computed: true, - Description: "timeout for the backend member inactivity (in milliseconds)", - }, - "timeout_client_data": { - Type: schema.TypeInt, - Computed: true, - Description: "timeout for the frontend client inactivity (in milliseconds)", - }, - "healthmonitor": { - Type: schema.TypeList, - Computed: true, - Description: `configuration for health checks to test the health and state of the backend members. -it determines how the load balancer identifies whether the backend members are healthy or unhealthy`, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - }, - "type": { - Type: schema.TypeString, - Computed: true, - }, - "delay": { - Type: schema.TypeInt, - Computed: true, - }, - "timeout": { - Type: schema.TypeInt, - Computed: true, - }, - "max_retries": { - Type: schema.TypeInt, - Computed: true, - }, - "max_retries_down": { - Type: schema.TypeInt, - Computed: true, - }, - "url_path": { - Type: schema.TypeString, - Computed: true, - }, - "expected_codes": { - Type: schema.TypeString, - Computed: true, - }, - "http_method": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the pool", - }, - "protocol": { - Type: schema.TypeString, - Computed: true, - Description: "protocol of the load balancer", - }, - "member": { - Type: schema.TypeList, - Computed: true, - Description: "members of the Pool", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "weight": { - Type: schema.TypeInt, - Computed: true, - }, - "address": { - Type: schema.TypeString, - Computed: true, - }, - "id": { - Type: schema.TypeString, - Computed: true, - }, - "protocol_port": { - Type: schema.TypeInt, - Computed: true, - }, - "subnet_id": { - Type: schema.TypeString, - Computed: true, - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - }, - "instance_id": { - Type: schema.TypeString, - Computed: true, - }, - "admin_state_up": { - Type: schema.TypeBool, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func dataSourceEdgeCenterLbPoolRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - loadbalancerID := d.Get("loadbalancer_id").(string) - - var foundPool *edgecloud.Pool - - if id, ok := d.GetOk("id"); ok { - pool, _, err := client.Loadbalancers.PoolGet(ctx, id.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundPool = pool - } else if poolName, ok := d.GetOk("name"); ok { - pool, err := util.LBPoolGetByName(ctx, client, poolName.(string), loadbalancerID) - if err != nil { - return diag.FromErr(err) - } - - foundPool = pool - } else { - return diag.Errorf("Error: specify either id or a name to lookup the lb pool") - } - - d.SetId(foundPool.ID) - d.Set("name", foundPool.Name) - d.Set("lb_algorithm", foundPool.LoadbalancerAlgorithm) - d.Set("protocol", foundPool.Protocol) - d.Set("provisioning_status", foundPool.ProvisioningStatus) - d.Set("operating_status", foundPool.OperatingStatus) - d.Set("listener_id", foundPool.Listeners[0].ID) - d.Set("timeout_member_connect", foundPool.TimeoutMemberConnect) - d.Set("timeout_member_data", foundPool.TimeoutMemberData) - d.Set("timeout_client_data", foundPool.TimeoutClientData) - - if err := setHealthMonitor(ctx, d, foundPool); err != nil { - return diag.FromErr(err) - } - - if err := setSessionPersistence(ctx, d, foundPool); err != nil { - return diag.FromErr(err) - } - - if err := setMembers(ctx, d, foundPool); err != nil { - return diag.FromErr(err) - } - - return nil -} diff --git a/edgecenter/lbpool/lbpool.go b/edgecenter/lbpool/lbpool.go deleted file mode 100644 index 0242137e..00000000 --- a/edgecenter/lbpool/lbpool.go +++ /dev/null @@ -1,231 +0,0 @@ -package lbpool - -import ( - "fmt" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func lbpoolSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "name": { - Type: schema.TypeString, - Required: true, - Description: `lb pool name`, - }, - "lb_algorithm": { - Type: schema.TypeString, - Required: true, - Description: fmt.Sprintf( - "algorithm of the load balancer. available values are '%s', '%s', '%s', '%s'", - edgecloud.LoadbalancerAlgorithmRoundRobin, edgecloud.LoadbalancerAlgorithmLeastConnections, - edgecloud.LoadbalancerAlgorithmSourceIP, edgecloud.LoadbalancerAlgorithmSourceIPPort, - ), - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - switch edgecloud.LoadbalancerAlgorithm(v) { - case edgecloud.LoadbalancerAlgorithmRoundRobin, edgecloud.LoadbalancerAlgorithmLeastConnections, edgecloud.LoadbalancerAlgorithmSourceIP, edgecloud.LoadbalancerAlgorithmSourceIPPort: - return diag.Diagnostics{} - } - - return diag.Errorf( - "wrong type %s, available values are '%s', '%s', '%s', '%s'", v, - edgecloud.LoadbalancerAlgorithmRoundRobin, edgecloud.LoadbalancerAlgorithmLeastConnections, - edgecloud.LoadbalancerAlgorithmSourceIP, edgecloud.LoadbalancerAlgorithmSourceIPPort, - ) - }, - }, - "protocol": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: fmt.Sprintf( - "available values are '%s', '%s', '%s', '%s' and '%s'", - edgecloud.LBPoolProtocolHTTP, edgecloud.LBPoolProtocolHTTPS, edgecloud.LBPoolProtocolTCP, - edgecloud.LBPoolProtocolUDP, edgecloud.LBPoolProtocolProxy, - ), - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - switch edgecloud.LoadbalancerPoolProtocol(v) { - case edgecloud.LBPoolProtocolHTTP, edgecloud.LBPoolProtocolHTTPS, edgecloud.LBPoolProtocolTCP, - edgecloud.LBPoolProtocolUDP, edgecloud.LBPoolProtocolProxy: - return diag.Diagnostics{} - default: - return diag.Errorf( - "wrong protocol %s, available values are '%s', '%s', '%s', '%s', '%s'", v, - edgecloud.LBPoolProtocolHTTP, edgecloud.LBPoolProtocolHTTPS, edgecloud.LBPoolProtocolTCP, - edgecloud.LBPoolProtocolUDP, edgecloud.LBPoolProtocolProxy, - ) - } - }, - }, - "loadbalancer_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "ID of the load balancer", - }, - "listener_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "ID of the load balancer listener", - }, - "session_persistence": { - Type: schema.TypeList, - Optional: true, - Computed: true, - MaxItems: 1, - Description: `configuration that enables the load balancer to bind a user's session to a specific backend member. -this ensures that all requests from the user during the session are sent to the same member.`, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - }, - "cookie_name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "persistence_granularity": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "persistence_timeout": { - Type: schema.TypeInt, - Optional: true, - Computed: true, - }, - }, - }, - }, - "timeout_member_connect": { - Type: schema.TypeInt, - Optional: true, - Default: 5000, //nolint: gomnd - Description: "timeout for the backend member connection (in milliseconds)", - }, - "timeout_member_data": { - Type: schema.TypeInt, - Optional: true, - Default: 5000, //nolint: gomnd - Description: "timeout for the backend member inactivity (in milliseconds)", - }, - "timeout_client_data": { - Type: schema.TypeInt, - Optional: true, - Default: 5000, //nolint: gomnd - Description: "timeout for the frontend client inactivity (in milliseconds)", - }, - "healthmonitor": { - Type: schema.TypeList, - Required: true, - MaxItems: 1, - Description: `configuration for health checks to test the health and state of the backend members. -it determines how the load balancer identifies whether the backend members are healthy or unhealthy.`, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "type": { - Type: schema.TypeString, - Required: true, - Description: fmt.Sprintf( - "available values are '%s', '%s', '%s', '%s', '%s', '%s", - edgecloud.HealthMonitorTypeHTTP, edgecloud.HealthMonitorTypeHTTPS, - edgecloud.HealthMonitorTypePING, edgecloud.HealthMonitorTypeTCP, - edgecloud.HealthMonitorTypeTLSHello, edgecloud.HealthMonitorTypeUDPConnect), - ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { - v := val.(string) - switch edgecloud.HealthMonitorType(v) { - case edgecloud.HealthMonitorTypeHTTP, edgecloud.HealthMonitorTypeHTTPS, - edgecloud.HealthMonitorTypePING, edgecloud.HealthMonitorTypeTCP, - edgecloud.HealthMonitorTypeTLSHello, edgecloud.HealthMonitorTypeUDPConnect: - return diag.Diagnostics{} - } - - return diag.Errorf( - "wrong type %s, available values is '%s', '%s', '%s', '%s', '%s', '%s", v, - edgecloud.HealthMonitorTypeHTTP, edgecloud.HealthMonitorTypeHTTPS, - edgecloud.HealthMonitorTypePING, edgecloud.HealthMonitorTypeTCP, - edgecloud.HealthMonitorTypeTLSHello, edgecloud.HealthMonitorTypeUDPConnect, - ) - }, - }, - "timeout": { - Type: schema.TypeInt, - Optional: true, - Default: 5, //nolint: gomnd - Description: "Response time (in sec)", - }, - "delay": { - Type: schema.TypeInt, - Optional: true, - Default: 60, //nolint: gomnd - Description: "check interval (in sec)", - }, - "max_retries": { - Type: schema.TypeInt, - Optional: true, - Default: 10, //nolint: gomnd - Description: "healthy thresholds", - }, - "max_retries_down": { - Type: schema.TypeInt, - Optional: true, - Default: 5, //nolint: gomnd - Description: "unhealthy thresholds", - }, - "http_method": { - Type: schema.TypeString, - Optional: true, - }, - "url_path": { - Type: schema.TypeString, - Optional: true, - }, - "expected_codes": { - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "id": { - Type: schema.TypeString, - Computed: true, - }, - }, - }, - }, - // computed attributes - "id": { - Type: schema.TypeString, - Computed: true, - Description: "lb pool uuid", - }, - "provisioning_status": { - Type: schema.TypeString, - Computed: true, - Description: "lifecycle status of the pool", - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the pool", - }, - } -} diff --git a/edgecenter/lbpool/resource_lbpool.go b/edgecenter/lbpool/resource_lbpool.go deleted file mode 100644 index 0ec96b80..00000000 --- a/edgecenter/lbpool/resource_lbpool.go +++ /dev/null @@ -1,186 +0,0 @@ -package lbpool - -import ( - "context" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" -) - -func ResourceEdgeCenterLbPool() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterLbPoolCreate, - ReadContext: resourceEdgeCenterLbPoolRead, - UpdateContext: resourceEdgeCenterLbPoolUpdate, - DeleteContext: resourceEdgeCenterLbPoolDelete, - Description: `A pool is a list of virtual machines to which the listener will redirect incoming traffic`, - Schema: lbpoolSchema(), - } -} - -func resourceEdgeCenterLbPoolCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - sessionPersistence := converter.ListInterfaceToLoadbalancerSessionPersistence(d.Get("session_persistence").([]interface{})) - healthMonitor := converter.ListInterfaceToHealthMonitor(d.Get("healthmonitor").([]interface{})) - - opts := &edgecloud.PoolCreateRequest{ - LoadbalancerPoolCreateRequest: edgecloud.LoadbalancerPoolCreateRequest{ - Name: d.Get("name").(string), - Protocol: edgecloud.LoadbalancerPoolProtocol(d.Get("protocol").(string)), - LoadbalancerAlgorithm: edgecloud.LoadbalancerAlgorithm(d.Get("lb_algorithm").(string)), - LoadbalancerID: d.Get("loadbalancer_id").(string), - ListenerID: d.Get("listener_id").(string), - TimeoutClientData: d.Get("timeout_client_data").(int), - TimeoutMemberData: d.Get("timeout_member_data").(int), - TimeoutMemberConnect: d.Get("timeout_member_connect").(int), - SessionPersistence: sessionPersistence, - HealthMonitor: healthMonitor, - }, - } - - log.Printf("[DEBUG] Loadbalancer pool create configuration: %#v", opts) - - taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Loadbalancers.PoolCreate, opts, client) - if err != nil { - return diag.Errorf("error creating loadbalancer pool: %s", err) - } - - d.SetId(taskResult.Pools[0]) - - log.Printf("[INFO] Pool: %s", d.Id()) - - return resourceEdgeCenterLbPoolRead(ctx, d, meta) -} - -func resourceEdgeCenterLbPoolRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - // Retrieve the loadbalancer pool properties for updating the state - pool, resp, err := client.Loadbalancers.PoolGet(ctx, d.Id()) - if err != nil { - // check if the loadbalancer pool no longer exists. - if resp != nil && resp.StatusCode == 404 { - log.Printf("[WARN] EdgeCenter Pool (%s) not found", d.Id()) - d.SetId("") - return nil - } - - return diag.Errorf("Error retrieving loadbalancer pool: %s", err) - } - - d.Set("name", pool.Name) - d.Set("lb_algorithm", pool.LoadbalancerAlgorithm) - d.Set("protocol", pool.Protocol) - d.Set("timeout_member_connect", pool.TimeoutMemberConnect) - d.Set("timeout_member_data", pool.TimeoutMemberData) - d.Set("timeout_client_data", pool.TimeoutClientData) - d.Set("provisioning_status", pool.ProvisioningStatus) - d.Set("operating_status", pool.OperatingStatus) - - if len(pool.Loadbalancers) > 0 { - d.Set("loadbalancer_id", pool.Loadbalancers[0].ID) - } - - if len(pool.Listeners) > 0 { - d.Set("listener_id", pool.Listeners[0].ID) - } - - if err := setHealthMonitor(ctx, d, pool); err != nil { - return diag.FromErr(err) - } - - if err := setSessionPersistence(ctx, d, pool); err != nil { - return diag.FromErr(err) - } - - return nil -} - -func resourceEdgeCenterLbPoolUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - var changed bool - opts := &edgecloud.PoolUpdateRequest{ - Name: d.Get("name").(string), - HealthMonitor: converter.ListInterfaceToHealthMonitor(d.Get("healthmonitor").([]interface{})), - } - - if d.HasChange("name") || d.HasChange("healthmonitor") { - changed = true - } - - if d.HasChange("timeout_client_data") { - opts.TimeoutClientData = d.Get("timeout_client_data").(int) - changed = true - } - - if d.HasChange("timeout_member_data") { - opts.TimeoutMemberData = d.Get("timeout_member_data").(int) - changed = true - } - - if d.HasChange("timeout_member_connect") { - opts.TimeoutMemberConnect = d.Get("timeout_member_connect").(int) - changed = true - } - - if d.HasChange("lb_algorithm") { - opts.LoadbalancerAlgorithm = edgecloud.LoadbalancerAlgorithm(d.Get("lb_algorithm").(string)) - changed = true - } - - if d.HasChange("session_persistence") { - opts.SessionPersistence = converter.ListInterfaceToLoadbalancerSessionPersistence(d.Get("session_persistence").([]interface{})) - changed = true - } - - if changed { - task, _, err := client.Loadbalancers.PoolUpdate(ctx, d.Id(), opts) - if err != nil { - return diag.Errorf("Error when changing the loadbalancer pool: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Error while waiting for loadbalancer pool update: %s", err) - } - } - - return resourceEdgeCenterLbPoolRead(ctx, d, meta) -} - -func resourceEdgeCenterLbPoolDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - log.Printf("[INFO] Deleting loadbalancer pool: %s", d.Id()) - task, _, err := client.Loadbalancers.PoolDelete(ctx, d.Id()) - if err != nil { - return diag.Errorf("Error deleting loadbalancer pool: %s", err) - } - - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.Errorf("Delete loadbalancer pool task failed with error: %s", err) - } - - if err = util.ResourceIsDeleted(ctx, client.Loadbalancers.PoolGet, d.Id()); err != nil { - return diag.Errorf("Loadbalancer pool with id %s was not deleted: %s", d.Id(), err) - } - - d.SetId("") - - return nil -} diff --git a/edgecenter/lbpool/set.go b/edgecenter/lbpool/set.go deleted file mode 100644 index c0a8f2f6..00000000 --- a/edgecenter/lbpool/set.go +++ /dev/null @@ -1,54 +0,0 @@ -package lbpool - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func setHealthMonitor(_ context.Context, d *schema.ResourceData, pool *edgecloud.Pool) error { - healthMonitor := []map[string]interface{}{{ - "id": pool.HealthMonitor.ID, - "type": pool.HealthMonitor.Type, - "delay": pool.HealthMonitor.Delay, - "timeout": pool.HealthMonitor.Timeout, - "max_retries": pool.HealthMonitor.MaxRetries, - "max_retries_down": pool.HealthMonitor.MaxRetriesDown, - "url_path": pool.HealthMonitor.URLPath, - "expected_codes": pool.HealthMonitor.ExpectedCodes, - }} - - return d.Set("healthmonitor", healthMonitor) -} - -func setSessionPersistence(_ context.Context, d *schema.ResourceData, pool *edgecloud.Pool) error { - sessionPersistence := []map[string]interface{}{{ - "type": pool.SessionPersistence.Type, - "cookie_name": pool.SessionPersistence.CookieName, - "persistence_timeout": pool.SessionPersistence.PersistenceTimeout, - "persistence_granularity": pool.SessionPersistence.PersistenceGranularity, - }} - - return d.Set("session_persistence", sessionPersistence) -} - -func setMembers(_ context.Context, d *schema.ResourceData, pool *edgecloud.Pool) error { - members := make([]map[string]interface{}, 0, len(pool.Members)) - for _, m := range pool.Members { - member := map[string]interface{}{ - "id": m.ID, - "operating_status": m.OperatingStatus, - "weight": m.Weight, - "address": m.Address.String(), - "protocol_port": m.ProtocolPort, - "subnet_id": m.SubnetID, - "instance_id": m.InstanceID, - "admin_state_up": m.AdminStateUP, - } - members = append(members, member) - } - - return d.Set("member", members) -} diff --git a/edgecenter/loadbalancer/change.go b/edgecenter/loadbalancer/change.go deleted file mode 100644 index 4b2fa92a..00000000 --- a/edgecenter/loadbalancer/change.go +++ /dev/null @@ -1,33 +0,0 @@ -package loadbalancer - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func changeFloatingIP(ctx context.Context, d *schema.ResourceData, client *edgecloud.Client) error { - oldFipRaw, newFipRaw := d.GetChange("floating_ip") - oldFip, newFip := oldFipRaw.(string), newFipRaw.(string) - - if oldFip != "" { - if _, _, err := client.Floatingips.UnAssign(ctx, oldFip); err != nil { - return fmt.Errorf("error while unassign fip from loadbalancer: %w", err) - } - } - - if newFip != "" { - assignFloatingIPRequest := &edgecloud.AssignFloatingIPRequest{ - PortID: d.Get("vip_port_id").(string), - } - - if _, _, err := client.Floatingips.Assign(ctx, newFip, assignFloatingIPRequest); err != nil { - return fmt.Errorf("error while assign fip to loadbalancer: %w", err) - } - } - - return nil -} diff --git a/edgecenter/loadbalancer/datasource_loadbalancer.go b/edgecenter/loadbalancer/datasource_loadbalancer.go deleted file mode 100644 index be44c6f5..00000000 --- a/edgecenter/loadbalancer/datasource_loadbalancer.go +++ /dev/null @@ -1,170 +0,0 @@ -package loadbalancer - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func DataSourceEdgeCenterLoadbalancer() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceEdgeCenterLoadbalancerRead, - Description: `A loadbalancer is a software service that distributes incoming network traffic -(e.g., web traffic, application requests) across multiple servers or resources.`, - - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "id": { - Type: schema.TypeString, - Optional: true, - Description: "loadbalancer uuid", - ValidateFunc: validation.IsUUID, - ExactlyOneOf: []string{"id", "name"}, - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: `loadbalancer name. this parameter is not unique, if there is more than one loadbalancer with the same name, -then the first one will be used. it is recommended to use "id"`, - ExactlyOneOf: []string{"id", "name"}, - }, - // computed attributes - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "provisioning_status": { - Type: schema.TypeString, - Computed: true, - Description: "lifecycle status of the load balancer", - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the load balancer", - }, - "vip_port_id": { - Type: schema.TypeString, - Computed: true, - Description: "IP port of the load balancer", - }, - "vip_network_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the network that the subnet belongs to. the port will be plugged in this network", - }, - "vrrp_ips": { - Type: schema.TypeList, - Computed: true, - Description: "list of VRRP IP addresses", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "vip_address": { - Type: schema.TypeString, - Computed: true, - Description: "loadbalancer IP address", - }, - "flavor": { - Type: schema.TypeMap, - Computed: true, - Description: "information about the flavor", - }, - "floating_ip": { - Type: schema.TypeMap, - Computed: true, - Description: "information about the assigned floating IP", - }, - "metadata_detailed": { - Type: schema.TypeList, - Computed: true, - Description: "metadata in detailed format", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - "read_only": { - Type: schema.TypeBool, - Computed: true, - }, - }, - }, - }, - }, - } -} - -func dataSourceEdgeCenterLoadbalancerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - var foundLoadbalancer *edgecloud.Loadbalancer - - if id, ok := d.GetOk("id"); ok { - loadbalancer, _, err := client.Loadbalancers.Get(ctx, id.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundLoadbalancer = loadbalancer - } else if loadbalancerName, ok := d.GetOk("name"); ok { - loadbalancer, err := util.LoadbalancerGetByName(ctx, client, loadbalancerName.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundLoadbalancer = loadbalancer - } else { - return diag.Errorf("Error: specify either id or a name to lookup the loadbalancer") - } - - d.SetId(foundLoadbalancer.ID) - d.Set("name", foundLoadbalancer.Name) - - d.Set("region", foundLoadbalancer.Region) - d.Set("provisioning_status", foundLoadbalancer.ProvisioningStatus) - d.Set("operating_status", foundLoadbalancer.OperatingStatus) - d.Set("vip_port_id", foundLoadbalancer.VipPortID) - d.Set("vip_network_id", foundLoadbalancer.VipNetworkID) - d.Set("vip_address", foundLoadbalancer.VipAddress.String()) - - if err := setMetadataDetailed(ctx, d, foundLoadbalancer); err != nil { - return diag.FromErr(err) - } - - if err := setFlavor(ctx, d, foundLoadbalancer); err != nil { - return diag.FromErr(err) - } - - if err := setVRRPIPs(ctx, d, foundLoadbalancer); err != nil { - return diag.FromErr(err) - } - - if err := setFloatingIP(ctx, d, foundLoadbalancer); err != nil { - return diag.FromErr(err) - } - - return nil -} diff --git a/edgecenter/loadbalancer/loadbalancer.go b/edgecenter/loadbalancer/loadbalancer.go deleted file mode 100644 index d7bdc42d..00000000 --- a/edgecenter/loadbalancer/loadbalancer.go +++ /dev/null @@ -1,112 +0,0 @@ -package loadbalancer - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func loadbalancerSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the region", - }, - "name": { - Type: schema.TypeString, - Required: true, - Description: "name of the load balancer", - }, - "flavor_name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "flavor name of the load balancer", - }, - "vip_port_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"vip_network_id"}, - Description: "ID of the existing reserved fixed IP port for the load balancer", - }, - "vip_network_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"vip_port_id"}, - Description: "ID of the Network. шf not specified, the default external network will be used", - }, - "vip_subnet_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - RequiredWith: []string{"vip_network_id"}, - Description: "ID of the subnet. if not specified, any subnet from vip_network_id will be selected", - }, - "metadata": { - Type: schema.TypeMap, - Optional: true, - Description: "map containing metadata, for example tags.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "floating_ip_source": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "floating IP type: 'existing' or 'new'", - RequiredWith: []string{"vip_network_id"}, - }, - "floating_ip": { - Type: schema.TypeString, - ValidateFunc: validation.IsUUID, - Optional: true, - Computed: true, - Description: "floating IP for this subnet attachment", - }, - // computed attributes - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "vip_address": { - Type: schema.TypeString, - Computed: true, - Description: "IP address of the load balancer", - }, - "provisioning_status": { - Type: schema.TypeString, - Computed: true, - Description: "lifecycle status of the load balancer", - }, - "operating_status": { - Type: schema.TypeString, - Computed: true, - Description: "operating status of the load balancer", - }, - "vrrp_ips": { - Type: schema.TypeList, - Computed: true, - Description: "list of VRRP IP addresses", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "flavor": { - Type: schema.TypeMap, - Computed: true, - Description: "information about the flavor", - }, - } -} diff --git a/edgecenter/loadbalancer/resource_loadbalancer.go b/edgecenter/loadbalancer/resource_loadbalancer.go deleted file mode 100644 index 92079533..00000000 --- a/edgecenter/loadbalancer/resource_loadbalancer.go +++ /dev/null @@ -1,159 +0,0 @@ -package loadbalancer - -import ( - "context" - "log" - "time" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" -) - -func ResourceEdgeCenterLoadbalancer() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterLoadbalancerCreate, - ReadContext: resourceEdgeCenterLoadbalancerRead, - UpdateContext: resourceEdgeCenterLoadbalancerUpdate, - DeleteContext: resourceEdgeCenterLoadbalancerDelete, - Description: `A loadbalancer is a software service that distributes incoming network traffic -(e.g., web traffic, application requests) across multiple servers or resources.`, - Schema: loadbalancerSchema(), - } -} - -func resourceEdgeCenterLoadbalancerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - opts := &edgecloud.LoadbalancerCreateRequest{ - Name: d.Get("name").(string), - Flavor: d.Get("flavor_name").(string), - VipPortID: d.Get("vip_port_id").(string), - VipNetworkID: d.Get("vip_network_id").(string), - VipSubnetID: d.Get("vip_subnet_id").(string), - } - - switch d.Get("floating_ip_source").(string) { - case "new": - opts.FloatingIP = &edgecloud.InterfaceFloatingIP{ - Source: edgecloud.NewFloatingIP, - } - case "existing": - opts.FloatingIP = &edgecloud.InterfaceFloatingIP{ - Source: edgecloud.ExistingFloatingIP, - ExistingFloatingID: d.Get("floating_ip").(string), - } - default: - opts.FloatingIP = nil - } - - if v, ok := d.GetOk("metadata"); ok { - metadata := converter.MapInterfaceToMapString(v.(map[string]interface{})) - opts.Metadata = metadata - } - - log.Printf("[DEBUG] Loadbalancer create configuration: %#v", opts) - - taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Loadbalancers.Create, opts, client, 2*time.Minute) //nolint: gomnd - if err != nil { - return diag.Errorf("error creating loadbalancer: %s", err) - } - - d.SetId(taskResult.Loadbalancers[0]) - - log.Printf("[INFO] Loadbalancer: %s", d.Id()) - - return resourceEdgeCenterLoadbalancerRead(ctx, d, meta) -} - -func resourceEdgeCenterLoadbalancerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - // Retrieve the loadbalancer properties for updating the state - loadbalancer, resp, err := client.Loadbalancers.Get(ctx, d.Id()) - if err != nil { - // check if the loadbalancer no longer exists. - if resp != nil && resp.StatusCode == 404 { - log.Printf("[WARN] EdgeCenter Loadbalancer (%s) not found", d.Id()) - d.SetId("") - return nil - } - - return diag.Errorf("Error retrieving loadbalancer: %s", err) - } - - d.Set("name", loadbalancer.Name) - d.Set("region", loadbalancer.Region) - d.Set("vip_address", loadbalancer.VipAddress.String()) - d.Set("provisioning_status", loadbalancer.ProvisioningStatus) - d.Set("operating_status", loadbalancer.OperatingStatus) - d.Set("vip_network_id", loadbalancer.VipNetworkID) - d.Set("vip_port_id", loadbalancer.VipPortID) - - if len(loadbalancer.FloatingIPs) > 0 { - d.Set("floating_ip", loadbalancer.FloatingIPs[0].ID) - } - - if err := setVRRPIPs(ctx, d, loadbalancer); err != nil { - return diag.FromErr(err) - } - - if err := setFlavor(ctx, d, loadbalancer); err != nil { - return diag.FromErr(err) - } - - // TODO need to add metadataDetailed to Loadbalancers.Get resp - - return nil -} - -func resourceEdgeCenterLoadbalancerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - if d.HasChange("name") { - newName := d.Get("name").(string) - if _, _, err := client.Loadbalancers.Rename(ctx, d.Id(), &edgecloud.Name{Name: newName}); err != nil { - return diag.Errorf("Error when renaming the loadbalancer: %s", err) - } - } - - if d.HasChange("metadata") { - metadata := edgecloud.Metadata(converter.MapInterfaceToMapString(d.Get("metadata").(map[string]interface{}))) - - if _, err := client.Loadbalancers.MetadataUpdate(ctx, d.Id(), &metadata); err != nil { - return diag.Errorf("cannot update metadata. error: %s", err) - } - } - - if d.HasChange("floating_ip") { - if err := changeFloatingIP(ctx, d, client); err != nil { - return diag.FromErr(err) - } - } - - return resourceEdgeCenterLoadbalancerRead(ctx, d, meta) -} - -func resourceEdgeCenterLoadbalancerDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - log.Printf("[INFO] Deleting loadbalancer: %s", d.Id()) - if err := util.DeleteResourceIfExist(ctx, client, client.Loadbalancers, d.Id()); err != nil { - return diag.Errorf("Error deleting loadbalancer: %s", err) - } - d.SetId("") - - return nil -} diff --git a/edgecenter/loadbalancer/set.go b/edgecenter/loadbalancer/set.go deleted file mode 100644 index 06bf642b..00000000 --- a/edgecenter/loadbalancer/set.go +++ /dev/null @@ -1,68 +0,0 @@ -package loadbalancer - -import ( - "context" - "strconv" - - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func setFlavor(_ context.Context, d *schema.ResourceData, loadbalancer *edgecloud.Loadbalancer) error { - flavor := map[string]interface{}{ - "flavor_name": loadbalancer.Flavor.FlavorName, - "vcpus": strconv.Itoa(loadbalancer.Flavor.VCPUS), - "ram": strconv.Itoa(loadbalancer.Flavor.RAM), - "flavor_id": loadbalancer.Flavor.FlavorID, - } - - return d.Set("flavor", flavor) -} - -func setMetadataDetailed(_ context.Context, d *schema.ResourceData, loadbalancer *edgecloud.Loadbalancer) error { - if len(loadbalancer.MetadataDetailed) > 0 { - metadata := make([]map[string]interface{}, 0, len(loadbalancer.MetadataDetailed)) - for _, metadataItem := range loadbalancer.MetadataDetailed { - metadata = append(metadata, map[string]interface{}{ - "key": metadataItem.Key, - "value": metadataItem.Value, - "read_only": metadataItem.ReadOnly, - }) - } - - return d.Set("metadata_detailed", metadata) - } - - return nil -} - -func setVRRPIPs(_ context.Context, d *schema.ResourceData, loadbalancer *edgecloud.Loadbalancer) error { - if len(loadbalancer.VrrpIPs) > 0 { - vrrpIPs := make([]string, 0, len(loadbalancer.VrrpIPs)) - for _, v := range loadbalancer.VrrpIPs { - vrrpIPs = append(vrrpIPs, v.VrrpIPAddress) - } - return d.Set("vrrp_ips", vrrpIPs) - } - - return nil -} - -func setFloatingIP(_ context.Context, d *schema.ResourceData, loadbalancer *edgecloud.Loadbalancer) error { - if len(loadbalancer.FloatingIPs) > 0 { - floatingIP := loadbalancer.FloatingIPs[0] - fip := map[string]interface{}{ - "status": floatingIP.Status, - "id": floatingIP.ID, - "fixed_ip_address": floatingIP.FixedIPAddress.String(), - "floating_ip_address": floatingIP.FloatingIPAddress, - "router_id": floatingIP.RouterID, - "port_id": floatingIP.PortID, - } - - return d.Set("floating_ip", fip) - } - - return nil -} diff --git a/edgecenter/metadata.go b/edgecenter/metadata.go new file mode 100644 index 00000000..15b25ed9 --- /dev/null +++ b/edgecenter/metadata.go @@ -0,0 +1,39 @@ +package edgecenter + +import "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" + +func PrepareMetadata(apiMetadata []metadata.Metadata) (map[string]string, []map[string]interface{}) { + metadataMap := make(map[string]string) + metadataReadOnly := make([]map[string]interface{}, 0, len(apiMetadata)) + + if len(apiMetadata) > 0 { + for _, metadataItem := range apiMetadata { + if !metadataItem.ReadOnly { + metadataMap[metadataItem.Key] = metadataItem.Value + } + metadataReadOnly = append(metadataReadOnly, map[string]interface{}{ + "key": metadataItem.Key, + "value": metadataItem.Value, + "read_only": metadataItem.ReadOnly, + }) + } + } + + return metadataMap, metadataReadOnly +} + +func PrepareMetadataReadonly(apiMetadata []metadata.Metadata) []map[string]interface{} { + metadataReadOnly := make([]map[string]interface{}, 0, len(apiMetadata)) + + if len(apiMetadata) > 0 { + for _, metadataItem := range apiMetadata { + metadataReadOnly = append(metadataReadOnly, map[string]interface{}{ + "key": metadataItem.Key, + "value": metadataItem.Value, + "read_only": metadataItem.ReadOnly, + }) + } + } + + return metadataReadOnly +} diff --git a/edgecenter/provider.go b/edgecenter/provider.go index 4527b6cc..0c02bad9 100644 --- a/edgecenter/provider.go +++ b/edgecenter/provider.go @@ -2,30 +2,93 @@ package edgecenter import ( "context" + "fmt" + "log" + "net/http" + "net/url" + "os" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/floatingip" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/instance" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/lblistener" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/lbmember" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/lbpool" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/loadbalancer" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/volume" + dnssdk "github.com/Edge-Center/edgecenter-dns-sdk-go" + storageSDK "github.com/Edge-Center/edgecenter-storage-sdk-go" + cdn "github.com/Edge-Center/edgecentercdn-go" + eccdnProvider "github.com/Edge-Center/edgecentercdn-go/edgecenter/provider" + edgecloud "github.com/Edge-Center/edgecentercloud-go" + ec "github.com/Edge-Center/edgecentercloud-go/edgecenter" +) + +const ( + ProviderOptPermanentToken = "permanent_api_token" + ProviderOptSkipCredsAuthErr = "ignore_creds_auth_error" // nolint: gosec + ProviderOptSingleAPIEndpoint = "api_endpoint" + + LifecyclePolicyResource = "edgecenter_lifecyclepolicy" ) -// Provider returns a schema.Provider for Edgecenter. func Provider() *schema.Provider { p := &schema.Provider{ Schema: map[string]*schema.Schema{ - "api_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"EC_PERMANENT_TOKEN", "API_KEY"}, nil), + "user_name": { + Type: schema.TypeString, + Optional: true, + // commented because it's broke all tests + // AtLeastOneOf: []string{ProviderOptPermanentToken, "user_name"}, + // RequiredWith: []string{"user_name", "password"}, + Deprecated: fmt.Sprintf("Use %s instead", ProviderOptPermanentToken), + DefaultFunc: schema.EnvDefaultFunc("EC_USERNAME", nil), + }, + "password": { + Type: schema.TypeString, + Optional: true, + // commented because it's broke all tests + // RequiredWith: []string{"user_name", "password"}, + Deprecated: fmt.Sprintf("Use %s instead", ProviderOptPermanentToken), + DefaultFunc: schema.EnvDefaultFunc("EC_PASSWORD", nil), + }, + ProviderOptPermanentToken: { + Type: schema.TypeString, + Optional: true, + // commented because it's broke all tests + // AtLeastOneOf: []string{ProviderOptPermanentToken, "user_name"}, Sensitive: true, Description: "A permanent [API-token](https://support.edgecenter.ru/knowledge_base/item/257788)", + DefaultFunc: schema.EnvDefaultFunc("EC_PERMANENT_TOKEN", nil), + }, + ProviderOptSingleAPIEndpoint: { + Type: schema.TypeString, + Optional: true, + Description: "A single API endpoint for all products. Will be used when specific product API url is not defined.", + DefaultFunc: schema.EnvDefaultFunc("EC_API_ENDPOINT", "https://api.edgecenter.ru"), + }, + ProviderOptSkipCredsAuthErr: { + Type: schema.TypeBool, + Optional: true, + Deprecated: "It doesn't make any effect anymore", + Description: "Should be set to true when you are gonna to use storage resource with permanent API-token only.", + }, + "edgecenter_platform": { + Type: schema.TypeString, + Optional: true, + Deprecated: "Use edgecenter_platform_api instead", + ConflictsWith: []string{"edgecenter_platform_api"}, + Description: "Platform URL is used for generate JWT", + DefaultFunc: schema.EnvDefaultFunc("EC_PLATFORM", nil), + }, + "edgecenter_platform_api": { + Type: schema.TypeString, + Optional: true, + Description: "Platform URL is used for generate JWT (define only if you want to override Platform API endpoint)", + DefaultFunc: schema.EnvDefaultFunc("EC_PLATFORM_API", nil), + }, + "edgecenter_api": { + Type: schema.TypeString, + Optional: true, + Deprecated: "Use edgecenter_cloud_api instead", + ConflictsWith: []string{"edgecenter_cloud_api"}, + Description: "Region API", + DefaultFunc: schema.EnvDefaultFunc("EC_API", nil), }, "edgecenter_cloud_api": { Type: schema.TypeString, @@ -33,23 +96,85 @@ func Provider() *schema.Provider { Description: "Region API (define only if you want to override Region API endpoint)", DefaultFunc: schema.EnvDefaultFunc("EC_CLOUD_API", nil), }, - }, - DataSourcesMap: map[string]*schema.Resource{ - "edgecenter_floatingip": floatingip.DataSourceEdgeCenterFloatingIP(), - "edgecenter_instance": instance.DataSourceEdgeCenterInstance(), - "edgecenter_lblistener": lblistener.DataSourceEdgeCenterLbListener(), - "edgecenter_lbpool": lbpool.DataSourceEdgeCenterLbPool(), - "edgecenter_loadbalancer": loadbalancer.DataSourceEdgeCenterLoadbalancer(), - "edgecenter_volume": volume.DataSourceEdgeCenterVolume(), + "edgecenter_cdn_api": { + Type: schema.TypeString, + Optional: true, + Description: "CDN API (define only if you want to override CDN API endpoint)", + DefaultFunc: schema.EnvDefaultFunc("EC_CDN_API", ""), + }, + "edgecenter_storage_api": { + Type: schema.TypeString, + Optional: true, + Description: "Storage API (define only if you want to override Storage API endpoint)", + DefaultFunc: schema.EnvDefaultFunc("EC_STORAGE_API", ""), + }, + "edgecenter_dns_api": { + Type: schema.TypeString, + Optional: true, + Description: "DNS API (define only if you want to override DNS API endpoint)", + DefaultFunc: schema.EnvDefaultFunc("EC_DNS_API", ""), + }, + "edgecenter_client_id": { + Type: schema.TypeString, + Optional: true, + Description: "Client id", + DefaultFunc: schema.EnvDefaultFunc("EC_CLIENT_ID", ""), + }, }, ResourcesMap: map[string]*schema.Resource{ - "edgecenter_floatingip": floatingip.ResourceEdgeCenterFloatingIP(), - "edgecenter_instance": instance.ResourceEdgeCenterInstance(), - "edgecenter_lblistener": lblistener.ResourceEdgeCenterLbListener(), - "edgecenter_lbpool": lbpool.ResourceEdgeCenterLbPool(), - "edgecenter_lbmember": lbmember.ResourceEdgeCenterLbMember(), - "edgecenter_loadbalancer": loadbalancer.ResourceEdgeCenterLoadbalancer(), - "edgecenter_volume": volume.ResourceEdgeCenterVolume(), + "edgecenter_volume": resourceVolume(), + "edgecenter_network": resourceNetwork(), + "edgecenter_subnet": resourceSubnet(), + "edgecenter_router": resourceRouter(), + "edgecenter_instance": resourceInstance(), + "edgecenter_keypair": resourceKeypair(), + "edgecenter_reservedfixedip": resourceReservedFixedIP(), + "edgecenter_floatingip": resourceFloatingIP(), + "edgecenter_loadbalancer": resourceLoadBalancer(), + "edgecenter_loadbalancerv2": resourceLoadBalancerV2(), + "edgecenter_lblistener": resourceLbListener(), + "edgecenter_lbpool": resourceLBPool(), + "edgecenter_lbmember": resourceLBMember(), + "edgecenter_securitygroup": resourceSecurityGroup(), + "edgecenter_baremetal": resourceBmInstance(), + "edgecenter_snapshot": resourceSnapshot(), + "edgecenter_servergroup": resourceServerGroup(), + "edgecenter_k8s": resourceK8s(), + "edgecenter_k8s_pool": resourceK8sPool(), + "edgecenter_secret": resourceSecret(), + "edgecenter_storage_s3": resourceStorageS3(), + "edgecenter_storage_s3_bucket": resourceStorageS3Bucket(), + DNSZoneResource: resourceDNSZone(), + DNSZoneRecordResource: resourceDNSZoneRecord(), + "edgecenter_cdn_resource": resourceCDNResource(), + "edgecenter_cdn_origingroup": resourceCDNOriginGroup(), + "edgecenter_cdn_rule": resourceCDNRule(), + "edgecenter_cdn_sslcert": resourceCDNCert(), + LifecyclePolicyResource: resourceLifecyclePolicy(), + }, + DataSourcesMap: map[string]*schema.Resource{ + "edgecenter_project": dataSourceProject(), + "edgecenter_region": dataSourceRegion(), + "edgecenter_securitygroup": dataSourceSecurityGroup(), + "edgecenter_image": dataSourceImage(), + "edgecenter_volume": dataSourceVolume(), + "edgecenter_network": dataSourceNetwork(), + "edgecenter_subnet": dataSourceSubnet(), + "edgecenter_router": dataSourceRouter(), + "edgecenter_loadbalancer": dataSourceLoadBalancer(), + "edgecenter_loadbalancerv2": dataSourceLoadBalancerV2(), + "edgecenter_lblistener": dataSourceLBListener(), + "edgecenter_lbpool": dataSourceLBPool(), + "edgecenter_instance": dataSourceInstance(), + "edgecenter_floatingip": dataSourceFloatingIP(), + "edgecenter_storage_s3": dataSourceStorageS3(), + "edgecenter_storage_s3_bucket": dataSourceStorageS3Bucket(), + "edgecenter_reservedfixedip": dataSourceReservedFixedIP(), + "edgecenter_servergroup": dataSourceServerGroup(), + "edgecenter_k8s": dataSourceK8s(), + "edgecenter_k8s_pool": dataSourceK8sPool(), + "edgecenter_k8s_client_config": dataSourceK8sClientConfig(), + "edgecenter_secret": dataSourceSecret(), }, } @@ -65,11 +190,115 @@ func Provider() *schema.Provider { } func providerConfigure(_ context.Context, d *schema.ResourceData, terraformVersion string) (interface{}, diag.Diagnostics) { - conf := config.Config{ - TerraformVersion: terraformVersion, - APIKey: d.Get("api_key").(string), - CloudAPIURL: d.Get("edgecenter_cloud_api").(string), + username := d.Get("user_name").(string) + password := d.Get("password").(string) + permanentToken := d.Get(ProviderOptPermanentToken).(string) + apiEndpoint := d.Get(ProviderOptSingleAPIEndpoint).(string) + + cloudAPI := d.Get("edgecenter_cloud_api").(string) + if cloudAPI == "" { + cloudAPI = d.Get("edgecenter_api").(string) + } + if cloudAPI == "" { + cloudAPI = apiEndpoint + "/cloud" + } + + cdnAPI := d.Get("edgecenter_cdn_api").(string) + if cdnAPI == "" { + cdnAPI = apiEndpoint + } + + storageAPI := d.Get("edgecenter_storage_api").(string) + if storageAPI == "" { + storageAPI = apiEndpoint + "/storage" + } + + dnsAPI := d.Get("edgecenter_dns_api").(string) + if dnsAPI == "" { + dnsAPI = apiEndpoint + "/dns" + } + + platform := d.Get("edgecenter_platform_api").(string) + if platform == "" { + platform = d.Get("edgecenter_platform").(string) + } + if platform == "" { + platform = apiEndpoint + "/iam" + } + + clientID := d.Get("edgecenter_client_id").(string) + + var diags diag.Diagnostics + + var err error + var provider *edgecloud.ProviderClient + if permanentToken != "" { + provider, err = ec.APITokenClient(edgecloud.APITokenOptions{ + APIURL: cloudAPI, + APIToken: permanentToken, + }) + } else { + provider, err = ec.AuthenticatedClient(edgecloud.AuthOptions{ + APIURL: cloudAPI, + AuthURL: platform, + Username: username, + Password: password, + AllowReauth: true, + ClientID: clientID, + }) + } + if err != nil { + provider = &edgecloud.ProviderClient{} + log.Printf("[WARN] init auth client: %s\n", err) + } + + cdnProvider := eccdnProvider.NewClient(cdnAPI, eccdnProvider.WithSignerFunc(func(req *http.Request) error { + for k, v := range provider.AuthenticatedHeaders() { + req.Header.Set(k, v) + } + + return nil + })) + cdnService := cdn.NewService(cdnProvider) + + config := Config{ + Provider: provider, + CDNClient: cdnService, + } + + userAgent := fmt.Sprintf("terraform/%s", terraformVersion) + if storageAPI != "" { + stHost, stPath, err := ExtractHostAndPath(storageAPI) + if err != nil { + return nil, diag.FromErr(fmt.Errorf("storage api url: %w", err)) + } + config.StorageClient = storageSDK.NewSDK( + stHost, + stPath, + storageSDK.WithBearerAuth(provider.AccessToken), + storageSDK.WithPermanentTokenAuth(func() string { return permanentToken }), + storageSDK.WithUserAgent(userAgent), + ) + } + if dnsAPI != "" { + baseURL, err := url.Parse(dnsAPI) + if err != nil { + return nil, diag.FromErr(fmt.Errorf("dns api url: %w", err)) + } + authorizer := dnssdk.BearerAuth(provider.AccessToken()) + if permanentToken != "" { + authorizer = dnssdk.PermanentAPIKeyAuth(permanentToken) + } + config.DNSClient = dnssdk.NewClient( + authorizer, + func(client *dnssdk.Client) { + client.BaseURL = baseURL + client.Debug = os.Getenv("TF_LOG") == "DEBUG" + }, + func(client *dnssdk.Client) { + client.UserAgent = userAgent + }) } - return conf.Client() + return &config, diags } diff --git a/edgecenter/resource_edgecenter_baremetal.go b/edgecenter/resource_edgecenter_baremetal.go new file mode 100644 index 00000000..d0ead172 --- /dev/null +++ b/edgecenter/resource_edgecenter_baremetal.go @@ -0,0 +1,772 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "sort" + "strconv" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/baremetal/v1/bminstances" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/instances" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + BmInstanceDeleting int = 1200 + BmInstanceCreatingTimeout int = 3600 + BmInstancePoint = "bminstances" +) + +var bmCreateTimeout = time.Second * time.Duration(BmInstanceCreatingTimeout) + +func resourceBmInstance() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceBmInstanceCreate, + ReadContext: resourceBmInstanceRead, + UpdateContext: resourceBmInstanceUpdate, + DeleteContext: resourceBmInstanceDelete, + Description: "Represent baremetal instance", + Timeouts: &schema.ResourceTimeout{ + Create: &bmCreateTimeout, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, InstanceID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(InstanceID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "flavor_id": { + Type: schema.TypeString, + Required: true, + }, + "interface": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available value is '%s', '%s', '%s', '%s'", types.SubnetInterfaceType, types.AnySubnetInterfaceType, types.ExternalInterfaceType, types.ReservedFixedIPType), + }, + "is_parent": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: "If not set will be calculated after creation. Trunk interface always attached first. Can't detach interface if is_parent true. Fields affect only on creation", + }, + "order": { + Type: schema.TypeInt, + Optional: true, + Description: "Order of attaching interface. Trunk interface always attached first, fields affect only on creation", + }, + "network_id": { + Type: schema.TypeString, + Description: "required if type is 'subnet' or 'any_subnet'", + Optional: true, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Description: "required if type is 'subnet'", + Optional: true, + Computed: true, + }, + "port_id": { + Type: schema.TypeString, + Computed: true, + Description: "required if type is 'reserved_fixed_ip'", + Optional: true, + }, + // nested map is not supported, in this case, you do not need to use the list for the map + "fip_source": { + Type: schema.TypeString, + Optional: true, + }, + "existing_fip_id": { + Type: schema.TypeString, + Optional: true, + }, + "ip_address": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + }, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The name of the baremetal instance.", + }, + "name_templates": { + Type: schema.TypeList, + Optional: true, + Deprecated: "Use name_template instead", + ConflictsWith: []string{"name_template"}, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name_template": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"name_templates"}, + }, + "image_id": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{ + "image_id", + "apptemplate_id", + }, + }, + "apptemplate_id": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{ + "image_id", + "apptemplate_id", + }, + }, + "keypair_name": { + Type: schema.TypeString, + Optional: true, + }, + "password": { + Type: schema.TypeString, + Optional: true, + }, + "username": { + Type: schema.TypeString, + Optional: true, + }, + "metadata": { + Type: schema.TypeList, + Optional: true, + Deprecated: "Use metadata_map instead", + ConflictsWith: []string{"metadata_map"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + ConflictsWith: []string{"metadata"}, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "app_config": { + Type: schema.TypeMap, + Optional: true, + }, + "user_data": { + Type: schema.TypeString, + Optional: true, + }, + + // computed + "flavor": { + Type: schema.TypeMap, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "vm_state": { + Type: schema.TypeString, + Computed: true, + }, + "addresses": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "net": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "addr": { + Type: schema.TypeString, + Required: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceBmInstanceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start BaremetalInstance creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, BmInstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + ifs := d.Get("interface").([]interface{}) + // sort interfaces by 'is_parent' at first and by 'order' key to attach it in right order + sort.Sort(instanceInterfaces(ifs)) + interfaceOptsList := make([]bminstances.InterfaceOpts, len(ifs)) + for i, iFace := range ifs { + raw := iFace.(map[string]interface{}) + interfaceOpts := bminstances.InterfaceOpts{ + Type: types.InterfaceType(raw["type"].(string)), + NetworkID: raw["network_id"].(string), + SubnetID: raw["subnet_id"].(string), + PortID: raw["port_id"].(string), + } + + fipSource := raw["fip_source"].(string) + fipID := raw["existing_fip_id"].(string) + if fipSource != "" { + interfaceOpts.FloatingIP = &bminstances.CreateNewInterfaceFloatingIPOpts{ + Source: types.FloatingIPSource(fipSource), + ExistingFloatingID: fipID, + } + } + interfaceOptsList[i] = interfaceOpts + } + + log.Printf("[DEBUG] Baremetal interfaces: %+v", interfaceOptsList) + opts := bminstances.CreateOpts{ + Flavor: d.Get("flavor_id").(string), + ImageID: d.Get("image_id").(string), + AppTemplateID: d.Get("apptemplate_id").(string), + Keypair: d.Get("keypair_name").(string), + Password: d.Get("password").(string), + Username: d.Get("username").(string), + UserData: d.Get("user_data").(string), + AppConfig: d.Get("app_config").(map[string]interface{}), + Interfaces: interfaceOptsList, + } + + name := d.Get("name").(string) + if len(name) > 0 { + opts.Names = []string{name} + } + + if nameTemplatesRaw, ok := d.GetOk("name_templates"); ok { + nameTemplates := nameTemplatesRaw.([]interface{}) + if len(nameTemplates) > 0 { + NameTemp := make([]string, len(nameTemplates)) + for i, nametemp := range nameTemplates { + NameTemp[i] = nametemp.(string) + } + opts.NameTemplates = NameTemp + } + } else if nameTemplate, ok := d.GetOk("name_template"); ok { + opts.NameTemplates = []string{nameTemplate.(string)} + } + + if metadata, ok := d.GetOk("metadata"); ok { + if len(metadata.([]interface{})) > 0 { + md, err := extractKeyValue(metadata.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + opts.Metadata = &md + } + } else if metadataRaw, ok := d.GetOk("metadata_map"); ok { + md := extractMetadataMap(metadataRaw.(map[string]interface{})) + opts.Metadata = &md + } + + results, err := bminstances.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + + InstanceID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, BmInstanceCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Instance, err := instances.ExtractInstanceIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Instance ID from task info: %w", err) + } + return Instance, nil + }, + ) + log.Printf("[DEBUG] Baremetal Instance id (%s)", InstanceID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(InstanceID.(string)) + resourceBmInstanceRead(ctx, d, m) + + log.Printf("[DEBUG] Finish Baremetal Instance creating (%s)", InstanceID) + + return diags +} + +func resourceBmInstanceRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Baremetal Instance reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + instanceID := d.Id() + log.Printf("[DEBUG] Instance id = %s", instanceID) + + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + instance, err := instances.Get(client, instanceID).Extract() + if err != nil { + return diag.Errorf("cannot get instance with ID: %s. Error: %s", instanceID, err) + } + + d.Set("name", instance.Name) + d.Set("flavor_id", instance.Flavor.FlavorID) + d.Set("status", instance.Status) + d.Set("vm_state", instance.VMState) + + flavor := make(map[string]interface{}, 4) + flavor["flavor_id"] = instance.Flavor.FlavorID + flavor["flavor_name"] = instance.Flavor.FlavorName + flavor["ram"] = strconv.Itoa(instance.Flavor.RAM) + flavor["vcpus"] = strconv.Itoa(instance.Flavor.VCPUS) + d.Set("flavor", flavor) + + interfacesListAPI, err := instances.ListInterfacesAll(client, instanceID) + if err != nil { + return diag.FromErr(err) + } + + if len(interfacesListAPI) == 0 { + return diag.Errorf("interface not found") + } + + ifs := d.Get("interface").([]interface{}) + sort.Sort(instanceInterfaces(ifs)) + interfacesListExtracted, err := extractInstanceInterfaceToListRead(ifs) + if err != nil { + return diag.FromErr(err) + } + + var interfacesList []interface{} + for order, iFace := range interfacesListAPI { + if len(iFace.IPAssignments) == 0 { + continue + } + + portID := iFace.PortID + for _, assignment := range iFace.IPAssignments { + subnetID := assignment.SubnetID + ipAddress := assignment.IPAddress.String() + + var interfaceOpts instances.InterfaceOpts + for _, interfaceExtracted := range interfacesListExtracted { + if interfaceExtracted.SubnetID == subnetID || + interfaceExtracted.IPAddress == ipAddress || + interfaceExtracted.PortID == portID { + interfaceOpts = interfaceExtracted + break + } + } + + i := make(map[string]interface{}) + i["type"] = interfaceOpts.Type.String() + i["order"] = order + i["network_id"] = iFace.NetworkID + i["subnet_id"] = subnetID + i["port_id"] = portID + i["is_parent"] = true + if interfaceOpts.FloatingIP != nil { + i["fip_source"] = interfaceOpts.FloatingIP.Source.String() + i["existing_fip_id"] = interfaceOpts.FloatingIP.ExistingFloatingID + } + i["ip_address"] = ipAddress + + interfacesList = append(interfacesList, i) + } + + for _, iFaceSubPort := range iFace.SubPorts { + subPortID := iFaceSubPort.PortID + for _, assignmentSubPort := range iFaceSubPort.IPAssignments { + assignmentSubnetID := assignmentSubPort.SubnetID + assignmentIPAddress := assignmentSubPort.IPAddress.String() + + var subPortInterfaceOpts instances.InterfaceOpts + for _, interfaceExtracted := range interfacesListExtracted { + if interfaceExtracted.SubnetID == assignmentSubnetID || + interfaceExtracted.IPAddress == assignmentIPAddress || + interfaceExtracted.PortID == subPortID { + subPortInterfaceOpts = interfaceExtracted + break + } + } + + i := make(map[string]interface{}) + + i["type"] = subPortInterfaceOpts.Type.String() + i["order"] = order + i["network_id"] = iFaceSubPort.NetworkID + i["subnet_id"] = assignmentSubnetID + i["port_id"] = subPortID + i["is_parent"] = false + if subPortInterfaceOpts.FloatingIP != nil { + i["fip_source"] = subPortInterfaceOpts.FloatingIP.Source.String() + i["existing_fip_id"] = subPortInterfaceOpts.FloatingIP.ExistingFloatingID + } + i["ip_address"] = assignmentIPAddress + + interfacesList = append(interfacesList, i) + } + } + } + if err := d.Set("interface", interfacesList); err != nil { + return diag.FromErr(err) + } + + if metadataRaw, ok := d.GetOk("metadata"); ok { + metadata := metadataRaw.([]interface{}) + sliced := make([]map[string]string, len(metadata)) + for i, data := range metadata { + d := data.(map[string]interface{}) + mdata := make(map[string]string, 2) + md, err := instances.MetadataGet(client, instanceID, d["key"].(string)).Extract() + if err != nil { + return diag.Errorf("cannot get metadata with key: %s. Error: %s", instanceID, err) + } + mdata["key"] = md.Key + mdata["value"] = md.Value + sliced[i] = mdata + } + d.Set("metadata", sliced) + } else { + metadata := d.Get("metadata_map").(map[string]interface{}) + newMetadata := make(map[string]interface{}, len(metadata)) + for k := range metadata { + md, err := instances.MetadataGet(client, instanceID, k).Extract() + if err != nil { + return diag.Errorf("cannot get metadata with key: %s. Error: %s", instanceID, err) + } + newMetadata[k] = md.Value + } + if err := d.Set("metadata_map", newMetadata); err != nil { + return diag.FromErr(err) + } + } + + addresses := []map[string][]map[string]string{} + for _, data := range instance.Addresses { + d := map[string][]map[string]string{} + netd := make([]map[string]string, len(data)) + for i, iaddr := range data { + ndata := make(map[string]string, 2) + ndata["type"] = iaddr.Type.String() + ndata["addr"] = iaddr.Address.String() + netd[i] = ndata + } + d["net"] = netd + addresses = append(addresses, d) + } + if err := d.Set("addresses", addresses); err != nil { + return diag.FromErr(err) + } + + fields := []string{"user_data", "app_config"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish Instance reading") + + return diags +} + +func resourceBmInstanceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Baremetal Instance updating") + instanceID := d.Id() + log.Printf("[DEBUG] Instance id = %s", instanceID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + nameTemplates := d.Get("name_templates").([]interface{}) + nameTemplate := d.Get("name_template").(string) + if len(nameTemplate) == 0 && len(nameTemplates) == 0 { + opts := instances.RenameInstanceOpts{ + Name: d.Get("name").(string), + } + if _, err := instances.RenameInstance(client, instanceID, opts).Extract(); err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("metadata") { + omd, nmd := d.GetChange("metadata") + if len(omd.([]interface{})) > 0 { + for _, data := range omd.([]interface{}) { + d := data.(map[string]interface{}) + k := d["key"].(string) + err := instances.MetadataDelete(client, instanceID, k).Err + if err != nil { + return diag.Errorf("cannot delete metadata key: %s. Error: %s", k, err) + } + } + } + if len(nmd.([]interface{})) > 0 { + var MetaData []instances.MetadataOpts + for _, data := range nmd.([]interface{}) { + d := data.(map[string]interface{}) + var md instances.MetadataOpts + md.Key = d["key"].(string) + md.Value = d["value"].(string) + MetaData = append(MetaData, md) + } + createOpts := instances.MetadataSetOpts{ + Metadata: MetaData, + } + err := instances.MetadataCreate(client, instanceID, createOpts).Err + if err != nil { + return diag.Errorf("cannot create metadata. Error: %s", err) + } + } + } else if d.HasChange("metadata_map") { + omd, nmd := d.GetChange("metadata_map") + if len(omd.(map[string]interface{})) > 0 { + for k := range omd.(map[string]interface{}) { + err := instances.MetadataDelete(client, instanceID, k).Err + if err != nil { + return diag.Errorf("cannot delete metadata key: %s. Error: %s", k, err) + } + } + } + if len(nmd.(map[string]interface{})) > 0 { + var MetaData []instances.MetadataOpts + for k, v := range nmd.(map[string]interface{}) { + md := instances.MetadataOpts{ + Key: k, + Value: v.(string), + } + MetaData = append(MetaData, md) + } + createOpts := instances.MetadataSetOpts{ + Metadata: MetaData, + } + err := instances.MetadataCreate(client, instanceID, createOpts).Err + if err != nil { + return diag.Errorf("cannot create metadata. Error: %s", err) + } + } + } + + if d.HasChange("interface") { + ifsOldRaw, ifsNewRaw := d.GetChange("interface") + + ifsOld := ifsOldRaw.([]interface{}) + ifsNew := ifsNewRaw.([]interface{}) + + for _, i := range ifsOld { + iface := i.(map[string]interface{}) + if isInterfaceContains(iface, ifsNew) { + log.Println("[DEBUG] Skipped, dont need detach") + continue + } + + if iface["is_parent"].(bool) { + return diag.Errorf("could not detach trunk interface") + } + + var opts instances.InterfaceOpts + opts.PortID = iface["port_id"].(string) + opts.IPAddress = iface["ip_address"].(string) + + log.Printf("[DEBUG] detach interface: %+v", opts) + results, err := instances.DetachInterface(client, instanceID, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, InstanceCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w, task: %+v", task, err, taskInfo) + } + return nil, nil + }, + ) + if err != nil { + return diag.FromErr(err) + } + } + + currentIfs, err := instances.ListInterfacesAll(client, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + sort.Sort(instanceInterfaces(ifsNew)) + for _, i := range ifsNew { + iface := i.(map[string]interface{}) + if isInterfaceContains(iface, ifsOld) { + log.Println("[DEBUG] Skipped, dont need attach") + continue + } + if isInterfaceAttached(currentIfs, iface) { + continue + } + + iType := types.InterfaceType(iface["type"].(string)) + opts := instances.InterfaceOpts{Type: iType} + switch iType { + case types.SubnetInterfaceType: + opts.SubnetID = iface["subnet_id"].(string) + case types.AnySubnetInterfaceType: + opts.NetworkID = iface["network_id"].(string) + case types.ReservedFixedIPType: + opts.PortID = iface["port_id"].(string) + case types.ExternalInterfaceType: + } + + log.Printf("[DEBUG] attach interface: %+v", opts) + results, err := instances.AttachInterface(client, instanceID, opts).Extract() + if err != nil { + return diag.Errorf("cannot attach interface: %s. Error: %s", iType, err) + } + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, InstanceCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w, task: %+v", task, err, taskInfo) + } + return nil, nil + }, + ) + if err != nil { + return diag.FromErr(err) + } + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish Instance updating") + + return resourceBmInstanceRead(ctx, d, m) +} + +func resourceBmInstanceDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Baremetal Instance deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + instanceID := d.Id() + log.Printf("[DEBUG] Instance id = %s", instanceID) + + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var delOpts instances.DeleteOpts + delOpts.DeleteFloatings = true + + results, err := instances.Delete(client, instanceID, delOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, BmInstanceDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := instances.Get(client, instanceID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete instance with ID: %s", instanceID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Instance resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of Instance deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_cdn_origin_group.go b/edgecenter/resource_edgecenter_cdn_origin_group.go new file mode 100644 index 00000000..de7095e3 --- /dev/null +++ b/edgecenter/resource_edgecenter_cdn_origin_group.go @@ -0,0 +1,218 @@ +package edgecenter + +import ( + "context" + "crypto/md5" + "encoding/binary" + "fmt" + "io" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercdn-go/origingroups" +) + +func resourceCDNOriginGroup() *schema.Resource { + return &schema.Resource{ + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the origin group", + }, + "use_next": { + Type: schema.TypeBool, + Required: true, + Description: "This options have two possible values: true — The option is active. In case the origin responds with 4XX or 5XX codes, use the next origin from the list. false — The option is disabled.", + }, + "origin": { + Type: schema.TypeSet, + Required: true, + Description: "Contains information about all IP address or Domain names of your origin and the port if custom", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeString, + Required: true, + Description: "IP address or Domain name of your origin and the port if custom", + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "The setting allows to enable or disable an Origin source in the Origins group", + }, + "backup": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "true — The option is active. The origin will not be used until one of active origins become unavailable. false — The option is disabled.", + }, + "id": { + Type: schema.TypeInt, + Computed: true, + }, + }, + }, + }, + }, + CreateContext: resourceCDNOriginGroupCreate, + ReadContext: resourceCDNOriginGroupRead, + UpdateContext: resourceCDNOriginGroupUpdate, + DeleteContext: resourceCDNOriginGroupDelete, + Description: "Represent origin group", + } +} + +func resourceCDNOriginGroupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start CDN OriginGroup creating") + config := m.(*Config) + client := config.CDNClient + + var req origingroups.GroupRequest + req.Name = d.Get("name").(string) + req.UseNext = d.Get("use_next").(bool) + req.Origins = setToOriginRequests(d.Get("origin").(*schema.Set)) + + result, err := client.OriginGroups().Create(ctx, &req) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%d", result.ID)) + resourceCDNOriginGroupRead(ctx, d, m) + + log.Printf("[DEBUG] Finish CDN OriginGroup creating (id=%d)\n", result.ID) + + return nil +} + +func resourceCDNOriginGroupRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + groupID := d.Id() + log.Printf("[DEBUG] Start CDN OriginGroup reading (id=%s)\n", groupID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(groupID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + result, err := client.OriginGroups().Get(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + d.Set("name", result.Name) + d.Set("use_next", result.UseNext) + if err := d.Set("origin", originsToSet(result.Origins)); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish CDN OriginGroup reading") + + return nil +} + +func resourceCDNOriginGroupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + groupID := d.Id() + log.Printf("[DEBUG] Start CDN OriginGroup updating (id=%s)\n", groupID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(groupID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + var req origingroups.GroupRequest + req.Name = d.Get("name").(string) + req.UseNext = d.Get("use_next").(bool) + req.Origins = setToOriginRequests(d.Get("origin").(*schema.Set)) + + if _, err := client.OriginGroups().Update(ctx, id, &req); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish CDN OriginGroup updating") + + return resourceCDNOriginGroupRead(ctx, d, m) +} + +func resourceCDNOriginGroupDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + resourceID := d.Id() + log.Printf("[DEBUG] Start CDN OriginGroup deleting (id=%s)\n", resourceID) + + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + if err := client.OriginGroups().Delete(ctx, id); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Println("[DEBUG] Finish CDN Resource deleting") + + return nil +} + +func setToOriginRequests(s *schema.Set) []origingroups.OriginRequest { + origins := make([]origingroups.OriginRequest, 0) + for _, fields := range s.List() { + var originReq origingroups.OriginRequest + + for key, val := range fields.(map[string]interface{}) { + switch key { + case "source": + originReq.Source = val.(string) + case "enabled": + originReq.Enabled = val.(bool) + case "backup": + originReq.Backup = val.(bool) + } + } + + origins = append(origins, originReq) + } + + return origins +} + +func originsToSet(origins []origingroups.Origin) *schema.Set { + s := &schema.Set{F: originSetIDFunc} + + for _, origin := range origins { + fields := make(map[string]interface{}) + fields["id"] = origin.ID + fields["source"] = origin.Source + fields["enabled"] = origin.Enabled + fields["backup"] = origin.Backup + + s.Add(fields) + } + + return s +} + +func originSetIDFunc(i interface{}) int { + fields := i.(map[string]interface{}) + h := md5.New() + + key := fmt.Sprintf("%d-%s-%t-%t", fields["id"], fields["source"], fields["enabled"], fields["backup"]) + log.Printf("[DEBUG] Origin Set ID = %s\n", key) + + io.WriteString(h, key) + + return int(binary.BigEndian.Uint64(h.Sum(nil))) +} diff --git a/edgecenter/resource_edgecenter_cdn_resource.go b/edgecenter/resource_edgecenter_cdn_resource.go new file mode 100644 index 00000000..da8633df --- /dev/null +++ b/edgecenter/resource_edgecenter_cdn_resource.go @@ -0,0 +1,1694 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "reflect" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + cdn "github.com/Edge-Center/edgecentercdn-go/edgecenter" + "github.com/Edge-Center/edgecentercdn-go/resources" +) + +var resourceOptionsSchema = &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "Each option in CDN resource settings. Each option added to CDN resource settings should have the following mandatory request fields: enabled, value.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_http_methods": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "brotli_compression": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "browser_cache_settings": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: "", + }, + }, + }, + }, + "cache_http_headers": { // Deprecated. Use - response_headers_hiding_policy. + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "cors": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + "always": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + "country_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "disable_proxy_force_ranges": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "edge_cache_settings": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "The cache expiration time for CDN servers.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: "Caching time for a response with codes 200, 206, 301, 302. Responses with codes 4xx, 5xx will not be cached. Use '0s' disable to caching. Use custom_values field to specify a custom caching time for a response with specific codes.", + }, + "default": { + Type: schema.TypeString, + Optional: true, + Description: "Content will be cached according to origin cache settings. The value applies for a response with codes 200, 201, 204, 206, 301, 302, 303, 304, 307, 308 if an origin server does not have caching HTTP headers. Responses with other codes will not be cached.", + }, + "custom_values": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + DefaultFunc: func() (interface{}, error) { + return map[string]interface{}{}, nil + }, + Elem: schema.TypeString, + Description: "Caching time for a response with specific codes. These settings have a higher priority than the value field. Response code ('304', '404' for example). Use 'any' to specify caching time for all response codes. Caching time in seconds ('0s', '600s' for example). Use '0s' to disable caching for a specific response code.", + }, + }, + }, + }, + "fetch_compressed": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "follow_origin_redirect": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "codes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeInt}, + Required: true, + Description: "", + }, + }, + }, + }, + "force_return": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "code": { + Type: schema.TypeInt, + Required: true, + Description: "", + }, + "body": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "", + }, + }, + }, + }, + "forward_host_header": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "gzip_on": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "host_header": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specify the Host header that CDN servers use when request content from an origin server. Your server must be able to process requests with the chosen header. If the option is in NULL state Host Header value is taken from the CNAME field.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "http3_enabled": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "ignore_cookie": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "ignore_query_string": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "image_stack": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "avif_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + "webp_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + "quality": { + Type: schema.TypeInt, + Required: true, + Description: "", + }, + "png_lossless": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + "ip_address_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "limit_bandwidth": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "limit_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "speed": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "", + }, + "buffer": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + "proxy_cache_methods_set": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "query_params_blacklist": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + }, + }, + "query_params_whitelist": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + }, + }, + "redirect_http_to_https": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Sets redirect from HTTP protocol to HTTPS for all resource requests.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "redirect_https_to_http": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "referrer_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "Possible values: allow, deny.", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "response_headers_hiding_policy": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "mode": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "rewrite": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "body": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "flag": { + Type: schema.TypeString, + Optional: true, + Default: "break", + Description: "", + }, + }, + }, + }, + "secure_key": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "key": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "type": { + Type: schema.TypeInt, + Required: true, + Description: "", + }, + }, + }, + }, + "slice": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "sni": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "sni_type": { + Type: schema.TypeString, + Optional: true, + Description: "Available values 'dynamic' or 'custom'", + }, + "custom_hostname": { + Type: schema.TypeString, + Optional: true, + Description: "Required to set custom hostname in case sni-type='custom'", + }, + }, + }, + }, + "stale": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "static_headers": { // Deprecated. Use - static_response_headers. + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Option has been deprecated. Use - static_response_headers.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + }, + }, + "static_request_headers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "static_response_headers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specify custom HTTP Headers that a CDN server adds to a response.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + "always": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + }, + }, + }, + "tls_versions": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "use_default_le_chain": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "user_agent_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "websockets": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + }, + }, +} + +func resourceCDNResource() *schema.Resource { + return &schema.Resource{ + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "cname": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "A CNAME that will be used to deliver content though a CDN. If you update this field new resource will be created.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "Custom client description of the resource.", + }, + "origin_group": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ExactlyOneOf: []string{ + "origin_group", + "origin", + }, + Description: "ID of the Origins Group. Use one of your Origins Group or create a new one. You can use either 'origin' parameter or 'originGroup' in the resource definition.", + }, + "origin": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{ + "origin_group", + "origin", + }, + Description: "A domain name or IP of your origin source. Specify a port if custom. You can use either 'origin' parameter or 'originGroup' in the resource definition.", + }, + "origin_protocol": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "This option defines the protocol that will be used by CDN servers to request content from an origin source. If not specified, we will use HTTP to connect to an origin server. Possible values are: HTTPS, HTTP, MATCH.", + }, + "secondary_hostnames": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + DefaultFunc: func() (interface{}, error) { + return []string{}, nil + }, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "List of additional CNAMEs.", + }, + "ssl_enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Use HTTPS protocol for content delivery.", + }, + "ssl_data": { + Type: schema.TypeInt, + Optional: true, + RequiredWith: []string{"ssl_enabled"}, + Description: "Specify the SSL Certificate ID which should be used for the CDN Resource.", + }, + "ssl_automated": { + Type: schema.TypeBool, + Optional: true, + Description: "generate LE certificate automatically.", + }, + "issue_le_cert": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Generate LE certificate.", + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "The setting allows to enable or disable a CDN Resource", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "Status of a CDN resource content availability. Possible values are: Active, Suspended, Processed.", + }, + "options": resourceOptionsSchema, + }, + CreateContext: resourceCDNResourceCreate, + ReadContext: resourceCDNResourceRead, + UpdateContext: resourceCDNResourceUpdate, + DeleteContext: resourceCDNResourceDelete, + Description: "Represent CDN resource", + } +} + +func resourceCDNResourceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start CDN Resource creating") + config := m.(*Config) + client := config.CDNClient + + var req resources.CreateRequest + req.Cname = d.Get("cname").(string) + req.Description = d.Get("description").(string) + req.Origin = d.Get("origin").(string) + req.OriginGroup = d.Get("origin_group").(int) + req.OriginProtocol = resources.Protocol(d.Get("origin_protocol").(string)) + req.SSlEnabled = d.Get("ssl_enabled").(bool) + req.SSLData = d.Get("ssl_data").(int) + req.SSLAutomated = d.Get("ssl_automated").(bool) + + if d.Get("issue_le_cert") != nil { + req.IssueLECert = d.Get("issue_le_cert").(bool) + } + + req.Options = listToResourceOptions(d.Get("options").([]interface{})) + + for _, hostname := range d.Get("secondary_hostnames").(*schema.Set).List() { + req.SecondaryHostnames = append(req.SecondaryHostnames, hostname.(string)) + } + + result, err := client.Resources().Create(ctx, &req) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%d", result.ID)) + resourceCDNResourceRead(ctx, d, m) + + log.Printf("[DEBUG] Finish CDN Resource creating (id=%d)\n", result.ID) + + return nil +} + +func resourceCDNResourceRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + resourceID := d.Id() + log.Printf("[DEBUG] Start CDN Resource reading (id=%s)\n", resourceID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + result, err := client.Resources().Get(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + d.Set("cname", result.Cname) + d.Set("description", result.Description) + d.Set("origin_group", result.OriginGroup) + d.Set("origin_protocol", result.OriginProtocol) + d.Set("secondary_hostnames", result.SecondaryHostnames) + d.Set("ssl_enabled", result.SSlEnabled) + d.Set("ssl_data", result.SSLData) + d.Set("ssl_automated", result.SSLAutomated) + d.Set("status", result.Status) + d.Set("active", result.Active) + if err := d.Set("options", resourceOptionsToList(result.Options)); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish CDN Resource reading") + + return nil +} + +func resourceCDNResourceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + resourceID := d.Id() + log.Printf("[DEBUG] Start CDN Resource updating (id=%s)\n", resourceID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + var req resources.UpdateRequest + req.Active = d.Get("active").(bool) + req.Description = d.Get("description").(string) + req.OriginGroup = d.Get("origin_group").(int) + req.SSlEnabled = d.Get("ssl_enabled").(bool) + req.SSLData = d.Get("ssl_data").(int) + req.OriginProtocol = resources.Protocol(d.Get("origin_protocol").(string)) + req.Options = listToResourceOptions(d.Get("options").([]interface{})) + for _, hostname := range d.Get("secondary_hostnames").(*schema.Set).List() { + req.SecondaryHostnames = append(req.SecondaryHostnames, hostname.(string)) + } + + if _, err := client.Resources().Update(ctx, id, &req); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish CDN Resource updating") + + return resourceCDNResourceRead(ctx, d, m) +} + +func resourceCDNResourceDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + resourceID := d.Id() + log.Printf("[DEBUG] Start CDN Resource deleting (id=%s)\n", resourceID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + if err := client.Resources().Delete(ctx, id); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Println("[DEBUG] Finish CDN Resource deleting") + + return nil +} + +func listToResourceOptions(l []interface{}) *cdn.ResourceOptions { + if len(l) == 0 { + return nil + } + + var opts cdn.ResourceOptions + fields := l[0].(map[string]interface{}) + if opt, ok := getOptByName(fields, "allowed_http_methods"); ok { + opts.AllowedHTTPMethods = &cdn.AllowedHTTPMethods{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.AllowedHTTPMethods.Value = append(opts.AllowedHTTPMethods.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "brotli_compression"); ok { + opts.BrotliCompression = &cdn.BrotliCompression{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.BrotliCompression.Value = append(opts.BrotliCompression.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "browser_cache_settings"); ok { + opts.BrowserCacheSettings = &cdn.BrowserCacheSettings{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(string), + } + } + if opt, ok := getOptByName(fields, "cache_http_headers"); ok { + opts.CacheHttpHeaders = &cdn.CacheHttpHeaders{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.CacheHttpHeaders.Value = append(opts.CacheHttpHeaders.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "cors"); ok { + opts.Cors = &cdn.Cors{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.Cors.Value = append(opts.Cors.Value, v.(string)) + } + if _, ok := opt["always"]; ok { + opts.Cors.Always = opt["always"].(bool) + } + } + if opt, ok := getOptByName(fields, "country_acl"); ok { + opts.CountryACL = &cdn.CountryACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.CountryACL.ExceptedValues = append(opts.CountryACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "disable_proxy_force_ranges"); ok { + opts.DisableProxyForceRanges = &cdn.DisableProxyForceRanges{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "edge_cache_settings"); ok { + rawCustomVals := opt["custom_values"].(map[string]interface{}) + customVals := make(map[string]string, len(rawCustomVals)) + for key, value := range rawCustomVals { + customVals[key] = value.(string) + } + + opts.EdgeCacheSettings = &cdn.EdgeCacheSettings{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(string), + CustomValues: customVals, + Default: opt["default"].(string), + } + } + if opt, ok := getOptByName(fields, "fetch_compressed"); ok { + opts.FetchCompressed = &cdn.FetchCompressed{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "follow_origin_redirect"); ok { + opts.FollowOriginRedirect = &cdn.FollowOriginRedirect{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["codes"].(*schema.Set).List() { + opts.FollowOriginRedirect.Codes = append(opts.FollowOriginRedirect.Codes, v.(int)) + } + } + if opt, ok := getOptByName(fields, "force_return"); ok { + opts.ForceReturn = &cdn.ForceReturn{ + Enabled: opt["enabled"].(bool), + Code: opt["code"].(int), + Body: opt["body"].(string), + } + } + if opt, ok := getOptByName(fields, "forward_host_header"); ok { + opts.ForwardHostHeader = &cdn.ForwardHostHeader{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "gzip_on"); ok { + opts.GzipOn = &cdn.GzipOn{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "host_header"); ok { + opts.HostHeader = &cdn.HostHeader{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(string), + } + } + if opt, ok := getOptByName(fields, "http3_enabled"); ok { + opts.HTTP3Enabled = &cdn.HTTP3Enabled{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "ignore_cookie"); ok { + opts.IgnoreCookie = &cdn.IgnoreCookie{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "ignore_query_string"); ok { + opts.IgnoreQueryString = &cdn.IgnoreQueryString{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "image_stack"); ok { + opts.ImageStack = &cdn.ImageStack{ + Enabled: opt["enabled"].(bool), + Quality: opt["quality"].(int), + } + if _, ok := opt["avif_enabled"]; ok { + opts.ImageStack.AvifEnabled = opt["avif_enabled"].(bool) + } + if _, ok := opt["webp_enabled"]; ok { + opts.ImageStack.WebpEnabled = opt["webp_enabled"].(bool) + } + if _, ok := opt["png_lossless"]; ok { + opts.ImageStack.PngLossless = opt["png_lossless"].(bool) + } + } + if opt, ok := getOptByName(fields, "ip_address_acl"); ok { + opts.IPAddressACL = &cdn.IPAddressACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.IPAddressACL.ExceptedValues = append(opts.IPAddressACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "limit_bandwidth"); ok { + opts.LimitBandwidth = &cdn.LimitBandwidth{ + Enabled: opt["enabled"].(bool), + LimitType: opt["limit_type"].(string), + } + if _, ok := opt["speed"]; ok { + opts.LimitBandwidth.Speed = opt["speed"].(int) + } + if _, ok := opt["buffer"]; ok { + opts.LimitBandwidth.Buffer = opt["buffer"].(int) + } + } + if opt, ok := getOptByName(fields, "proxy_cache_methods_set"); ok { + opts.ProxyCacheMethodsSet = &cdn.ProxyCacheMethodsSet{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "query_params_blacklist"); ok { + opts.QueryParamsBlacklist = &cdn.QueryParamsBlacklist{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.QueryParamsBlacklist.Value = append(opts.QueryParamsBlacklist.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "query_params_whitelist"); ok { + opts.QueryParamsWhitelist = &cdn.QueryParamsWhitelist{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.QueryParamsWhitelist.Value = append(opts.QueryParamsWhitelist.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "redirect_http_to_https"); ok { + opts.RedirectHttpToHttps = &cdn.RedirectHttpToHttps{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "redirect_https_to_http"); ok { + opts.RedirectHttpsToHttp = &cdn.RedirectHttpsToHttp{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "referrer_acl"); ok { + opts.ReferrerACL = &cdn.ReferrerACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.ReferrerACL.ExceptedValues = append(opts.ReferrerACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "response_headers_hiding_policy"); ok { + opts.ResponseHeadersHidingPolicy = &cdn.ResponseHeadersHidingPolicy{ + Enabled: opt["enabled"].(bool), + Mode: opt["mode"].(string), + } + for _, v := range opt["excepted"].(*schema.Set).List() { + opts.ResponseHeadersHidingPolicy.Excepted = append(opts.ResponseHeadersHidingPolicy.Excepted, v.(string)) + } + } + if opt, ok := getOptByName(fields, "rewrite"); ok { + opts.Rewrite = &cdn.Rewrite{ + Enabled: opt["enabled"].(bool), + Body: opt["body"].(string), + Flag: opt["flag"].(string), + } + } + if opt, ok := getOptByName(fields, "secure_key"); ok { + opts.SecureKey = &cdn.SecureKey{ + Enabled: opt["enabled"].(bool), + Key: opt["key"].(string), + Type: opt["type"].(int), + } + } + if opt, ok := getOptByName(fields, "slice"); ok { + opts.Slice = &cdn.Slice{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "sni"); ok { + opts.SNI = &cdn.SNIOption{ + Enabled: opt["enabled"].(bool), + SNIType: opt["sni_type"].(string), + CustomHostname: opt["custom_hostname"].(string), + } + } + if opt, ok := getOptByName(fields, "stale"); ok { + opts.Stale = &cdn.Stale{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.Stale.Value = append(opts.Stale.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "static_headers"); ok { + opts.StaticHeaders = &cdn.StaticHeaders{ + Enabled: opt["enabled"].(bool), + Value: map[string]string{}, + } + for k, v := range opt["value"].(map[string]interface{}) { + opts.StaticHeaders.Value[k] = v.(string) + } + } + if opt, ok := getOptByName(fields, "static_request_headers"); ok { + opts.StaticRequestHeaders = &cdn.StaticRequestHeaders{ + Enabled: opt["enabled"].(bool), + Value: map[string]string{}, + } + for k, v := range opt["value"].(map[string]interface{}) { + opts.StaticRequestHeaders.Value[k] = v.(string) + } + } + if opt, ok := getOptByName(fields, "static_response_headers"); ok { + opts.StaticResponseHeaders = &cdn.StaticResponseHeaders{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].([]interface{}) { + itemData := v.(map[string]interface{}) + item := &cdn.StaticResponseHeadersItem{ + Name: itemData["name"].(string), + } + for _, val := range itemData["value"].(*schema.Set).List() { + item.Value = append(item.Value, val.(string)) + } + if _, ok := itemData["always"]; ok { + item.Always = itemData["always"].(bool) + } + opts.StaticResponseHeaders.Value = append(opts.StaticResponseHeaders.Value, *item) + } + } + if opt, ok := getOptByName(fields, "tls_versions"); ok { + opts.TLSVersions = &cdn.TLSVersions{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.TLSVersions.Value = append(opts.TLSVersions.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "use_default_le_chain"); ok { + opts.UseDefaultLEChain = &cdn.UseDefaultLEChain{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "user_agent_acl"); ok { + opts.UserAgentACL = &cdn.UserAgentACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.UserAgentACL.ExceptedValues = append(opts.UserAgentACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "websockets"); ok { + opts.WebSockets = &cdn.WebSockets{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + + return &opts +} + +func getOptByName(fields map[string]interface{}, name string) (map[string]interface{}, bool) { + if _, ok := fields[name]; !ok { + return nil, false + } + + container, ok := fields[name].([]interface{}) + if !ok { + return nil, false + } + + if len(container) == 0 { + return nil, false + } + + opt, ok := container[0].(map[string]interface{}) + if !ok { + return nil, false + } + + return opt, true +} + +func resourceOptionsToList(options *cdn.ResourceOptions) []interface{} { + result := make(map[string][]interface{}) + if options.AllowedHTTPMethods != nil { + m := structToMap(options.AllowedHTTPMethods) + result["allowed_http_methods"] = []interface{}{m} + } + if options.BrotliCompression != nil { + m := structToMap(options.BrotliCompression) + result["brotli_compression"] = []interface{}{m} + } + if options.BrowserCacheSettings != nil { + m := structToMap(options.BrowserCacheSettings) + result["browser_cache_settings"] = []interface{}{m} + } + if options.CacheHttpHeaders != nil { + m := structToMap(options.CacheHttpHeaders) + result["cache_http_headers"] = []interface{}{m} + } + if options.Cors != nil { + m := structToMap(options.Cors) + result["cors"] = []interface{}{m} + } + if options.CountryACL != nil { + m := structToMap(options.CountryACL) + result["country_acl"] = []interface{}{m} + } + if options.DisableProxyForceRanges != nil { + m := structToMap(options.DisableProxyForceRanges) + result["disable_proxy_force_ranges"] = []interface{}{m} + } + if options.EdgeCacheSettings != nil { + m := structToMap(options.EdgeCacheSettings) + result["edge_cache_settings"] = []interface{}{m} + } + if options.FetchCompressed != nil { + m := structToMap(options.FetchCompressed) + result["fetch_compressed"] = []interface{}{m} + } + if options.FollowOriginRedirect != nil { + m := structToMap(options.FollowOriginRedirect) + result["follow_origin_redirect"] = []interface{}{m} + } + if options.ForceReturn != nil { + m := structToMap(options.ForceReturn) + result["force_return"] = []interface{}{m} + } + if options.ForwardHostHeader != nil { + m := structToMap(options.ForwardHostHeader) + result["forward_host_header"] = []interface{}{m} + } + if options.GzipOn != nil { + m := structToMap(options.GzipOn) + result["gzip_on"] = []interface{}{m} + } + if options.HostHeader != nil { + m := structToMap(options.HostHeader) + result["host_header"] = []interface{}{m} + } + if options.HTTP3Enabled != nil { + m := structToMap(options.HTTP3Enabled) + result["http3_enabled"] = []interface{}{m} + } + if options.IgnoreCookie != nil { + m := structToMap(options.IgnoreCookie) + result["ignore_cookie"] = []interface{}{m} + } + if options.IgnoreQueryString != nil { + m := structToMap(options.IgnoreQueryString) + result["ignore_query_string"] = []interface{}{m} + } + if options.ImageStack != nil { + m := structToMap(options.ImageStack) + result["image_stack"] = []interface{}{m} + } + if options.IPAddressACL != nil { + m := structToMap(options.IPAddressACL) + result["ip_address_acl"] = []interface{}{m} + } + if options.LimitBandwidth != nil { + m := structToMap(options.LimitBandwidth) + result["limit_bandwidth"] = []interface{}{m} + } + if options.ProxyCacheMethodsSet != nil { + m := structToMap(options.ProxyCacheMethodsSet) + result["proxy_cache_methods_set"] = []interface{}{m} + } + if options.QueryParamsBlacklist != nil { + m := structToMap(options.QueryParamsBlacklist) + result["query_params_blacklist"] = []interface{}{m} + } + if options.QueryParamsWhitelist != nil { + m := structToMap(options.QueryParamsWhitelist) + result["query_params_whitelist"] = []interface{}{m} + } + if options.RedirectHttpsToHttp != nil { + m := structToMap(options.RedirectHttpsToHttp) + result["redirect_https_to_http"] = []interface{}{m} + } + if options.RedirectHttpToHttps != nil { + m := structToMap(options.RedirectHttpToHttps) + result["redirect_http_to_https"] = []interface{}{m} + } + if options.ReferrerACL != nil { + m := structToMap(options.ReferrerACL) + result["referrer_acl"] = []interface{}{m} + } + if options.ResponseHeadersHidingPolicy != nil { + m := structToMap(options.ResponseHeadersHidingPolicy) + result["response_headers_hiding_policy"] = []interface{}{m} + } + if options.Rewrite != nil { + m := structToMap(options.Rewrite) + result["rewrite"] = []interface{}{m} + } + if options.SecureKey != nil { + m := structToMap(options.SecureKey) + result["secure_key"] = []interface{}{m} + } + if options.Slice != nil { + m := structToMap(options.Slice) + result["slice"] = []interface{}{m} + } + if options.SNI != nil { + m := structToMap(options.SNI) + result["sni"] = []interface{}{m} + } + if options.Stale != nil { + m := structToMap(options.Stale) + result["stale"] = []interface{}{m} + } + if options.StaticHeaders != nil { + m := structToMap(options.StaticHeaders) + result["static_headers"] = []interface{}{m} + } + if options.StaticRequestHeaders != nil { + m := structToMap(options.StaticRequestHeaders) + result["static_request_headers"] = []interface{}{m} + } + if options.StaticResponseHeaders != nil { + m := structToMap(options.StaticResponseHeaders) + items := []interface{}{} + for _, v := range m["value"].([]cdn.StaticResponseHeadersItem) { + items = append(items, structToMap(v)) + } + m["value"] = items + result["static_response_headers"] = []interface{}{m} + } + if options.TLSVersions != nil { + m := structToMap(options.TLSVersions) + result["tls_versions"] = []interface{}{m} + } + if options.UseDefaultLEChain != nil { + m := structToMap(options.UseDefaultLEChain) + result["use_default_le_chain"] = []interface{}{m} + } + if options.UserAgentACL != nil { + m := structToMap(options.UserAgentACL) + result["user_agent_acl"] = []interface{}{m} + } + if options.WebSockets != nil { + m := structToMap(options.WebSockets) + result["websockets"] = []interface{}{m} + } + + return []interface{}{result} +} + +func structToMap(item interface{}) map[string]interface{} { + res := map[string]interface{}{} + if item == nil { + return res + } + v := reflect.TypeOf(item) + reflectValue := reflect.ValueOf(item) + reflectValue = reflect.Indirect(reflectValue) + + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + for i := 0; i < v.NumField(); i++ { + tag := v.Field(i).Tag.Get("json") + field := reflectValue.Field(i).Interface() + if tag != "" && tag != "-" { + if v.Field(i).Type.Kind() == reflect.Struct { + res[tag] = structToMap(field) + } else { + res[tag] = field + } + } + } + + return res +} diff --git a/edgecenter/resource_edgecenter_cdn_rule.go b/edgecenter/resource_edgecenter_cdn_rule.go new file mode 100644 index 00000000..c20fc8ac --- /dev/null +++ b/edgecenter/resource_edgecenter_cdn_rule.go @@ -0,0 +1,1524 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/AlekSi/pointer" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + cdn "github.com/Edge-Center/edgecentercdn-go/edgecenter" + "github.com/Edge-Center/edgecentercdn-go/rules" +) + +var locationOptionsSchema = &schema.Schema{ + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "Each option in CDN resource settings. Each option added to CDN resource settings should have the following mandatory request fields: enabled, value.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "allowed_http_methods": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "brotli_compression": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "browser_cache_settings": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: "", + }, + }, + }, + }, + "cache_http_headers": { // Deprecated. Use - response_headers_hiding_policy. + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "cors": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + "always": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + "country_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "disable_proxy_force_ranges": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "edge_cache_settings": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "The cache expiration time for CDN servers.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeString, + Optional: true, + Description: "Caching time for a response with codes 200, 206, 301, 302. Responses with codes 4xx, 5xx will not be cached. Use '0s' disable to caching. Use custom_values field to specify a custom caching time for a response with specific codes.", + }, + "default": { + Type: schema.TypeString, + Optional: true, + Description: "Content will be cached according to origin cache settings. The value applies for a response with codes 200, 201, 204, 206, 301, 302, 303, 304, 307, 308 if an origin server does not have caching HTTP headers. Responses with other codes will not be cached.", + }, + "custom_values": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + DefaultFunc: func() (interface{}, error) { + return map[string]interface{}{}, nil + }, + Elem: schema.TypeString, + Description: "Caching time for a response with specific codes. These settings have a higher priority than the value field. Response code ('304', '404' for example). Use 'any' to specify caching time for all response codes. Caching time in seconds ('0s', '600s' for example). Use '0s' to disable caching for a specific response code.", + }, + }, + }, + }, + "fetch_compressed": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "follow_origin_redirect": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "codes": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeInt}, + Required: true, + Description: "", + }, + }, + }, + }, + "force_return": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "code": { + Type: schema.TypeInt, + Required: true, + Description: "", + }, + "body": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "", + }, + }, + }, + }, + "forward_host_header": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "gzip_on": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "host_header": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specify the Host header that CDN servers use when request content from an origin server. Your server must be able to process requests with the chosen header. If the option is in NULL state Host Header value is taken from the CNAME field.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "ignore_cookie": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "ignore_query_string": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "image_stack": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "avif_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + "webp_enabled": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + "quality": { + Type: schema.TypeInt, + Required: true, + Description: "", + }, + "png_lossless": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + "ip_address_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "limit_bandwidth": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "limit_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "speed": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "", + }, + "buffer": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + "proxy_cache_methods_set": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "query_params_blacklist": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + }, + }, + "query_params_whitelist": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + }, + }, + "redirect_http_to_https": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Sets redirect from HTTP protocol to HTTPS for all resource requests.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "redirect_https_to_http": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "referrer_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "Possible values: allow, deny.", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "response_headers_hiding_policy": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "mode": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "rewrite": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "body": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "flag": { + Type: schema.TypeString, + Optional: true, + Default: "break", + Description: "", + }, + }, + }, + }, + "secure_key": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "key": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "type": { + Type: schema.TypeInt, + Required: true, + Description: "", + }, + }, + }, + }, + "slice": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + "sni": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "sni_type": { + Type: schema.TypeString, + Optional: true, + Description: "Available values 'dynamic' or 'custom'", + }, + "custom_hostname": { + Type: schema.TypeString, + Optional: true, + Description: "Required to set custom hostname in case sni-type='custom'", + }, + }, + }, + }, + "stale": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Computed: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "static_headers": { // Deprecated. Use - static_response_headers. + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Option has been deprecated. Use - static_response_headers.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + }, + }, + }, + }, + "static_request_headers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeMap, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "static_response_headers": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "Specify custom HTTP Headers that a CDN server adds to a response.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "value": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + "always": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "", + }, + }, + }, + }, + }, + }, + }, + "user_agent_acl": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "policy_type": { + Type: schema.TypeString, + Required: true, + Description: "", + }, + "excepted_values": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Description: "", + }, + }, + }, + }, + "websockets": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Description: "", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "value": { + Type: schema.TypeBool, + Required: true, + }, + }, + }, + }, + }, + }, +} + +func resourceCDNRule() *schema.Resource { + return &schema.Resource{ + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "resource_id": { + Type: schema.TypeInt, + Required: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Rule name", + }, + "rule": { + Type: schema.TypeString, + Required: true, + Description: "A pattern that defines when the rule is triggered. By default, we add a leading forward slash to any rule pattern. Specify a pattern without a forward slash.", + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Shows if the location is enabled.", + }, + "weight": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "", + }, + "origin_group": { + Type: schema.TypeInt, + Optional: true, + Description: "ID of the Origins Group. Use one of your Origins Group or create a new one. You can use either 'origin' parameter or 'originGroup' in the resource definition.", + }, + "origin_protocol": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "This option defines the protocol that will be used by CDN servers to request content from an origin source. If not specified, it will be inherit from resource. Possible values are: HTTPS, HTTP, MATCH.", + }, + "options": locationOptionsSchema, + }, + CreateContext: resourceCDNRuleCreate, + ReadContext: resourceCDNRuleRead, + UpdateContext: resourceCDNRuleUpdate, + DeleteContext: resourceCDNRuleDelete, + Description: "Represent cdn resource rule", + } +} + +func resourceCDNRuleCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start CDN Rule creating") + config := m.(*Config) + client := config.CDNClient + + var req rules.CreateRequest + req.Name = d.Get("name").(string) + req.Rule = d.Get("rule").(string) + + if d.Get("active") != nil { + req.Active = d.Get("active").(bool) + } + + if d.Get("weight") != nil { + req.Weight = d.Get("weight").(int) + } + + if d.Get("origin_group") != nil && d.Get("origin_group").(int) > 0 { + req.OriginGroup = pointer.ToInt(d.Get("origin_group").(int)) + } + + if d.Get("origin_protocol") != nil && d.Get("origin_protocol") != "" { + req.OverrideOriginProtocol = pointer.ToString(d.Get("origin_protocol").(string)) + } + + resourceID := d.Get("resource_id").(int) + + req.Options = listToLocationOptions(d.Get("options").([]interface{})) + + result, err := client.Rules().Create(ctx, int64(resourceID), &req) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%d", result.ID)) + resourceCDNRuleRead(ctx, d, m) + + log.Printf("[DEBUG] Finish CDN Rule creating (id=%d)\n", result.ID) + + return nil +} + +func resourceCDNRuleRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + ruleID := d.Id() + log.Printf("[DEBUG] Start CDN Rule reading (id=%s)\n", ruleID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(ruleID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + resourceID := d.Get("resource_id").(int) + + result, err := client.Rules().Get(ctx, int64(resourceID), id) + if err != nil { + return diag.FromErr(err) + } + + d.Set("name", result.Name) + d.Set("rule", result.Pattern) + d.Set("active", result.Active) + d.Set("origin_group", result.OriginGroup) + d.Set("origin_protocol", result.OriginProtocol) + d.Set("weight", result.Weight) + if err := d.Set("options", locationOptionsToList(result.Options)); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish CDN Rule reading") + + return nil +} + +func resourceCDNRuleUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + ruleID := d.Id() + log.Printf("[DEBUG] Start CDN Rule updating (id=%s)\n", ruleID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(ruleID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + var req rules.UpdateRequest + req.Name = d.Get("name").(string) + req.Rule = d.Get("rule").(string) + req.Active = d.Get("active").(bool) + + if d.Get("weight") != nil { + req.Weight = d.Get("weight").(int) + } + + if d.Get("origin_group") != nil && d.Get("origin_group").(int) > 0 { + req.OriginGroup = pointer.ToInt(d.Get("origin_group").(int)) + } + + if d.Get("origin_protocol") != nil && d.Get("origin_protocol") != "" { + req.OverrideOriginProtocol = pointer.ToString(d.Get("origin_protocol").(string)) + } + + req.Options = listToLocationOptions(d.Get("options").([]interface{})) + + resourceID := d.Get("resource_id").(int) + + if _, err := client.Rules().Update(ctx, int64(resourceID), id, &req); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish CDN Rule updating") + + return resourceCDNRuleRead(ctx, d, m) +} + +func resourceCDNRuleDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + ruleID := d.Id() + log.Printf("[DEBUG] Start CDN Rule deleting (id=%s)\n", ruleID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(ruleID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + resourceID := d.Get("resource_id").(int) + + if err := client.Rules().Delete(ctx, int64(resourceID), id); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Println("[DEBUG] Finish CDN Rule deleting") + + return nil +} + +func listToLocationOptions(l []interface{}) *cdn.LocationOptions { + if len(l) == 0 { + return nil + } + + var opts cdn.LocationOptions + fields := l[0].(map[string]interface{}) + if opt, ok := getOptByName(fields, "allowed_http_methods"); ok { + opts.AllowedHTTPMethods = &cdn.AllowedHTTPMethods{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.AllowedHTTPMethods.Value = append(opts.AllowedHTTPMethods.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "brotli_compression"); ok { + opts.BrotliCompression = &cdn.BrotliCompression{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.BrotliCompression.Value = append(opts.BrotliCompression.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "browser_cache_settings"); ok { + opts.BrowserCacheSettings = &cdn.BrowserCacheSettings{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(string), + } + } + if opt, ok := getOptByName(fields, "cache_http_headers"); ok { + opts.CacheHttpHeaders = &cdn.CacheHttpHeaders{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.CacheHttpHeaders.Value = append(opts.CacheHttpHeaders.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "cors"); ok { + opts.Cors = &cdn.Cors{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.Cors.Value = append(opts.Cors.Value, v.(string)) + } + if _, ok := opt["always"]; ok { + opts.Cors.Always = opt["always"].(bool) + } + } + if opt, ok := getOptByName(fields, "country_acl"); ok { + opts.CountryACL = &cdn.CountryACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.CountryACL.ExceptedValues = append(opts.CountryACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "disable_proxy_force_ranges"); ok { + opts.DisableProxyForceRanges = &cdn.DisableProxyForceRanges{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "edge_cache_settings"); ok { + rawCustomVals := opt["custom_values"].(map[string]interface{}) + customVals := make(map[string]string, len(rawCustomVals)) + for key, value := range rawCustomVals { + customVals[key] = value.(string) + } + + opts.EdgeCacheSettings = &cdn.EdgeCacheSettings{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(string), + CustomValues: customVals, + Default: opt["default"].(string), + } + } + if opt, ok := getOptByName(fields, "fetch_compressed"); ok { + opts.FetchCompressed = &cdn.FetchCompressed{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "follow_origin_redirect"); ok { + opts.FollowOriginRedirect = &cdn.FollowOriginRedirect{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["codes"].(*schema.Set).List() { + opts.FollowOriginRedirect.Codes = append(opts.FollowOriginRedirect.Codes, v.(int)) + } + } + if opt, ok := getOptByName(fields, "force_return"); ok { + opts.ForceReturn = &cdn.ForceReturn{ + Enabled: opt["enabled"].(bool), + Code: opt["code"].(int), + Body: opt["body"].(string), + } + } + if opt, ok := getOptByName(fields, "forward_host_header"); ok { + opts.ForwardHostHeader = &cdn.ForwardHostHeader{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "gzip_on"); ok { + opts.GzipOn = &cdn.GzipOn{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "host_header"); ok { + opts.HostHeader = &cdn.HostHeader{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(string), + } + } + if opt, ok := getOptByName(fields, "ignore_cookie"); ok { + opts.IgnoreCookie = &cdn.IgnoreCookie{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "ignore_query_string"); ok { + opts.IgnoreQueryString = &cdn.IgnoreQueryString{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "image_stack"); ok { + opts.ImageStack = &cdn.ImageStack{ + Enabled: opt["enabled"].(bool), + Quality: opt["quality"].(int), + } + if _, ok := opt["avif_enabled"]; ok { + opts.ImageStack.AvifEnabled = opt["avif_enabled"].(bool) + } + if _, ok := opt["webp_enabled"]; ok { + opts.ImageStack.WebpEnabled = opt["webp_enabled"].(bool) + } + if _, ok := opt["png_lossless"]; ok { + opts.ImageStack.PngLossless = opt["png_lossless"].(bool) + } + } + if opt, ok := getOptByName(fields, "ip_address_acl"); ok { + opts.IPAddressACL = &cdn.IPAddressACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.IPAddressACL.ExceptedValues = append(opts.IPAddressACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "limit_bandwidth"); ok { + opts.LimitBandwidth = &cdn.LimitBandwidth{ + Enabled: opt["enabled"].(bool), + LimitType: opt["limit_type"].(string), + } + if _, ok := opt["speed"]; ok { + opts.LimitBandwidth.Speed = opt["speed"].(int) + } + if _, ok := opt["buffer"]; ok { + opts.LimitBandwidth.Buffer = opt["buffer"].(int) + } + } + if opt, ok := getOptByName(fields, "proxy_cache_methods_set"); ok { + opts.ProxyCacheMethodsSet = &cdn.ProxyCacheMethodsSet{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "query_params_blacklist"); ok { + opts.QueryParamsBlacklist = &cdn.QueryParamsBlacklist{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.QueryParamsBlacklist.Value = append(opts.QueryParamsBlacklist.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "query_params_whitelist"); ok { + opts.QueryParamsWhitelist = &cdn.QueryParamsWhitelist{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.QueryParamsWhitelist.Value = append(opts.QueryParamsWhitelist.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "redirect_http_to_https"); ok { + opts.RedirectHttpToHttps = &cdn.RedirectHttpToHttps{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "redirect_https_to_http"); ok { + opts.RedirectHttpsToHttp = &cdn.RedirectHttpsToHttp{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "referrer_acl"); ok { + opts.ReferrerACL = &cdn.ReferrerACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.ReferrerACL.ExceptedValues = append(opts.ReferrerACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "response_headers_hiding_policy"); ok { + opts.ResponseHeadersHidingPolicy = &cdn.ResponseHeadersHidingPolicy{ + Enabled: opt["enabled"].(bool), + Mode: opt["mode"].(string), + } + for _, v := range opt["excepted"].(*schema.Set).List() { + opts.ResponseHeadersHidingPolicy.Excepted = append(opts.ResponseHeadersHidingPolicy.Excepted, v.(string)) + } + } + if opt, ok := getOptByName(fields, "rewrite"); ok { + opts.Rewrite = &cdn.Rewrite{ + Enabled: opt["enabled"].(bool), + Body: opt["body"].(string), + Flag: opt["flag"].(string), + } + } + if opt, ok := getOptByName(fields, "secure_key"); ok { + opts.SecureKey = &cdn.SecureKey{ + Enabled: opt["enabled"].(bool), + Key: opt["key"].(string), + Type: opt["type"].(int), + } + } + if opt, ok := getOptByName(fields, "slice"); ok { + opts.Slice = &cdn.Slice{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + if opt, ok := getOptByName(fields, "sni"); ok { + opts.SNI = &cdn.SNIOption{ + Enabled: opt["enabled"].(bool), + SNIType: opt["sni_type"].(string), + CustomHostname: opt["custom_hostname"].(string), + } + } + if opt, ok := getOptByName(fields, "stale"); ok { + opts.Stale = &cdn.Stale{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].(*schema.Set).List() { + opts.Stale.Value = append(opts.Stale.Value, v.(string)) + } + } + if opt, ok := getOptByName(fields, "static_headers"); ok { + opts.StaticHeaders = &cdn.StaticHeaders{ + Enabled: opt["enabled"].(bool), + Value: map[string]string{}, + } + for k, v := range opt["value"].(map[string]interface{}) { + opts.StaticHeaders.Value[k] = v.(string) + } + } + if opt, ok := getOptByName(fields, "static_request_headers"); ok { + opts.StaticRequestHeaders = &cdn.StaticRequestHeaders{ + Enabled: opt["enabled"].(bool), + Value: map[string]string{}, + } + for k, v := range opt["value"].(map[string]interface{}) { + opts.StaticRequestHeaders.Value[k] = v.(string) + } + } + if opt, ok := getOptByName(fields, "static_response_headers"); ok { + opts.StaticResponseHeaders = &cdn.StaticResponseHeaders{ + Enabled: opt["enabled"].(bool), + } + for _, v := range opt["value"].([]interface{}) { + itemData := v.(map[string]interface{}) + item := &cdn.StaticResponseHeadersItem{ + Name: itemData["name"].(string), + } + for _, val := range itemData["value"].(*schema.Set).List() { + item.Value = append(item.Value, val.(string)) + } + if _, ok := itemData["always"]; ok { + item.Always = itemData["always"].(bool) + } + opts.StaticResponseHeaders.Value = append(opts.StaticResponseHeaders.Value, *item) + } + } + if opt, ok := getOptByName(fields, "user_agent_acl"); ok { + opts.UserAgentACL = &cdn.UserAgentACL{ + Enabled: opt["enabled"].(bool), + PolicyType: opt["policy_type"].(string), + } + for _, v := range opt["excepted_values"].(*schema.Set).List() { + opts.UserAgentACL.ExceptedValues = append(opts.UserAgentACL.ExceptedValues, v.(string)) + } + } + if opt, ok := getOptByName(fields, "websockets"); ok { + opts.WebSockets = &cdn.WebSockets{ + Enabled: opt["enabled"].(bool), + Value: opt["value"].(bool), + } + } + + return &opts +} + +func locationOptionsToList(options *cdn.LocationOptions) []interface{} { + result := make(map[string][]interface{}) + if options.AllowedHTTPMethods != nil { + m := structToMap(options.AllowedHTTPMethods) + result["allowed_http_methods"] = []interface{}{m} + } + if options.BrotliCompression != nil { + m := structToMap(options.BrotliCompression) + result["brotli_compression"] = []interface{}{m} + } + if options.BrowserCacheSettings != nil { + m := structToMap(options.BrowserCacheSettings) + result["browser_cache_settings"] = []interface{}{m} + } + if options.CacheHttpHeaders != nil { + m := structToMap(options.CacheHttpHeaders) + result["cache_http_headers"] = []interface{}{m} + } + if options.Cors != nil { + m := structToMap(options.Cors) + result["cors"] = []interface{}{m} + } + if options.CountryACL != nil { + m := structToMap(options.CountryACL) + result["country_acl"] = []interface{}{m} + } + if options.DisableProxyForceRanges != nil { + m := structToMap(options.DisableProxyForceRanges) + result["disable_proxy_force_ranges"] = []interface{}{m} + } + if options.EdgeCacheSettings != nil { + m := structToMap(options.EdgeCacheSettings) + result["edge_cache_settings"] = []interface{}{m} + } + if options.FetchCompressed != nil { + m := structToMap(options.FetchCompressed) + result["fetch_compressed"] = []interface{}{m} + } + if options.FollowOriginRedirect != nil { + m := structToMap(options.FollowOriginRedirect) + result["follow_origin_redirect"] = []interface{}{m} + } + if options.ForceReturn != nil { + m := structToMap(options.ForceReturn) + result["force_return"] = []interface{}{m} + } + if options.ForwardHostHeader != nil { + m := structToMap(options.ForwardHostHeader) + result["forward_host_header"] = []interface{}{m} + } + if options.GzipOn != nil { + m := structToMap(options.GzipOn) + result["gzip_on"] = []interface{}{m} + } + if options.HostHeader != nil { + m := structToMap(options.HostHeader) + result["host_header"] = []interface{}{m} + } + if options.IgnoreCookie != nil { + m := structToMap(options.IgnoreCookie) + result["ignore_cookie"] = []interface{}{m} + } + if options.IgnoreQueryString != nil { + m := structToMap(options.IgnoreQueryString) + result["ignore_query_string"] = []interface{}{m} + } + if options.ImageStack != nil { + m := structToMap(options.ImageStack) + result["image_stack"] = []interface{}{m} + } + if options.IPAddressACL != nil { + m := structToMap(options.IPAddressACL) + result["ip_address_acl"] = []interface{}{m} + } + if options.LimitBandwidth != nil { + m := structToMap(options.LimitBandwidth) + result["limit_bandwidth"] = []interface{}{m} + } + if options.ProxyCacheMethodsSet != nil { + m := structToMap(options.ProxyCacheMethodsSet) + result["proxy_cache_methods_set"] = []interface{}{m} + } + if options.QueryParamsBlacklist != nil { + m := structToMap(options.QueryParamsBlacklist) + result["query_params_blacklist"] = []interface{}{m} + } + if options.QueryParamsWhitelist != nil { + m := structToMap(options.QueryParamsWhitelist) + result["query_params_whitelist"] = []interface{}{m} + } + if options.RedirectHttpsToHttp != nil { + m := structToMap(options.RedirectHttpsToHttp) + result["redirect_https_to_http"] = []interface{}{m} + } + if options.RedirectHttpToHttps != nil { + m := structToMap(options.RedirectHttpToHttps) + result["redirect_http_to_https"] = []interface{}{m} + } + if options.ReferrerACL != nil { + m := structToMap(options.ReferrerACL) + result["referrer_acl"] = []interface{}{m} + } + if options.ResponseHeadersHidingPolicy != nil { + m := structToMap(options.ResponseHeadersHidingPolicy) + result["response_headers_hiding_policy"] = []interface{}{m} + } + if options.Rewrite != nil { + m := structToMap(options.Rewrite) + result["rewrite"] = []interface{}{m} + } + if options.SecureKey != nil { + m := structToMap(options.SecureKey) + result["secure_key"] = []interface{}{m} + } + if options.Slice != nil { + m := structToMap(options.Slice) + result["slice"] = []interface{}{m} + } + if options.SNI != nil { + m := structToMap(options.SNI) + result["sni"] = []interface{}{m} + } + if options.Stale != nil { + m := structToMap(options.Stale) + result["stale"] = []interface{}{m} + } + if options.StaticHeaders != nil { + m := structToMap(options.StaticHeaders) + result["static_headers"] = []interface{}{m} + } + if options.StaticRequestHeaders != nil { + m := structToMap(options.StaticRequestHeaders) + result["static_request_headers"] = []interface{}{m} + } + if options.StaticResponseHeaders != nil { + m := structToMap(options.StaticResponseHeaders) + items := []interface{}{} + for _, v := range m["value"].([]cdn.StaticResponseHeadersItem) { + items = append(items, structToMap(v)) + } + m["value"] = items + result["static_response_headers"] = []interface{}{m} + } + if options.UserAgentACL != nil { + m := structToMap(options.UserAgentACL) + result["user_agent_acl"] = []interface{}{m} + } + if options.WebSockets != nil { + m := structToMap(options.WebSockets) + result["websockets"] = []interface{}{m} + } + + return []interface{}{result} +} diff --git a/edgecenter/resource_edgecenter_cdn_sslcerts.go b/edgecenter/resource_edgecenter_cdn_sslcerts.go new file mode 100644 index 00000000..9c1a6451 --- /dev/null +++ b/edgecenter/resource_edgecenter_cdn_sslcerts.go @@ -0,0 +1,124 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercdn-go/sslcerts" +) + +func resourceCDNCert() *schema.Resource { + return &schema.Resource{ + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the SSL certificate. Must be unique.", + }, + "cert": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + ForceNew: true, + Description: "The public part of the SSL certificate. All chain of the SSL certificate should be added.", + }, + "private_key": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + ForceNew: true, + Description: "The private key of the SSL certificate.", + }, + "has_related_resources": { + Type: schema.TypeBool, + Computed: true, + Description: "It shows if the SSL certificate is used by a CDN resource.", + }, + "automated": { + Type: schema.TypeBool, + Computed: true, + Description: "The way SSL certificate was issued.", + }, + }, + CreateContext: resourceCDNCertCreate, + ReadContext: resourceCDNCertRead, + DeleteContext: resourceCDNCertDelete, + } +} + +func resourceCDNCertCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start CDN Cert creating") + config := m.(*Config) + client := config.CDNClient + + var req sslcerts.CreateRequest + req.Name = d.Get("name").(string) + req.Cert = d.Get("cert").(string) + req.PrivateKey = d.Get("private_key").(string) + + result, err := client.SSLCerts().Create(ctx, &req) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%d", result.ID)) + resourceCDNCertRead(ctx, d, m) + + log.Printf("[DEBUG] Finish CDN Cert creating (id=%d)\n", result.ID) + + return nil +} + +func resourceCDNCertRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + certID := d.Id() + log.Printf("[DEBUG] Start CDN Cert reading (id=%s)\n", certID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(certID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + result, err := client.SSLCerts().Get(ctx, id) + if err != nil { + return diag.FromErr(err) + } + + d.Set("has_related_resources", result.HasRelatedResources) + d.Set("automated", result.Automated) + + log.Println("[DEBUG] Finish CDN Cert reading") + + return nil +} + +func resourceCDNCertDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + certID := d.Id() + log.Printf("[DEBUG] Start CDN Cert deleting (id=%s)\n", certID) + config := m.(*Config) + client := config.CDNClient + + id, err := strconv.ParseInt(certID, 10, 64) + if err != nil { + return diag.FromErr(err) + } + + if err := client.SSLCerts().Delete(ctx, id); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Println("[DEBUG] Finish CDN Cert deleting") + + return nil +} diff --git a/edgecenter/resource_edgecenter_dns_zone.go b/edgecenter/resource_edgecenter_dns_zone.go new file mode 100644 index 00000000..e179aa75 --- /dev/null +++ b/edgecenter/resource_edgecenter_dns_zone.go @@ -0,0 +1,120 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +const ( + DNSZoneResource = "edgecenter_dns_zone" + DNSZoneSchemaName = "name" +) + +func resourceDNSZone() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + DNSZoneSchemaName: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + zoneName := i.(string) + if strings.TrimSpace(zoneName) == "" || len(zoneName) > 255 { + return diag.Errorf("dns name can't be empty, it also should be less than 256 symbols") + } + return nil + }, + Description: "A name of DNS Zone resource.", + }, + }, + CreateContext: checkDNSDependency(resourceDNSZoneCreate), + ReadContext: checkDNSDependency(resourceDNSZoneRead), + DeleteContext: checkDNSDependency(resourceDNSZoneDelete), + Description: "Represent DNS zone resource. https://dns.edgecenter.ru/zones", + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func checkDNSDependency(next func(context.Context, *schema.ResourceData, + interface{}) diag.Diagnostics, +) func(context.Context, *schema.ResourceData, interface{}) diag.Diagnostics { + return func(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + config := i.(*Config) + client := config.DNSClient + if client == nil { + return diag.Errorf("dns api client is null. make sure that you defined edgecenter_dns_api var in edgecenter provider section.") + } + return next(ctx, data, i) + } +} + +func resourceDNSZoneCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + name := strings.TrimSpace(d.Get(DNSZoneSchemaName).(string)) + log.Println("[DEBUG] Start DNS Zone Resource creating") + defer log.Printf("[DEBUG] Finish DNS Zone Resource creating (id=%s)\n", name) + + config := m.(*Config) + client := config.DNSClient + + _, err := client.CreateZone(ctx, name) + if err != nil { + return diag.FromErr(fmt.Errorf("create zone: %w", err)) + } + d.SetId(name) + + return resourceDNSZoneRead(ctx, d, m) +} + +func resourceDNSZoneRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + zoneName := dnsZoneResourceID(d) + log.Printf("[DEBUG] Start DNS Zone Resource reading (id=%s)\n", zoneName) + defer log.Println("[DEBUG] Finish DNS Zone Resource reading") + + config := m.(*Config) + client := config.DNSClient + + result, err := client.Zone(ctx, zoneName) + if err != nil { + return diag.FromErr(fmt.Errorf("get zone: %w", err)) + } + d.SetId(result.Name) + _ = d.Set(DNSZoneSchemaName, result.Name) + + return nil +} + +func resourceDNSZoneDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + zoneName := dnsZoneResourceID(d) + log.Printf("[DEBUG] Start DNS Zone Resource deleting (id=%s)\n", zoneName) + defer log.Println("[DEBUG] Finish DNS Zone Resource deleting") + if zoneName == "" { + return diag.Errorf("empty zone name") + } + + config := m.(*Config) + client := config.DNSClient + + err := client.DeleteZone(ctx, zoneName) + if err != nil { + return diag.FromErr(fmt.Errorf("delete zone: %w", err)) + } + d.SetId("") + + return nil +} + +func dnsZoneResourceID(d *schema.ResourceData) string { + resourceID := d.Id() + if resourceID == "" { + resourceID = d.Get(DNSZoneSchemaName).(string) + } + return resourceID +} diff --git a/edgecenter/resource_edgecenter_dns_zone_record.go b/edgecenter/resource_edgecenter_dns_zone_record.go new file mode 100644 index 00000000..42b1623d --- /dev/null +++ b/edgecenter/resource_edgecenter_dns_zone_record.go @@ -0,0 +1,502 @@ +package edgecenter + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net" + "strings" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + dnssdk "github.com/Edge-Center/edgecenter-dns-sdk-go" +) + +const ( + DNSZoneRecordResource = "edgecenter_dns_zone_record" + + DNSZoneRecordSchemaZone = "zone" + DNSZoneRecordSchemaDomain = "domain" + DNSZoneRecordSchemaType = "type" + DNSZoneRecordSchemaTTL = "ttl" + DNSZoneRecordSchemaFilter = "filter" + + DNSZoneRecordSchemaFilterLimit = "limit" + DNSZoneRecordSchemaFilterType = "type" + DNSZoneRecordSchemaFilterStrict = "strict" + + DNSZoneRecordSchemaResourceRecord = "resource_record" + DNSZoneRecordSchemaContent = "content" + DNSZoneRecordSchemaEnabled = "enabled" + DNSZoneRecordSchemaMeta = "meta" + + DNSZoneRecordSchemaMetaAsn = "asn" + DNSZoneRecordSchemaMetaIP = "ip" + DNSZoneRecordSchemaMetaCountries = "countries" + DNSZoneRecordSchemaMetaContinents = "continents" + DNSZoneRecordSchemaMetaLatLong = "latlong" + DNSZoneRecordSchemaMetaNotes = "notes" + DNSZoneRecordSchemaMetaDefault = "default" +) + +func resourceDNSZoneRecord() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + DNSZoneRecordSchemaZone: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + val := i.(string) + if strings.TrimSpace(val) == "" || len(val) > 255 { + return diag.Errorf("dns record zone can't be empty, it also should be less than 256 symbols") + } + return nil + }, + Description: "A zone of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaDomain: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + val := i.(string) + if strings.TrimSpace(val) == "" || len(val) > 255 { + return diag.Errorf("dns record domain can't be empty, it also should be less than 256 symbols") + } + return nil + }, + Description: "A domain of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaType: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + val := strings.TrimSpace(i.(string)) + types := []string{"A", "AAAA", "MX", "CNAME", "TXT", "CAA", "NS", "SRV"} + for _, t := range types { + if strings.EqualFold(t, val) { + return nil + } + } + return diag.Errorf("dns record type should be one of %v", types) + }, + Description: "A type of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaTTL: { + Type: schema.TypeInt, + Optional: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + val := i.(int) + if val < 0 { + return diag.Errorf("dns record ttl can't be less than 0") + } + return nil + }, + Description: "A ttl of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaFilter: { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + DNSZoneRecordSchemaFilterLimit: { + Type: schema.TypeInt, + Optional: true, + Description: "A DNS Zone Record filter option that describe how many records will be percolated.", + }, + DNSZoneRecordSchemaFilterStrict: { + Type: schema.TypeBool, + Optional: true, + Description: "A DNS Zone Record filter option that describe possibility to return answers if no records were percolated through filter.", + }, + DNSZoneRecordSchemaFilterType: { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + names := []string{"geodns", "geodistance", "default", "first_n"} + name := i.(string) + for _, n := range names { + if n == name { + return nil + } + } + return diag.Errorf("dns record filter type should be one of %v", names) + }, + Description: "A DNS Zone Record filter option that describe a name of filter.", + }, + }, + }, + }, + DNSZoneRecordSchemaResourceRecord: { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + DNSZoneRecordSchemaContent: { + Type: schema.TypeString, + Required: true, + Description: `A content of DNS Zone Record resource. (TXT: 'anyString', MX: '50 mail.company.io.', CAA: '0 issue "company.org; account=12345"')`, + }, + DNSZoneRecordSchemaEnabled: { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Manage of public appearing of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMeta: { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + DNSZoneRecordSchemaMetaAsn: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeInt, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + if i.(int) < 0 { + return diag.Errorf("asn cannot be less then 0") + } + return nil + }, + }, + Optional: true, + Description: "An asn meta (e.g. 12345) of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMetaIP: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + val := i.(string) + ip := net.ParseIP(val) + if ip == nil { + return diag.Errorf("dns record meta ip has wrong format: %s", val) + } + return nil + }, + }, + Optional: true, + Description: "An ip meta (e.g. 127.0.0.0) of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMetaLatLong: { + Optional: true, + Type: schema.TypeList, + MaxItems: 2, + MinItems: 2, + Elem: &schema.Schema{ + Type: schema.TypeFloat, + }, + Description: "A latlong meta (e.g. 27.988056, 86.925278) of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMetaNotes: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "A notes meta (e.g. Miami DC) of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMetaContinents: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "Continents meta (e.g. Asia) of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMetaCountries: { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "Countries meta (e.g. USA) of DNS Zone Record resource.", + }, + DNSZoneRecordSchemaMetaDefault: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Fallback meta equals true marks records which are used as a default answer (when nothing was selected by specified meta fields).", + }, + }, + }, + }, + }, + }, + Description: "An array of contents with meta of DNS Zone Record resource.", + }, + }, + CreateContext: checkDNSDependency(resourceDNSZoneRecordCreate), + UpdateContext: checkDNSDependency(resourceDNSZoneRecordUpdate), + ReadContext: checkDNSDependency(resourceDNSZoneRecordRead), + DeleteContext: checkDNSDependency(resourceDNSZoneRecordDelete), + Description: "Represent DNS Zone Record resource. https://dns.edgecenter.ru/zones", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + parts := strings.Split(d.Id(), ":") + if len(parts) != 3 { + return nil, fmt.Errorf("format must be as zone:domain:type") + } + _ = d.Set(DNSZoneRecordSchemaZone, parts[0]) + d.SetId(parts[0]) + _ = d.Set(DNSZoneRecordSchemaDomain, parts[1]) + _ = d.Set(DNSZoneRecordSchemaType, parts[2]) + + return []*schema.ResourceData{d}, nil + }, + }, + } +} + +func resourceDNSZoneRecordCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + zone := strings.TrimSpace(d.Get(DNSZoneRecordSchemaZone).(string)) + domain := strings.TrimSpace(d.Get(DNSZoneRecordSchemaDomain).(string)) + rType := strings.TrimSpace(d.Get(DNSZoneRecordSchemaType).(string)) + log.Println("[DEBUG] Start DNS Zone Record Resource creating") + defer log.Printf("[DEBUG] Finish DNS Zone Record Resource creating (id=%s %s %s)\n", zone, domain, rType) + + ttl := d.Get(DNSZoneRecordSchemaTTL).(int) + rrSet := dnssdk.RRSet{TTL: ttl, Records: make([]dnssdk.ResourceRecord, 0)} + err := fillRRSet(d, rType, &rrSet) + if err != nil { + return diag.FromErr(err) + } + + config := m.(*Config) + client := config.DNSClient + + _, err = client.Zone(ctx, zone) + if err != nil { + return diag.FromErr(fmt.Errorf("find zone: %w", err)) + } + + err = client.CreateRRSet(ctx, zone, domain, rType, rrSet) + if err != nil { + return diag.FromErr(fmt.Errorf("create zone rrset: %w", err)) + } + d.SetId(zone) + + return resourceDNSZoneRecordRead(ctx, d, m) +} + +func resourceDNSZoneRecordUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if d.Id() == "" { + return diag.Errorf("empty id") + } + zone := strings.TrimSpace(d.Get(DNSZoneRecordSchemaZone).(string)) + domain := strings.TrimSpace(d.Get(DNSZoneRecordSchemaDomain).(string)) + rType := strings.TrimSpace(d.Get(DNSZoneRecordSchemaType).(string)) + log.Println("[DEBUG] Start DNS Zone Record Resource updating") + defer log.Printf("[DEBUG] Finish DNS Zone Record Resource updating (id=%s %s %s)\n", zone, domain, rType) + + ttl := d.Get(DNSZoneRecordSchemaTTL).(int) + rrSet := dnssdk.RRSet{TTL: ttl, Records: make([]dnssdk.ResourceRecord, 0)} + err := fillRRSet(d, rType, &rrSet) + if err != nil { + return diag.FromErr(err) + } + + config := m.(*Config) + client := config.DNSClient + + err = client.UpdateRRSet(ctx, zone, domain, rType, rrSet) + if err != nil { + return diag.FromErr(fmt.Errorf("update zone rrset: %w", err)) + } + d.SetId(zone) + + return resourceDNSZoneRecordRead(ctx, d, m) +} + +func resourceDNSZoneRecordRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if d.Id() == "" { + return diag.Errorf("empty id") + } + zone := strings.TrimSpace(d.Get(DNSZoneRecordSchemaZone).(string)) + domain := strings.TrimSpace(d.Get(DNSZoneRecordSchemaDomain).(string)) + rType := strings.TrimSpace(d.Get(DNSZoneRecordSchemaType).(string)) + log.Println("[DEBUG] Start DNS Zone Record Resource reading") + defer log.Printf("[DEBUG] Finish DNS Zone Record Resource reading (id=%s %s %s)\n", zone, domain, rType) + + config := m.(*Config) + client := config.DNSClient + + result, err := client.RRSet(ctx, zone, domain, rType) + if err != nil { + return diag.FromErr(fmt.Errorf("get zone rrset: %w", err)) + } + id := struct{ Zone, Domain, Type string }{zone, domain, rType} //nolint: musttag + bs, err := json.Marshal(id) + if err != nil { + return diag.FromErr(err) + } + d.SetId(string(bs)) + _ = d.Set(DNSZoneRecordSchemaZone, zone) + _ = d.Set(DNSZoneRecordSchemaDomain, domain) + _ = d.Set(DNSZoneRecordSchemaType, rType) + _ = d.Set(DNSZoneRecordSchemaTTL, result.TTL) + + filters := make([]map[string]interface{}, 0) + for _, f := range result.Filters { + filters = append(filters, map[string]interface{}{ + DNSZoneRecordSchemaFilterLimit: f.Limit, + DNSZoneRecordSchemaFilterType: f.Type, + DNSZoneRecordSchemaFilterStrict: f.Strict, + }) + } + if len(filters) > 0 { + _ = d.Set(DNSZoneRecordSchemaFilter, filters) + } + + rr := make([]map[string]interface{}, 0) + for _, rec := range result.Records { + r := map[string]interface{}{} + r[DNSZoneRecordSchemaEnabled] = rec.Enabled + r[DNSZoneRecordSchemaContent] = rec.ContentToString() + meta := map[string]interface{}{} + for key, val := range rec.Meta { + meta[key] = val + } + if len(meta) > 0 { + r[DNSZoneRecordSchemaMeta] = []map[string]interface{}{meta} + } + rr = append(rr, r) + } + if len(rr) > 0 { + _ = d.Set(DNSZoneRecordSchemaResourceRecord, rr) + } + + return nil +} + +func resourceDNSZoneRecordDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + if d.Id() == "" { + return diag.Errorf("empty id") + } + zone := strings.TrimSpace(d.Get(DNSZoneRecordSchemaZone).(string)) + domain := strings.TrimSpace(d.Get(DNSZoneRecordSchemaDomain).(string)) + rType := strings.TrimSpace(d.Get(DNSZoneRecordSchemaType).(string)) + log.Println("[DEBUG] Start DNS Zone Record Resource deleting") + defer log.Printf("[DEBUG] Finish DNS Zone Record Resource deleting (id=%s %s %s)\n", zone, domain, rType) + + config := m.(*Config) + client := config.DNSClient + + err := client.DeleteRRSet(ctx, zone, domain, rType) + if err != nil { + return diag.FromErr(fmt.Errorf("delete zone rrset: %w", err)) + } + + d.SetId("") + + return nil +} + +func fillRRSet(d *schema.ResourceData, rType string, rrSet *dnssdk.RRSet) error { + // set filters + for _, resource := range d.Get(DNSZoneRecordSchemaFilter).(*schema.Set).List() { + filter := dnssdk.RecordFilter{} + filterData := resource.(map[string]interface{}) + name := filterData[DNSZoneRecordSchemaFilterType].(string) + filter.Type = name + limit, ok := filterData[DNSZoneRecordSchemaFilterLimit].(int) + if ok { + filter.Limit = uint(limit) + } + strict, ok := filterData[DNSZoneRecordSchemaFilterStrict].(bool) + if ok { + filter.Strict = strict + } + rrSet.AddFilter(filter) + } + // set meta + for _, resource := range d.Get(DNSZoneRecordSchemaResourceRecord).(*schema.Set).List() { + data := resource.(map[string]interface{}) + content := data[DNSZoneRecordSchemaContent].(string) + rr := (&dnssdk.ResourceRecord{}).SetContent(rType, content) + enabled := data[DNSZoneRecordSchemaEnabled].(bool) + rr.Enabled = enabled + metaErrs := make([]error, 0) + + for _, dataMeta := range data[DNSZoneRecordSchemaMeta].(*schema.Set).List() { + meta := dataMeta.(map[string]interface{}) + validWrap := func(rm dnssdk.ResourceMeta) dnssdk.ResourceMeta { + if rm.Valid() != nil { + metaErrs = append(metaErrs, rm.Valid()) + } + return rm + } + + val := meta[DNSZoneRecordSchemaMetaIP].([]interface{}) + ips := make([]string, len(val)) + for i, v := range val { + ips[i] = v.(string) + } + if len(ips) > 0 { + rr.AddMeta(dnssdk.NewResourceMetaIP(ips...)) + } + + val = meta[DNSZoneRecordSchemaMetaCountries].([]interface{}) + countries := make([]string, len(val)) + for i, v := range val { + countries[i] = v.(string) + } + if len(countries) > 0 { + rr.AddMeta(dnssdk.NewResourceMetaCountries(countries...)) + } + + val = meta[DNSZoneRecordSchemaMetaContinents].([]interface{}) + continents := make([]string, len(val)) + for i, v := range val { + continents[i] = v.(string) + } + if len(continents) > 0 { + rr.AddMeta(dnssdk.NewResourceMetaContinents(continents...)) + } + + val = meta[DNSZoneRecordSchemaMetaNotes].([]interface{}) + notes := make([]string, len(val)) + for i, v := range val { + notes[i] = v.(string) + } + if len(notes) > 0 { + rr.AddMeta(dnssdk.NewResourceMetaNotes(notes...)) + } + + latLongVal := meta[DNSZoneRecordSchemaMetaLatLong].([]interface{}) + if len(latLongVal) == 2 { + rr.AddMeta( + validWrap( + dnssdk.NewResourceMetaLatLong( + fmt.Sprintf("%f,%f", latLongVal[0].(float64), latLongVal[1].(float64))))) + } + + val = meta[DNSZoneRecordSchemaMetaAsn].([]interface{}) + asn := make([]uint64, len(val)) + for i, v := range val { + asn[i] = uint64(v.(int)) + } + if len(asn) > 0 { + rr.AddMeta(dnssdk.NewResourceMetaAsn(asn...)) + } + + valDefault := meta[DNSZoneRecordSchemaMetaDefault].(bool) + if valDefault { + rr.AddMeta(validWrap(dnssdk.NewResourceMetaDefault())) + } + } + + if len(metaErrs) > 0 { + return fmt.Errorf("invalid meta for zone rrset with content %s: %v", content, metaErrs) + } + rrSet.Records = append(rrSet.Records, *rr) + } + + return nil +} diff --git a/edgecenter/resource_edgecenter_floatingip.go b/edgecenter/resource_edgecenter_floatingip.go new file mode 100644 index 00000000..54e548c6 --- /dev/null +++ b/edgecenter/resource_edgecenter_floatingip.go @@ -0,0 +1,353 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "net" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/floatingip/v1/floatingips" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +const ( + FloatingIPsPoint = "floatingips" + FloatingIPCreateTimeout = 1200 +) + +func resourceFloatingIP() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceFloatingIPCreate, + ReadContext: resourceFloatingIPRead, + UpdateContext: resourceFloatingIPUpdate, + DeleteContext: resourceFloatingIPDelete, + Description: `A floating IP is a static IP address that can be associated with one of your instances or loadbalancers, +allowing it to have a static public IP address. The floating IP can be re-associated to any other instance in the same datacenter.`, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, fipID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(fipID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "floating_ip_address": { + Type: schema.TypeString, + Computed: true, + Description: "The floating IP address assigned to the resource.", + }, + "port_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The ID (uuid) of the network port that the floating IP is associated with.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the floating IP. Can be 'DOWN' or 'ACTIVE'.", + }, + "fixed_ip_address": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The fixed (reserved) IP address that is associated with the floating IP.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + ip := net.ParseIP(v) + if ip != nil { + return diag.Diagnostics{} + } + + return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) + }, + }, + "router_id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID (uuid) of the router that the floating IP is associated with.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the floating IP was created.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the floating IP was updated.", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceFloatingIPCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start FloatingIP creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, FloatingIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := floatingips.CreateOpts{ + PortID: d.Get("port_id").(string), + FixedIPAddress: net.ParseIP(d.Get("fixed_ip_address").(string)), + } + + if metadataRaw, ok := d.GetOk("metadata_map"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + opts.Metadata = meta + } + + results, err := floatingips.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + floatingIPID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, FloatingIPCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + floatingIPID, err := floatingips.ExtractFloatingIPIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve FloatingIP ID from task info: %w", err) + } + return floatingIPID, nil + }) + + log.Printf("[DEBUG] FloatingIP id (%s)", floatingIPID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(floatingIPID.(string)) + resourceFloatingIPRead(ctx, d, m) + + log.Printf("[DEBUG] Finish FloatingIP creating (%s)", floatingIPID) + + return diags +} + +func resourceFloatingIPRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start FloatingIP reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, FloatingIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + floatingIP, err := floatingips.Get(client, d.Id()).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + log.Printf("[WARN] Removing floating ip %s because resource doesn't exist anymore", d.Id()) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + if floatingIP.FixedIPAddress != nil { + d.Set("fixed_ip_address", floatingIP.FixedIPAddress.String()) + } else { + d.Set("fixed_ip_address", "") + } + + d.Set("project_id", floatingIP.ProjectID) + d.Set("region_id", floatingIP.RegionID) + d.Set("status", floatingIP.Status) + d.Set("port_id", floatingIP.PortID) + d.Set("router_id", floatingIP.RouterID) + d.Set("floating_ip_address", floatingIP.FloatingIPAddress.String()) + + metadataMap, metadataReadOnly := PrepareMetadata(floatingIP.Metadata) + + if err = d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish FloatingIP reading") + + return diags +} + +func resourceFloatingIPUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start FloatingIP updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, FloatingIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChanges("fixed_ip_address", "port_id") { + oldFixedIP, newFixedIP := d.GetChange("fixed_ip_address") + oldPortID, newPortID := d.GetChange("port_id") + if oldPortID.(string) != "" || oldFixedIP.(string) != "" { + _, err := floatingips.UnAssign(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + } + + if newPortID.(string) != "" || newFixedIP.(string) != "" { + opts := floatingips.CreateOpts{ + PortID: d.Get("port_id").(string), + FixedIPAddress: net.ParseIP(d.Get("fixed_ip_address").(string)), + } + + _, err = floatingips.Assign(client, d.Id(), opts).Extract() + if err != nil { + return diag.FromErr(err) + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + + meta, err := utils.MapInterfaceToMapString(nmd.(map[string]interface{})) + if err != nil { + return diag.Errorf("cannot get metadata. Error: %s", err) + } + + err = metadata.ResourceMetadataReplace(client, d.Id(), meta).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + return resourceFloatingIPRead(ctx, d, m) +} + +func resourceFloatingIPDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start FloatingIP deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, FloatingIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + results, err := floatingips.Delete(client, id).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, FloatingIPCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := floatingips.Get(client, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete floating ip with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting FloatingIP resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of FloatingIP deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_instance.go b/edgecenter/resource_edgecenter_instance.go new file mode 100644 index 00000000..31142501 --- /dev/null +++ b/edgecenter/resource_edgecenter_instance.go @@ -0,0 +1,1069 @@ +package edgecenter + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "log" + "sort" + "strconv" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/instances" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + edgecloudMeta "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" +) + +const ( + InstanceDeleting int = 1200 + InstanceCreatingTimeout int = 1200 + InstancePoint = "instances" + + InstanceVMStateActive = "active" + InstanceVMStateStopped = "stopped" +) + +func resourceInstance() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceInstanceCreate, + ReadContext: resourceInstanceRead, + UpdateContext: resourceInstanceUpdate, + DeleteContext: resourceInstanceDelete, + Description: "A cloud instance is a virtual machine in a cloud environment.", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, InstanceID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(InstanceID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The name of the instance.", + }, + "flavor_id": { + Type: schema.TypeString, + Required: true, + Description: "The ID of the flavor to be used for the instance, determining its compute and memory, for example 'g1-standard-2-4'.", + }, + "name_templates": { + Type: schema.TypeList, + Optional: true, + Deprecated: "Use name_template instead.", + ConflictsWith: []string{"name_template"}, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "name_template": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"name_templates"}, + Description: "A template used to generate the instance name. This field cannot be used with 'name_templates'.", + }, + "volume": { + Type: schema.TypeSet, + Required: true, + Set: volumeUniqueID, + Description: "A set defining the volumes to be attached to the instance.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Description: "The name assigned to the volume. Defaults to 'system'.", + }, + "source": { + Type: schema.TypeString, + Required: true, + Description: "Currently available only 'existing-volume' value", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + if types.VolumeSource(v) == types.ExistingVolume { + return diag.Diagnostics{} + } + return diag.Errorf("wrong source type %s, now available values is '%s'", v, types.ExistingVolume) + }, + }, + "boot_index": { + Type: schema.TypeInt, + Description: "If boot_index==0 volumes can not detached", + Optional: true, + }, + "type_name": { + Type: schema.TypeString, + Optional: true, + Description: "The type of volume to create. Valid values are 'ssd_hiiops', 'standard', 'cold', and 'ultra'. Defaults to 'standard'.", + }, + "image_id": { + Type: schema.TypeString, + Optional: true, + }, + "size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "The size of the volume, specified in gigabytes (GB).", + }, + "volume_id": { + Type: schema.TypeString, + Optional: true, + }, + "attachment_tag": { + Type: schema.TypeString, + Optional: true, + }, + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "delete_on_termination": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + }, + }, + }, + "interface": { + Type: schema.TypeList, + Required: true, + Description: "A list defining the network interfaces to be attached to the instance.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Optional: true, + Description: fmt.Sprintf("Available value is '%s', '%s', '%s', '%s'", types.SubnetInterfaceType, types.AnySubnetInterfaceType, types.ExternalInterfaceType, types.ReservedFixedIPType), + }, + "order": { + Type: schema.TypeInt, + Optional: true, + Description: "Order of attaching interface", + Computed: true, + }, + "network_id": { + Type: schema.TypeString, + Description: "Required if type is 'subnet' or 'any_subnet'.", + Optional: true, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Description: "Required if type is 'subnet'.", + Optional: true, + Computed: true, + }, + // nested map is not supported, in this case, you do not need to use the list for the map + "fip_source": { + Type: schema.TypeString, + Optional: true, + }, + "existing_fip_id": { + Type: schema.TypeString, + Optional: true, + }, + "port_id": { + Type: schema.TypeString, + Computed: true, + Description: "required if type is 'reserved_fixed_ip'", + Optional: true, + }, + "security_groups": { + Type: schema.TypeList, + Optional: true, + Description: "list of security group IDs", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "ip_address": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + }, + }, + "keypair_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the key pair to be associated with the instance for SSH access.", + }, + "server_group": { + Type: schema.TypeString, + Optional: true, + Description: "The ID (uuid) of the server group to which the instance should belong.", + }, + "security_group": { + Type: schema.TypeList, + Computed: true, + Description: "A list of firewall configurations applied to the instance, defined by their ID and name.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "Firewall unique id (uuid)", + Required: true, + }, + "name": { + Type: schema.TypeString, + Description: "Firewall name", + Required: true, + }, + }, + }, + }, + "password": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"username"}, + Description: "The password to be used for accessing the instance. Required with username.", + }, + "username": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"password"}, + Description: "The username to be used for accessing the instance. Required with password.", + }, + "metadata": { + Type: schema.TypeList, + Optional: true, + Deprecated: "Use metadata_map instead", + ConflictsWith: []string{"metadata_map"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + ConflictsWith: []string{"metadata"}, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "configuration": { + Type: schema.TypeList, + Optional: true, + Description: `A list of key-value pairs specifying configuration settings for the instance when created +from a template (marketplace), e.g. {"gitlab_external_url": "https://gitlab/..."}`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Required: true, + }, + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "userdata": { + Type: schema.TypeString, + Optional: true, + Description: "**Deprecated**", + Deprecated: "Use user_data instead", + ConflictsWith: []string{"user_data"}, + }, + "user_data": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"userdata"}, + Description: "A field for specifying user data to be used for configuring the instance at launch time.", + }, + "allow_app_ports": { + Type: schema.TypeBool, + Optional: true, + Description: "A boolean indicating whether to allow application ports on the instance.", + }, + "flavor": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Description: `A map defining the flavor of the instance, for example, {"flavor_name": "g1-standard-2-4", "ram": 4096, ...}.`, + }, + "status": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The current status of the instance. This is computed automatically and can be used to track the instance's state.", + }, + "vm_state": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: fmt.Sprintf(`The current virtual machine state of the instance, +allowing you to start or stop the VM. Possible values are %s and %s.`, InstanceVMStateStopped, InstanceVMStateActive), + ValidateFunc: validation.StringInSlice([]string{InstanceVMStateActive, InstanceVMStateStopped}, true), + }, + "addresses": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: `A list of network addresses associated with the instance, for example "pub_net": [...]`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "net": { + Type: schema.TypeList, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "addr": { + Type: schema.TypeString, + Required: true, + Description: "The net ip address, for example '45.147.163.112'.", + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: "The net type, for example 'fixed'.", + }, + }, + }, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceInstanceCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Instance creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + clientV1, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clientV2, err := CreateClient(provider, d, InstancePoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + createOpts := instances.CreateOpts{ + Flavor: d.Get("flavor_id").(string), + SecurityGroups: []edgecloud.ItemID{}, + Keypair: d.Get("keypair_name").(string), + Password: d.Get("password").(string), + Username: d.Get("username").(string), + ServerGroupID: d.Get("server_group").(string), + AllowAppPorts: d.Get("allow_app_ports").(bool), + } + + if userData, ok := d.GetOk("user_data"); ok { + createOpts.UserData = base64.StdEncoding.EncodeToString([]byte(userData.(string))) + } else if userData, ok := d.GetOk("userdata"); ok { + createOpts.UserData = base64.StdEncoding.EncodeToString([]byte(userData.(string))) + } + + name := d.Get("name").(string) + if len(name) > 0 { + createOpts.Names = []string{name} + } + + if nameTemplatesRaw, ok := d.GetOk("name_templates"); ok { + nameTemplates := nameTemplatesRaw.([]interface{}) + if len(nameTemplates) > 0 { + NameTemp := make([]string, len(nameTemplates)) + for i, nametemp := range nameTemplates { + NameTemp[i] = nametemp.(string) + } + createOpts.NameTemplates = NameTemp + } + } else if nameTemplate, ok := d.GetOk("name_template"); ok { + createOpts.NameTemplates = []string{nameTemplate.(string)} + } + + currentVols := d.Get("volume").(*schema.Set).List() + if len(currentVols) > 0 { + vs, err := extractVolumesMap(currentVols) + if err != nil { + return diag.FromErr(err) + } + createOpts.Volumes = vs + } + + ifs := d.Get("interface").([]interface{}) + if len(ifs) > 0 { + interfacesList, err := extractInstanceInterfaceToListCreate(ifs) + if err != nil { + return diag.FromErr(err) + } + createOpts.Interfaces = interfacesList + } + + if metadata, ok := d.GetOk("metadata"); ok { + if len(metadata.([]interface{})) > 0 { + md, err := extractKeyValue(metadata.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + createOpts.Metadata = &md + } + } else if metadataRaw, ok := d.GetOk("metadata_map"); ok { + md := extractMetadataMap(metadataRaw.(map[string]interface{})) + createOpts.Metadata = &md + } + + configuration := d.Get("configuration") + if len(configuration.([]interface{})) > 0 { + conf, err := extractKeyValue(configuration.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + createOpts.Configuration = &conf + } + + log.Printf("[DEBUG] Instance create options: %+v", createOpts) + results, err := instances.Create(clientV2, createOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + InstanceID, err := tasks.WaitTaskAndReturnResult(clientV1, taskID, true, InstanceCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(clientV1, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Instance, err := instances.ExtractInstanceIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Instance ID from task info: %w", err) + } + return Instance, nil + }, + ) + log.Printf("[DEBUG] Instance id (%s)", InstanceID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(InstanceID.(string)) + resourceInstanceRead(ctx, d, m) + + log.Printf("[DEBUG] Finish Instance creating (%s)", InstanceID) + + return diags +} + +func resourceInstanceRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Instance reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + instanceID := d.Id() + log.Printf("[DEBUG] Instance id = %s", instanceID) + + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clientV2, err := CreateClient(provider, d, InstancePoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + instance, err := instances.Get(client, instanceID).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + log.Printf("[WARN] Removing instance %s because resource doesn't exist anymore", d.Id()) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + d.Set("name", instance.Name) + d.Set("flavor_id", instance.Flavor.FlavorID) + d.Set("status", instance.Status) + d.Set("vm_state", instance.VMState) + + flavor := make(map[string]interface{}, 4) + flavor["flavor_id"] = instance.Flavor.FlavorID + flavor["flavor_name"] = instance.Flavor.FlavorName + flavor["ram"] = strconv.Itoa(instance.Flavor.RAM) + flavor["vcpus"] = strconv.Itoa(instance.Flavor.VCPUS) + d.Set("flavor", flavor) + + currentVolumes := extractVolumesIntoMap(d.Get("volume").(*schema.Set).List()) + + extVolumes := make([]interface{}, 0, len(instance.Volumes)) + for _, vol := range instance.Volumes { + v, ok := currentVolumes[vol.ID] + // todo fix it + if !ok { + v = make(map[string]interface{}) + v["volume_id"] = vol.ID + v["source"] = types.ExistingVolume.String() + } + + v["id"] = vol.ID + v["delete_on_termination"] = vol.DeleteOnTermination + extVolumes = append(extVolumes, v) + } + + if err := d.Set("volume", schema.NewSet(volumeUniqueID, extVolumes)); err != nil { + return diag.FromErr(err) + } + + instancePorts, err := instances.ListPortsAll(client, instanceID) + if err != nil { + return diag.FromErr(err) + } + secGroups := prepareSecurityGroups(instancePorts) + + if err := d.Set("security_group", secGroups); err != nil { + return diag.FromErr(err) + } + + interfacesListAPI, err := instances.ListInterfacesAll(client, instanceID) + if err != nil { + return diag.FromErr(err) + } + + ifs := d.Get("interface").([]interface{}) + sort.Sort(instanceInterfaces(ifs)) + interfacesListExtracted, err := extractInstanceInterfaceToListRead(ifs) + if err != nil { + return diag.FromErr(err) + } + + var interfacesList []interface{} + for order, iFace := range interfacesListAPI { + if len(iFace.IPAssignments) == 0 { + continue + } + + portID := iFace.PortID + for _, assignment := range iFace.IPAssignments { + subnetID := assignment.SubnetID + ipAddress := assignment.IPAddress.String() + + var interfaceOpts instances.InterfaceOpts + for _, interfaceExtracted := range interfacesListExtracted { + if interfaceExtracted.SubnetID == subnetID || interfaceExtracted.IPAddress == ipAddress || interfaceExtracted.PortID == portID { + interfaceOpts = interfaceExtracted + break + } + } + + i := make(map[string]interface{}) + i["type"] = interfaceOpts.Type.String() + i["order"] = order + i["network_id"] = iFace.NetworkID + i["subnet_id"] = subnetID + i["port_id"] = portID + if interfaceOpts.FloatingIP != nil { + i["fip_source"] = interfaceOpts.FloatingIP.Source.String() + i["existing_fip_id"] = interfaceOpts.FloatingIP.ExistingFloatingID + } + i["ip_address"] = ipAddress + + if port, err := findInstancePort(portID, instancePorts); err == nil { + sgs := make([]string, len(port.SecurityGroups)) + for i, sg := range port.SecurityGroups { + sgs[i] = sg.ID + } + i["security_groups"] = sgs + } + + interfacesList = append(interfacesList, i) + } + } + if err := d.Set("interface", interfacesList); err != nil { + return diag.FromErr(err) + } + + if metadataRaw, ok := d.GetOk("metadata"); ok { + metadata := metadataRaw.([]interface{}) + sliced := make([]map[string]string, len(metadata)) + for i, data := range metadata { + d := data.(map[string]interface{}) + mdata := make(map[string]string, 2) + md, err := instances.MetadataGet(client, instanceID, d["key"].(string)).Extract() + if err != nil { + return diag.Errorf("cannot get metadata with key: %s. Error: %s", instanceID, err) + } + mdata["key"] = md.Key + mdata["value"] = md.Value + sliced[i] = mdata + } + d.Set("metadata", sliced) + } else { + metadata := d.Get("metadata_map").(map[string]interface{}) + newMetadata := make(map[string]interface{}, len(metadata)) + for k := range metadata { + md, err := edgecloudMeta.ResourceMetadataGet(clientV2, instanceID, k).Extract() + if err != nil { + return diag.Errorf("cannot get metadata with key: %s. Error: %s", instanceID, err) + } + newMetadata[k] = md.Value + } + if err := d.Set("metadata_map", newMetadata); err != nil { + return diag.FromErr(err) + } + } + + addresses := []map[string][]map[string]string{} + for _, data := range instance.Addresses { + d := map[string][]map[string]string{} + netd := make([]map[string]string, len(data)) + for i, iaddr := range data { + ndata := make(map[string]string, 2) + ndata["type"] = iaddr.Type.String() + ndata["addr"] = iaddr.Address.String() + netd[i] = ndata + } + d["net"] = netd + addresses = append(addresses, d) + } + if err := d.Set("addresses", addresses); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish Instance reading") + + return diags +} + +func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Instance updating") + instanceID := d.Id() + log.Printf("[DEBUG] Instance id = %s", instanceID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + nameTemplates := d.Get("name_templates").([]interface{}) + nameTemplate := d.Get("name_template").(string) + if len(nameTemplate) == 0 && len(nameTemplates) == 0 { + opts := instances.RenameInstanceOpts{ + Name: d.Get("name").(string), + } + if _, err := instances.RenameInstance(client, instanceID, opts).Extract(); err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("flavor_id") { + flavorID := d.Get("flavor_id").(string) + results, err := instances.Resize(client, instanceID, instances.ChangeFlavorOpts{FlavorID: flavorID}).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + taskState, err := tasks.WaitTaskAndReturnResult(client, taskID, true, InstanceCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + return taskInfo.State, nil + }, + ) + log.Printf("[DEBUG] Task state (%s)", taskState) + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("metadata") { + omd, nmd := d.GetChange("metadata") + if len(omd.([]interface{})) > 0 { + for _, data := range omd.([]interface{}) { + d := data.(map[string]interface{}) + k := d["key"].(string) + err := instances.MetadataDelete(client, instanceID, k).Err + if err != nil { + return diag.Errorf("cannot delete metadata key: %s. Error: %s", k, err) + } + } + } + if len(nmd.([]interface{})) > 0 { + var MetaData []instances.MetadataOpts + for _, data := range nmd.([]interface{}) { + d := data.(map[string]interface{}) + var md instances.MetadataOpts + md.Key = d["key"].(string) + md.Value = d["value"].(string) + MetaData = append(MetaData, md) + } + createOpts := instances.MetadataSetOpts{ + Metadata: MetaData, + } + err := instances.MetadataCreate(client, instanceID, createOpts).Err + if err != nil { + return diag.Errorf("cannot create metadata. Error: %s", err) + } + } + } else if d.HasChange("metadata_map") { + omd, nmd := d.GetChange("metadata_map") + if len(omd.(map[string]interface{})) > 0 { + for k := range omd.(map[string]interface{}) { + err := instances.MetadataDelete(client, instanceID, k).Err + if err != nil { + return diag.Errorf("cannot delete metadata key: %s. Error: %s", k, err) + } + } + } + if len(nmd.(map[string]interface{})) > 0 { + var MetaData []instances.MetadataOpts + for k, v := range nmd.(map[string]interface{}) { + md := instances.MetadataOpts{ + Key: k, + Value: v.(string), + } + MetaData = append(MetaData, md) + } + createOpts := instances.MetadataSetOpts{ + Metadata: MetaData, + } + err := instances.MetadataCreate(client, instanceID, createOpts).Err + if err != nil { + return diag.Errorf("cannot create metadata. Error: %s", err) + } + } + } + + if d.HasChange("interface") { + iOldRaw, iNewRaw := d.GetChange("interface") + ifsOldSlice, ifsNewSlice := iOldRaw.([]interface{}), iNewRaw.([]interface{}) + sort.Sort(instanceInterfaces(ifsOldSlice)) + sort.Sort(instanceInterfaces(ifsNewSlice)) + + switch { + // the same number of interfaces + case len(ifsOldSlice) == len(ifsNewSlice): + for idx, item := range ifsOldSlice { + iOld := item.(map[string]interface{}) + iNew := ifsNewSlice[idx].(map[string]interface{}) + + sgsIDsOld := getSecurityGroupsIDs(iOld["security_groups"].([]interface{})) + sgsIDsNew := getSecurityGroupsIDs(iNew["security_groups"].([]interface{})) + if len(sgsIDsOld) > 0 || len(sgsIDsNew) > 0 { + portID := iOld["port_id"].(string) + sgClient, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + removeSGs := getSecurityGroupsDifference(sgsIDsNew, sgsIDsOld) + if err := removeSecurityGroupFromInstance(sgClient, client, instanceID, portID, removeSGs); err != nil { + return diag.FromErr(err) + } + addSGs := getSecurityGroupsDifference(sgsIDsOld, sgsIDsNew) + if err := attachSecurityGroupToInstance(sgClient, client, instanceID, portID, addSGs); err != nil { + return diag.FromErr(err) + } + } + + differentFields := getMapDifference(iOld, iNew, []string{"security_groups"}) + if len(differentFields) > 0 { + if err := detachInterfaceFromInstance(client, instanceID, iOld); err != nil { + return diag.FromErr(err) + } + if err := attachInterfaceToInstance(client, instanceID, iNew); err != nil { + return diag.FromErr(err) + } + } + } + + // new interfaces > old interfaces - need to attach new + case len(ifsOldSlice) < len(ifsNewSlice): + for idx, item := range ifsOldSlice { + iOld := item.(map[string]interface{}) + iNew := ifsNewSlice[idx].(map[string]interface{}) + + sgsIDsOld := getSecurityGroupsIDs(iOld["security_groups"].([]interface{})) + sgsIDsNew := getSecurityGroupsIDs(iNew["security_groups"].([]interface{})) + if len(sgsIDsOld) > 0 || len(sgsIDsNew) > 0 { + portID := iOld["port_id"].(string) + clientSG, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + removeSGs := getSecurityGroupsDifference(sgsIDsNew, sgsIDsOld) + if err := removeSecurityGroupFromInstance(clientSG, client, instanceID, portID, removeSGs); err != nil { + return diag.FromErr(err) + } + + addSGs := getSecurityGroupsDifference(sgsIDsOld, sgsIDsNew) + if err := attachSecurityGroupToInstance(clientSG, client, instanceID, portID, addSGs); err != nil { + return diag.FromErr(err) + } + } + + differentFields := getMapDifference(iOld, iNew, []string{"security_groups"}) + if len(differentFields) > 0 { + if err := detachInterfaceFromInstance(client, instanceID, iOld); err != nil { + return diag.FromErr(err) + } + if err := attachInterfaceToInstance(client, instanceID, iNew); err != nil { + return diag.FromErr(err) + } + } + } + + for _, item := range ifsNewSlice[len(ifsOldSlice):] { + iNew := item.(map[string]interface{}) + if err := attachInterfaceToInstance(client, instanceID, iNew); err != nil { + return diag.FromErr(err) + } + } + + // old interfaces > new interfaces - need to detach old + case len(ifsOldSlice) > len(ifsNewSlice): + for idx, item := range ifsOldSlice[:len(ifsNewSlice)] { + iOld := item.(map[string]interface{}) + iNew := ifsNewSlice[idx].(map[string]interface{}) + + sgsIDsOld := getSecurityGroupsIDs(iOld["security_groups"].([]interface{})) + sgsIDsNew := getSecurityGroupsIDs(iNew["security_groups"].([]interface{})) + if len(sgsIDsOld) > 0 || len(sgsIDsNew) > 0 { + portID := iOld["port_id"].(string) + clientSG, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + removeSGs := getSecurityGroupsDifference(sgsIDsNew, sgsIDsOld) + if err := removeSecurityGroupFromInstance(clientSG, client, instanceID, portID, removeSGs); err != nil { + return diag.FromErr(err) + } + + addSGs := getSecurityGroupsDifference(sgsIDsOld, sgsIDsNew) + if err := attachSecurityGroupToInstance(clientSG, client, instanceID, portID, addSGs); err != nil { + return diag.FromErr(err) + } + } + + differentFields := getMapDifference(iOld, iNew, []string{"security_groups"}) + if len(differentFields) > 0 { + if err := detachInterfaceFromInstance(client, instanceID, iOld); err != nil { + return diag.FromErr(err) + } + if err := attachInterfaceToInstance(client, instanceID, iNew); err != nil { + return diag.FromErr(err) + } + } + } + + for _, item := range ifsOldSlice[len(ifsNewSlice):] { + iOld := item.(map[string]interface{}) + if err := detachInterfaceFromInstance(client, instanceID, iOld); err != nil { + return diag.FromErr(err) + } + } + } + } + + if d.HasChange("server_group") { + oldSGRaw, newSGRaw := d.GetChange("server_group") + oldSGID, newSGID := oldSGRaw.(string), newSGRaw.(string) + + clientSG, err := CreateClient(provider, d, ServerGroupsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + // delete old server group + if oldSGID != "" { + err := deleteServerGroup(clientSG, client, instanceID, oldSGID) + if err != nil { + return diag.FromErr(err) + } + } + + // add new server group if needed + if newSGID != "" { + err := addServerGroup(clientSG, client, instanceID, newSGID) + if err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("volume") { + vClient, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + oldVolumesRaw, newVolumesRaw := d.GetChange("volume") + oldVolumes := extractInstanceVolumesMap(oldVolumesRaw.(*schema.Set).List()) + newVolumes := extractInstanceVolumesMap(newVolumesRaw.(*schema.Set).List()) + + vOpts := volumes.InstanceOperationOpts{InstanceID: d.Id()} + for vid := range oldVolumes { + if isAttached := newVolumes[vid]; isAttached { + // mark as already attached + newVolumes[vid] = false + continue + } + if _, err := volumes.Detach(vClient, vid, vOpts).Extract(); err != nil { + return diag.FromErr(err) + } + } + + // range over not attached volumes + for vid, ok := range newVolumes { + if ok { + if _, err := volumes.Attach(vClient, vid, vOpts).Extract(); err != nil { + return diag.FromErr(err) + } + } + } + } + + if d.HasChange("vm_state") { + state := d.Get("vm_state").(string) + switch state { + case InstanceVMStateActive: + if _, err := instances.Start(client, instanceID).Extract(); err != nil { + return diag.FromErr(err) + } + startStateConf := &retry.StateChangeConf{ + Target: []string{InstanceVMStateActive}, + Refresh: ServerV2StateRefreshFunc(client, instanceID), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + _, err = startStateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for instance (%s) to become active: %s", d.Id(), err) + } + case InstanceVMStateStopped: + if _, err := instances.Stop(client, instanceID).Extract(); err != nil { + return diag.FromErr(err) + } + stopStateConf := &retry.StateChangeConf{ + Target: []string{InstanceVMStateStopped}, + Refresh: ServerV2StateRefreshFunc(client, instanceID), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + _, err = stopStateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for instance (%s) to become inactive(stopped): %s", d.Id(), err) + } + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish Instance updating") + + return resourceInstanceRead(ctx, d, m) +} + +func resourceInstanceDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Instance deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + instanceID := d.Id() + log.Printf("[DEBUG] Instance id = %s", instanceID) + + client, err := CreateClient(provider, d, InstancePoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var delOpts instances.DeleteOpts + results, err := instances.Delete(client, instanceID, delOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, InstanceDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := instances.Get(client, instanceID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete instance with ID: %s", instanceID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Instance resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of Instance deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_k8s.go b/edgecenter/resource_edgecenter_k8s.go new file mode 100644 index 00000000..84705c1f --- /dev/null +++ b/edgecenter/resource_edgecenter_k8s.go @@ -0,0 +1,573 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/pools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/keypair/v2/keypairs" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" +) + +const ( + K8sPoint = "k8s/clusters" + K8sCreateTimeout = 3600 +) + +var k8sCreateTimeout = time.Second * time.Duration(K8sCreateTimeout) + +func resourceK8s() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceK8sCreate, + ReadContext: resourceK8sRead, + UpdateContext: resourceK8sUpdate, + DeleteContext: resourceK8sDelete, + Description: "Represent k8s cluster with one default pool.", + Timeouts: &schema.ResourceTimeout{ + Create: &k8sCreateTimeout, + Update: &k8sCreateTimeout, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, k8sID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(k8sID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the Kubernetes cluster.", + }, + "fixed_network": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Fixed network (uuid) associated with the Kubernetes cluster.", + }, + "fixed_subnet": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Subnet (uuid) associated with the fixed network. Ensure there's a router on this subnet.", + }, + "auto_healing_enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Indicates whether auto-healing is enabled for the Kubernetes cluster. true by default.", + }, + "master_lb_floating_ip_enabled": { + Type: schema.TypeBool, + Optional: true, + Description: "Flag indicating if the master LoadBalancer should have a floating IP.", + }, + "pods_ip_pool": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "IP pool to be used for pods within the Kubernetes cluster.", + }, + "services_ip_pool": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "IP pool to be used for services within the Kubernetes cluster.", + }, + "keypair": { + Type: schema.TypeString, + Required: true, + Description: "The name of the keypair", + }, + "pool": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + MinItems: 1, + Description: "Configuration details of the node pool in the Kubernetes cluster.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "flavor_id": { + Type: schema.TypeString, + Required: true, + }, + "min_node_count": { + Type: schema.TypeInt, + Required: true, + }, + "max_node_count": { + Type: schema.TypeInt, + Required: true, + }, + "node_count": { + Type: schema.TypeInt, + Required: true, + }, + "docker_volume_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Available value is 'standard', 'ssd_hiiops', 'cold', 'ultra'.", + }, + "docker_volume_size": { + Type: schema.TypeInt, + Optional: true, + }, + "uuid": { + Type: schema.TypeString, + Computed: true, + }, + "stack_id": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "node_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Total number of nodes in the Kubernetes cluster.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the Kubernetes cluster.", + }, + "status_reason": { + Type: schema.TypeString, + Computed: true, + Description: "The reason for the current status of the Kubernetes cluster, if ERROR.", + }, + "master_addresses": { + Type: schema.TypeList, + Computed: true, + Description: "List of IP addresses for master nodes in the Kubernetes cluster.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "node_addresses": { + Type: schema.TypeList, + Computed: true, + Description: "List of IP addresses for worker nodes in the Kubernetes cluster.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "container_version": { + Type: schema.TypeString, + Computed: true, + Description: "The container runtime version used in the Kubernetes cluster.", + }, + "api_address": { + Type: schema.TypeString, + Computed: true, + Description: "API endpoint address for the Kubernetes cluster.", + }, + "user_id": { + Type: schema.TypeString, + Computed: true, + Description: "User identifier associated with the Kubernetes cluster.", + }, + "discovery_url": { + Type: schema.TypeString, + Computed: true, + Description: "URL used for node discovery within the Kubernetes cluster.", + }, + "health_status": { + Type: schema.TypeString, + Computed: true, + Description: "Overall health status of the Kubernetes cluster.", + }, + "health_status_reason": { + Type: schema.TypeMap, + Computed: true, + }, + "faults": { + Type: schema.TypeMap, + Computed: true, + }, + "master_flavor_id": { + Type: schema.TypeString, + Computed: true, + Description: "Identifier for the master node flavor in the Kubernetes cluster.", + }, + "cluster_template_id": { + Type: schema.TypeString, + Computed: true, + Description: "Template identifier from which the Kubernetes cluster was instantiated.", + }, + "version": { + Type: schema.TypeString, + Required: true, + Description: "The version of the Kubernetes cluster.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the Kubernetes cluster was updated.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the Kubernetes cluster was created.", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceK8sCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := clusters.CreateOpts{ + Name: d.Get("name").(string), + Version: d.Get("version").(string), + FixedNetwork: d.Get("fixed_network").(string), + FixedSubnet: d.Get("fixed_subnet").(string), + KeyPair: d.Get("keypair").(string), + AutoHealingEnabled: d.Get("auto_healing_enabled").(bool), + MasterLBFloatingIPEnabled: d.Get("master_lb_floating_ip_enabled").(bool), + } + + if podsIP, ok := d.GetOk("pods_ip_pool"); ok { + eccidr, err := parseCIDRFromString(podsIP.(string)) + if err != nil { + return diag.FromErr(err) + } + opts.PodsIPPool = &eccidr + } + + if svcIP, ok := d.GetOk("services_ip_pool"); ok { + eccidr, err := parseCIDRFromString(svcIP.(string)) + if err != nil { + return diag.FromErr(err) + } + opts.ServicesIPPool = &eccidr + } + + poolRaw := d.Get("pool").([]interface{}) + pool := poolRaw[0].(map[string]interface{}) + + poolNodeCount := pool["node_count"].(int) + maxNodeCount := pool["max_node_count"].(int) + optPool := pools.CreateOpts{ + Name: pool["name"].(string), + FlavorID: pool["flavor_id"].(string), + NodeCount: &poolNodeCount, + MinNodeCount: pool["min_node_count"].(int), + MaxNodeCount: &maxNodeCount, + } + + dockerVolumeSize := pool["docker_volume_size"].(int) + if dockerVolumeSize != 0 { + optPool.DockerVolumeSize = &dockerVolumeSize + } + + dockerVolumeType := pool["docker_volume_type"].(string) + if dockerVolumeType != "" { + optPool.DockerVolumeType = volumes.VolumeType(dockerVolumeType) + } + + opts.Pools = []pools.CreateOpts{optPool} + results, err := clusters.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + k8sID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + k8sID, err := clusters.ExtractClusterIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve k8s ID from task info: %w", err) + } + return k8sID, nil + }, + ) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(k8sID.(string)) + resourceK8sRead(ctx, d, m) + + log.Printf("[DEBUG] Finish K8s creating (%s)", k8sID) + + return diags +} + +func resourceK8sRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + clientK8S, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clusterID := d.Id() + cluster, err := clusters.Get(clientK8S, clusterID).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("name", cluster.Name) + d.Set("fixed_network", cluster.FixedNetwork) + d.Set("fixed_subnet", cluster.FixedSubnet) + d.Set("master_lb_floating_ip_enabled", cluster.FloatingIPEnabled) + d.Set("node_count", cluster.NodeCount) + d.Set("status", cluster.Status) + d.Set("status_reason", cluster.StatusReason) + + clientKeypairs, err := CreateClient(provider, d, KeypairsPoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + keypairInfo, err := keypairs.Get(clientKeypairs, cluster.KeyPair).Extract() + if err != nil { + return diag.FromErr(err) + } + d.Set("keypair", keypairInfo.Name) + + masterAddresses := make([]string, len(cluster.MasterAddresses)) + for i, addr := range cluster.MasterAddresses { + masterAddresses[i] = addr.String() + } + if err := d.Set("master_addresses", masterAddresses); err != nil { + return diag.FromErr(err) + } + + nodeAddresses := make([]string, len(cluster.NodeAddresses)) + for i, addr := range cluster.NodeAddresses { + nodeAddresses[i] = addr.String() + } + if err := d.Set("node_addresses", nodeAddresses); err != nil { + return diag.FromErr(err) + } + + d.Set("container_version", cluster.ContainerVersion) + d.Set("api_address", cluster.APIAddress.String()) + d.Set("user_id", cluster.UserID) + d.Set("discovery_url", cluster.DiscoveryURL.String()) + + d.Set("health_status", cluster.HealthStatus) + if err := d.Set("health_status_reason", cluster.HealthStatusReason); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("faults", cluster.Faults); err != nil { + return diag.FromErr(err) + } + + d.Set("master_flavor_id", cluster.MasterFlavorID) + d.Set("cluster_template_id", cluster.ClusterTemplateID) + d.Set("version", cluster.Version) + d.Set("updated_at", cluster.UpdatedAt.Format(time.RFC850)) + d.Set("created_at", cluster.CreatedAt.Format(time.RFC850)) + + var pool pools.ClusterPool + for _, p := range cluster.Pools { + if p.IsDefault { + pool = p + } + } + + p := make(map[string]interface{}) + p["uuid"] = pool.UUID + p["name"] = pool.Name + p["flavor_id"] = pool.FlavorID + p["min_node_count"] = pool.MinNodeCount + p["max_node_count"] = pool.MaxNodeCount + p["node_count"] = pool.NodeCount + p["docker_volume_type"] = pool.DockerVolumeType.String() + p["docker_volume_size"] = pool.DockerVolumeSize + p["stack_id"] = pool.StackID + p["created_at"] = pool.CreatedAt.Format(time.RFC850) + + if err := d.Set("pool", []interface{}{p}); err != nil { + return diag.FromErr(err) + } + + fields := []string{"region_id", "auto_healing_enabled", "pods_ip_pool", "services_ip_pool"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish K8s reading") + + return diags +} + +func resourceK8sUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("pool") { + poolRaw := d.Get("pool").([]interface{})[0] + pool := poolRaw.(map[string]interface{}) + + clusterID := d.Id() + poolID := pool["uuid"].(string) + + if d.HasChanges("pool.0.name", "pool.0.min_node_count", "pool.0.max_node_count") { + updateOpts := pools.UpdateOpts{ + Name: pool["name"].(string), + MinNodeCount: pool["min_node_count"].(int), + MaxNodeCount: pool["max_node_count"].(int), + } + results, err := pools.Update(client, clusterID, poolID, updateOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := pools.Get(client, clusterID, poolID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get pool with ID: %s. Error: %w", poolID, err) + } + return nil, nil + }) + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("pool.0.node_count") { + resizeOpts := clusters.ResizeOpts{ + NodeCount: pool["node_count"].(*int), + } + results, err := clusters.Resize(client, clusterID, poolID, resizeOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := pools.Get(client, clusterID, poolID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get pool with ID: %s. Error: %w", poolID, err) + } + return nil, nil + }) + if err != nil { + return diag.FromErr(err) + } + } + } + + return resourceK8sRead(ctx, d, m) +} + +func resourceK8sDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + results, err := clusters.Delete(client, id).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := clusters.Get(client, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete k8s cluster with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Cluster resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of K8s deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_k8s_pool.go b/edgecenter/resource_edgecenter_k8s_pool.go new file mode 100644 index 00000000..ed7c0654 --- /dev/null +++ b/edgecenter/resource_edgecenter_k8s_pool.go @@ -0,0 +1,330 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/pools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" +) + +func resourceK8sPool() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceK8sPoolCreate, + ReadContext: resourceK8sPoolRead, + UpdateContext: resourceK8sPoolUpdate, + DeleteContext: resourceK8sPoolDelete, + Description: "Represent k8s cluster's pool.", + Timeouts: &schema.ResourceTimeout{ + Create: &k8sCreateTimeout, + Update: &k8sCreateTimeout, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, poolID, clusterID, err := ImportStringParserExtended(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.Set("cluster_id", clusterID) + d.SetId(poolID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "cluster_id": { + Type: schema.TypeString, + Required: true, + Description: "The uuid of the Kubernetes cluster this pool belongs to.", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the Kubernetes pool.", + }, + "flavor_id": { + Type: schema.TypeString, + Required: true, + Description: "The identifier of the flavor used for nodes in this pool, e.g. g1-standard-2-4.", + }, + "min_node_count": { + Type: schema.TypeInt, + Required: true, + Description: "The minimum number of nodes in the pool.", + }, + "max_node_count": { + Type: schema.TypeInt, + Required: true, + Description: "The maximum number of nodes the pool can scale to.", + }, + "node_count": { + Type: schema.TypeInt, + Required: true, + Description: "The current number of nodes in the pool.", + }, + "docker_volume_type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The type of volume used for the Docker containers. Available values are 'standard', 'ssd_hiiops', 'cold', and 'ultra'.", + }, + "docker_volume_size": { + Type: schema.TypeInt, + Optional: true, + Description: "The size of the volume used for Docker containers, in gigabytes.", + }, + "stack_id": { + Type: schema.TypeString, + Computed: true, + Description: "The identifier of the underlying infrastructure stack used by this pool.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The timestamp when the Kubernetes pool was created.", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceK8sPoolCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s pool creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + poolNodeCount := d.Get("node_count").(int) + maxNodeCount := d.Get("max_node_count").(int) + opts := pools.CreateOpts{ + Name: d.Get("name").(string), + FlavorID: d.Get("flavor_id").(string), + NodeCount: &poolNodeCount, + MinNodeCount: d.Get("min_node_count").(int), + MaxNodeCount: &maxNodeCount, + } + + dockerVolumeSize := d.Get("docker_volume_size").(int) + if dockerVolumeSize != 0 { + opts.DockerVolumeSize = &dockerVolumeSize + } + + dockerVolumeType := d.Get("docker_volume_type").(string) + if dockerVolumeType != "" { + opts.DockerVolumeType = volumes.VolumeType(dockerVolumeType) + } + + clusterID := d.Get("cluster_id").(string) + results, err := pools.Create(client, clusterID, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + poolID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + poolID, err := pools.ExtractClusterPoolIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve k8s pool ID from task info: %w", err) + } + return poolID, nil + }, + ) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(poolID.(string)) + resourceK8sPoolRead(ctx, d, m) + + log.Printf("[DEBUG] Finish K8s pool creating (%s)", poolID) + + return diags +} + +func resourceK8sPoolRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s pool reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clusterID := d.Get("cluster_id").(string) + poolID := d.Id() + + pool, err := pools.Get(client, clusterID, poolID).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("name", pool.Name) + d.Set("cluster_id", pool.ClusterID) + d.Set("flavor_id", pool.FlavorID) + d.Set("min_node_count", pool.MinNodeCount) + d.Set("max_node_count", pool.MaxNodeCount) + d.Set("node_count", pool.NodeCount) + d.Set("docker_volume_type", pool.DockerVolumeType.String()) + d.Set("docker_volume_size", pool.DockerVolumeSize) + d.Set("stack_id", pool.StackID) + d.Set("created_at", pool.CreatedAt.Format(time.RFC850)) + + log.Println("[DEBUG] Finish K8s pool reading") + + return diags +} + +func resourceK8sPoolUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + poolID := d.Id() + clusterID := d.Get("cluster_id").(string) + + if d.HasChanges("name", "min_node_count", "max_node_count") { + updateOpts := pools.UpdateOpts{ + Name: d.Get("name").(string), + MinNodeCount: d.Get("min_node_count").(int), + MaxNodeCount: d.Get("max_node_count").(int), + } + results, err := pools.Update(client, clusterID, poolID, updateOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := pools.Get(client, clusterID, poolID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get pool with ID: %s. Error: %w", poolID, err) + } + return nil, nil + }) + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("node_count") { + resizeOpts := clusters.ResizeOpts{ + NodeCount: d.Get("node_count").(*int), + } + results, err := clusters.Resize(client, clusterID, poolID, resizeOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := pools.Get(client, clusterID, poolID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get pool with ID: %s. Error: %w", poolID, err) + } + return nil, nil + }) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceK8sPoolRead(ctx, d, m) +} + +func resourceK8sPoolDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start K8s deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, K8sPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + clusterID := d.Get("cluster_id").(string) + results, err := pools.Delete(client, clusterID, id).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := pools.Get(client, clusterID, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete k8s cluster pool with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Pool resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of K8s pool deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_keypair.go b/edgecenter/resource_edgecenter_keypair.go new file mode 100644 index 00000000..946cc122 --- /dev/null +++ b/edgecenter/resource_edgecenter_keypair.go @@ -0,0 +1,145 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/keypair/v2/keypairs" +) + +const KeypairsPoint = "keypairs" + +func resourceKeypair() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeypairCreate, + ReadContext: resourceKeypairRead, + DeleteContext: resourceKeypairDelete, + Description: "Represent a ssh key, do not depends on region", + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "public_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The public portion of the SSH key pair.", + }, + "sshkey_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name assigned to the SSH key pair, used for identification purposes.", + }, + "sshkey_id": { + Type: schema.TypeString, + Computed: true, + Description: "The unique identifier assigned by the provider to the SSH key pair.", + }, + "fingerprint": { + Type: schema.TypeString, + Computed: true, + Description: "A fingerprint of the SSH public key, used to verify the integrity of the key.", + }, + }, + } +} + +func resourceKeypairCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start KeyPair creating") + + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, KeypairsPoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + opts := keypairs.CreateOpts{ + Name: d.Get("sshkey_name").(string), + PublicKey: d.Get("public_key").(string), + ProjectID: d.Get("project_id").(int), + } + + kp, err := keypairs.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[DEBUG] KeyPair id (%s)", kp.ID) + d.SetId(kp.ID) + + resourceKeypairRead(ctx, d, m) + + log.Printf("[DEBUG] Finish KeyPair creating (%s)", kp.ID) + + return diags +} + +func resourceKeypairRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start KeyPair reading") + + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, KeypairsPoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + kpID := d.Id() + kp, err := keypairs.Get(client, kpID).Extract() + if err != nil { + return diag.Errorf("cannot get keypairs with ID %s. Error: %s", kpID, err.Error()) + } + + d.Set("sshkey_name", kp.Name) + d.Set("public_key", kp.PublicKey) + d.Set("sshkey_id", kp.ID) + d.Set("fingerprint", kp.Fingerprint) + d.Set("project_id", kp.ProjectID) + + log.Println("[DEBUG] Finish KeyPair reading") + + return diags +} + +func resourceKeypairDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start KeyPair deleting") + + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, KeypairsPoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + kpID := d.Id() + if err := keypairs.Delete(client, kpID).ExtractErr(); err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Println("[DEBUG] Finish of KeyPair deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_lblistener.go b/edgecenter/resource_edgecenter_lblistener.go new file mode 100644 index 00000000..a57d0a7a --- /dev/null +++ b/edgecenter/resource_edgecenter_lblistener.go @@ -0,0 +1,383 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + LBListenersPoint = "lblisteners" + LBListenerCreateTimeout = 2400 +) + +func resourceLbListener() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceLBListenerCreate, + ReadContext: resourceLBListenerRead, + UpdateContext: resourceLBListenerUpdate, + DeleteContext: resourceLBListenerDelete, + Description: "Represent a load balancer listener. Can not be created without a load balancer. A listener is a process that checks for connection requests using the protocol and port that you configure.", + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, listenerID, lbID, err := ImportStringParserExtended(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.Set("loadbalancer_id", lbID) + d.SetId(listenerID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer listener.", + }, + "loadbalancer_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The uuid for the load balancer.", + }, + "protocol": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Available values are 'TCP', 'UDP', 'HTTP', 'HTTPS' and 'Terminated HTTPS'.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch types.ProtocolType(v) { + case types.ProtocolTypeTCP, types.ProtocolTypeUDP, types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTerminatedHTTPS: + return diag.Diagnostics{} + case types.ProtocolTypePROXY: + } + return diag.Errorf("wrong protocol %s, available values are 'TCP', 'UDP', 'HTTP', 'HTTPS' and 'Terminated HTTPS'.", v) + }, + }, + "protocol_port": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "The port on which the protocol is bound.", + }, + "insert_x_forwarded": { + Type: schema.TypeBool, + Optional: true, + Description: "Insert *-forwarded headers", + ForceNew: true, + }, + "pool_count": { + Type: schema.TypeInt, + Computed: true, + Description: "Number of pools associated with the load balancer.", + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "The current operational status of the load balancer.", + }, + "provisioning_status": { + Type: schema.TypeString, + Computed: true, + Description: "The current provisioning status of the load balancer.", + }, + "secret_id": { + Type: schema.TypeString, + Optional: true, + Description: "The identifier for the associated secret, typically used for SSL configurations.", + }, + "sni_secret_id": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "List of secret identifiers used for Server Name Indication (SNI).", + }, + "allowed_cidrs": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "The allowed CIDRs for listener.", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceLBListenerCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBListener creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := listeners.CreateOpts{ + Name: d.Get("name").(string), + Protocol: types.ProtocolType(d.Get("protocol").(string)), + ProtocolPort: d.Get("protocol_port").(int), + LoadBalancerID: d.Get("loadbalancer_id").(string), + InsertXForwarded: d.Get("insert_x_forwarded").(bool), + } + secretID := d.Get("secret_id").(string) + sniSecretIDRaw := d.Get("sni_secret_id").([]interface{}) + + switch opts.Protocol { //nolint: exhaustive + case types.ProtocolTypeTCP, types.ProtocolTypeUDP, types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS: + if secretID != "" { + return diag.Errorf("secret_id parameter can only be used with %s listener protocol type", types.ProtocolTypeTerminatedHTTPS) + } + + if len(sniSecretIDRaw) > 0 { + return diag.Errorf("sni_secret_id parameter can only be used with %s listener protocol type", types.ProtocolTypeTerminatedHTTPS) + } + + if opts.InsertXForwarded && (opts.Protocol == types.ProtocolTypeTCP || opts.Protocol == types.ProtocolTypeUDP || opts.Protocol == types.ProtocolTypeHTTPS) { + return diag.Errorf( + "X-Forwarded headers can only be used with %s or %s listener protocol type", + types.ProtocolTypeHTTP, types.ProtocolTypeTerminatedHTTPS, + ) + } + case types.ProtocolTypeTerminatedHTTPS: + if secretID == "" { + return diag.Errorf("secret_id parameter is required with %s listener protocol type", types.ProtocolTypeTerminatedHTTPS) + } + opts.SecretID = secretID + if len(sniSecretIDRaw) > 0 { + opts.SNISecretID = make([]string, len(sniSecretIDRaw)) + for i, s := range sniSecretIDRaw { + opts.SNISecretID[i] = s.(string) + } + } + default: + return diag.Errorf("wrong protocol") + } + + allowedCIRDsRaw := d.Get("allowed_cidrs").([]interface{}) + if len(allowedCIRDsRaw) > 0 { + opts.AllowedCIDRs = make([]string, len(allowedCIRDsRaw)) + for i, s := range allowedCIRDsRaw { + opts.AllowedCIDRs[i] = s.(string) + } + } + + results, err := listeners.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + listenerID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, LBListenerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + listenerID, err := listeners.ExtractListenerIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LBListener ID from task info: %w", err) + } + return listenerID, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(listenerID.(string)) + resourceLBListenerRead(ctx, d, m) + + log.Printf("[DEBUG] Finish LBListener creating (%s)", listenerID) + + return diags +} + +func resourceLBListenerRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBListener reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + lb, err := listeners.Get(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + d.Set("name", lb.Name) + d.Set("protocol", lb.Protocol.String()) + d.Set("protocol_port", lb.ProtocolPort) + d.Set("pool_count", lb.PoolCount) + d.Set("operating_status", lb.OperationStatus.String()) + d.Set("provisioning_status", lb.ProvisioningStatus.String()) + d.Set("secret_id", lb.SecretID) + d.Set("sni_secret_id", lb.SNISecretID) + d.Set("allowed_cidrs", lb.AllowedCIDRs) + + fields := []string{"project_id", "region_id", "loadbalancer_id", "insert_x_forwarded"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish LBListener reading") + + return diags +} + +func resourceLBListenerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBListener updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBListenersPoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + var changed bool + opts := listeners.UpdateOpts{ + Name: d.Get("name").(string), + } + + if d.HasChange("name") { + changed = true + } + + if d.HasChange("secret_id") { + if types.ProtocolType(d.Get("protocol").(string)) != types.ProtocolTypeTerminatedHTTPS { + return diag.Errorf("secret_id parameter can only be used with %s listener protocol type", types.ProtocolTypeTerminatedHTTPS) + } + opts.SecretID = d.Get("secret_id").(string) + changed = true + } + + if d.HasChange("sni_secret_id") { + if types.ProtocolType(d.Get("protocol").(string)) != types.ProtocolTypeTerminatedHTTPS { + return diag.Errorf("sni_secret_id parameter can only be used with %s listener protocol type", types.ProtocolTypeTerminatedHTTPS) + } + sniSecretIDRaw := d.Get("sni_secret_id").([]interface{}) + sniSecretID := make([]string, len(sniSecretIDRaw)) + for i, s := range sniSecretIDRaw { + sniSecretID[i] = s.(string) + } + opts.SNISecretID = sniSecretID + changed = true + } + + if d.HasChange("allowed_cidrs") { + allowedCIDRsRaw := d.Get("allowed_cidrs").([]interface{}) + allowedCIDRs := make([]string, len(allowedCIDRsRaw)) + for i, s := range allowedCIDRsRaw { + allowedCIDRs[i] = s.(string) + } + opts.AllowedCIDRs = allowedCIDRs + changed = true + } + + if changed { + _, err = listeners.Update(client, d.Id(), opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + } + + log.Println("[DEBUG] Finish LBListener updating") + + return resourceLBListenerRead(ctx, d, m) +} + +func resourceLBListenerDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBListener deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + results, err := listeners.Delete(client, id).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBListenerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := listeners.Get(client, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete LBListener with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Listener resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of LBListener deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_lbmember.go b/edgecenter/resource_edgecenter_lbmember.go new file mode 100644 index 00000000..133da787 --- /dev/null +++ b/edgecenter/resource_edgecenter_lbmember.go @@ -0,0 +1,341 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "net" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/lbpools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + minWeight = 0 + maxWeight = 256 +) + +func resourceLBMember() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceLBMemberCreate, + ReadContext: resourceLBMemberRead, + UpdateContext: resourceLBMemberUpdate, + DeleteContext: resourceLBMemberDelete, + Description: "Represent load balancer member", + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, memberID, lbPoolID, err := ImportStringParserExtended(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.Set("pool_id", lbPoolID) + d.SetId(memberID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "pool_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The uuid for the load balancer pool.", + }, + "address": { + Type: schema.TypeString, + Required: true, + Description: "The IP address of the load balancer pool member.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + ip := net.ParseIP(v) + if ip != nil { + return diag.Diagnostics{} + } + + return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) + }, + }, + "protocol_port": { + Type: schema.TypeInt, + Required: true, + Description: "The port on which the member listens for requests.", + }, + "weight": { + Type: schema.TypeInt, + Optional: true, + Description: "A weight value between 0 and 256, determining the distribution of requests among the members of the pool.", + ValidateDiagFunc: func(val interface{}, path cty.Path) diag.Diagnostics { + v := val.(int) + if v >= minWeight && v <= maxWeight { + return nil + } + return diag.Errorf("Valid values: %d to %d got: %d", minWeight, maxWeight, v) + }, + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The uuid of the subnet in which the pool member is located.", + }, + "instance_id": { + Type: schema.TypeString, + Optional: true, + Description: "The uuid of the instance (amphora) associated with the pool member.", + }, + "operating_status": { + Type: schema.TypeString, + Computed: true, + Description: "The current operating status of the pool member.", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceLBMemberCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBMember creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := lbpools.CreatePoolMemberOpts{ + Address: net.ParseIP(d.Get("address").(string)), + ProtocolPort: d.Get("protocol_port").(int), + Weight: d.Get("weight").(int), + SubnetID: d.Get("subnet_id").(string), + InstanceID: d.Get("instance_id").(string), + } + + results, err := lbpools.CreateMember(client, d.Get("pool_id").(string), opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + pmID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + pmID, err := lbpools.ExtractPoolMemberIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LBMember ID from task info: %w", err) + } + return pmID, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(pmID.(string)) + resourceLBMemberRead(ctx, d, m) + + log.Printf("[DEBUG] Finish LBMember creating (%s)", pmID) + + return diags +} + +func resourceLBMemberRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBMember reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + pool, err := lbpools.Get(client, d.Get("pool_id").(string)).Extract() + if err != nil { + return diag.FromErr(err) + } + + mid := d.Id() + for _, pm := range pool.Members { + if mid == pm.ID { + d.Set("address", pm.Address.String()) + d.Set("protocol_port", pm.ProtocolPort) + d.Set("weight", pm.Weight) + d.Set("subnet_id", pm.SubnetID) + d.Set("instance_id", pm.InstanceID) + d.Set("operating_status", pm.OperatingStatus) + } + } + + fields := []string{"project_id", "region_id"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish LBMember reading)") + + return diags +} + +func resourceLBMemberUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBMember updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + pool, err := lbpools.Get(client, d.Get("pool_id").(string)).Extract() + if err != nil { + return diag.FromErr(err) + } + + members := make([]lbpools.CreatePoolMemberOpts, len(pool.Members)) + for i, pm := range pool.Members { + if pm.ID != d.Id() { + members[i] = lbpools.CreatePoolMemberOpts{ + Address: *pm.Address, + ProtocolPort: pm.ProtocolPort, + Weight: pm.Weight, + SubnetID: pm.SubnetID, + InstanceID: pm.InstanceID, + ID: pm.ID, + } + continue + } + + members[i] = lbpools.CreatePoolMemberOpts{ + Address: net.ParseIP(d.Get("address").(string)), + ProtocolPort: d.Get("protocol_port").(int), + Weight: d.Get("weight").(int), + SubnetID: d.Get("subnet_id").(string), + InstanceID: d.Get("instance_id").(string), + ID: d.Id(), + } + } + + opts := lbpools.UpdateOpts{Name: pool.Name, Members: members} + results, err := lbpools.Update(client, pool.ID, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + lbPoolID, err := lbpools.ExtractPoolMemberIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LBPool ID from task info: %w, %+v, %+v", err, taskInfo, task) + } + return lbPoolID, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish LBMember updating") + + return resourceLBMemberRead(ctx, d, m) +} + +func resourceLBMemberDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBMember deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + mid := d.Id() + pid := d.Get("pool_id").(string) + results, err := lbpools.DeleteMember(client, pid, mid).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + d.SetId("") + log.Printf("[DEBUG] Finish of LBMember deleting") + return diags + } + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + pool, err := lbpools.Get(client, pid).Extract() + if err != nil { + return nil, fmt.Errorf("extracting LBPool resource error: %w", err) + } + + for _, pm := range pool.Members { + if pm.ID == mid { + return nil, fmt.Errorf("pool member %s still exist", mid) + } + } + + return nil, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of LBMember deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_lbpool.go b/edgecenter/resource_edgecenter_lbpool.go new file mode 100644 index 00000000..2149076f --- /dev/null +++ b/edgecenter/resource_edgecenter_lbpool.go @@ -0,0 +1,441 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/lbpools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + LBPoolsPoint = "lbpools" + LBPoolsCreateTimeout = 2400 +) + +func resourceLBPool() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceLBPoolCreate, + ReadContext: resourceLBPoolRead, + UpdateContext: resourceLBPoolUpdate, + DeleteContext: resourceLBPoolDelete, + Description: "Represent load balancer listener pool. A pool is a list of virtual machines to which the listener will redirect incoming traffic", + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, lbPoolID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(lbPoolID) + + return []*schema.ResourceData{d}, nil + }, + }, + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer listener pool.", + }, + "lb_algorithm": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available values is '%s', '%s', '%s', '%s'", types.LoadBalancerAlgorithmRoundRobin, types.LoadBalancerAlgorithmLeastConnections, types.LoadBalancerAlgorithmSourceIP, types.LoadBalancerAlgorithmSourceIPPort), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch types.LoadBalancerAlgorithm(v) { + case types.LoadBalancerAlgorithmRoundRobin, types.LoadBalancerAlgorithmLeastConnections, types.LoadBalancerAlgorithmSourceIP, types.LoadBalancerAlgorithmSourceIPPort: + return diag.Diagnostics{} + } + return diag.Errorf("wrong type %s, available values is '%s', '%s', '%s', '%s'", v, types.LoadBalancerAlgorithmRoundRobin, types.LoadBalancerAlgorithmLeastConnections, types.LoadBalancerAlgorithmSourceIP, types.LoadBalancerAlgorithmSourceIPPort) + }, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available values is '%s' (currently work, other do not work on ed-8), '%s', '%s', '%s'", types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch types.ProtocolType(v) { + case types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP: + return diag.Diagnostics{} + case types.ProtocolTypeTerminatedHTTPS, types.ProtocolTypePROXY: + } + return diag.Errorf("wrong type %s, available values is '%s', '%s', '%s', '%s'", v, types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP) + }, + }, + "loadbalancer_id": { + Type: schema.TypeString, + Optional: true, + Description: "The uuid for the load balancer.", + }, + "listener_id": { + Type: schema.TypeString, + Optional: true, + Description: "The uuid for the load balancer listener.", + }, + "health_monitor": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: `Configuration for health checks to test the health and state of the backend members. +It determines how the load balancer identifies whether the backend members are healthy or unhealthy.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available values is '%s', '%s', '%s', '%s', '%s', '%s", types.HealthMonitorTypeHTTP, types.HealthMonitorTypeHTTPS, types.HealthMonitorTypePING, types.HealthMonitorTypeTCP, types.HealthMonitorTypeTLSHello, types.HealthMonitorTypeUDPConnect), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch types.HealthMonitorType(v) { + case types.HealthMonitorTypeHTTP, types.HealthMonitorTypeHTTPS, types.HealthMonitorTypePING, types.HealthMonitorTypeTCP, types.HealthMonitorTypeTLSHello, types.HealthMonitorTypeUDPConnect: + return diag.Diagnostics{} + } + return diag.Errorf("wrong type %s, available values is '%s', '%s', '%s', '%s', '%s', '%s", v, types.HealthMonitorTypeHTTP, types.HealthMonitorTypeHTTPS, types.HealthMonitorTypePING, types.HealthMonitorTypeTCP, types.HealthMonitorTypeTLSHello, types.HealthMonitorTypeUDPConnect) + }, + }, + "delay": { + Type: schema.TypeInt, + Required: true, + }, + "max_retries": { + Type: schema.TypeInt, + Required: true, + }, + "timeout": { + Type: schema.TypeInt, + Required: true, + }, + "max_retries_down": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "http_method": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "url_path": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "expected_codes": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + }, + "session_persistence": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: `Configuration that enables the load balancer to bind a user's session to a specific backend member. +This ensures that all requests from the user during the session are sent to the same member.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Required: true, + }, + "cookie_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "persistence_granularity": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "persistence_timeout": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceLBPoolCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBPool creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + healthOpts := extractHealthMonitorMap(d) + sessionOpts := extractSessionPersistenceMap(d) + opts := lbpools.CreateOpts{ + Name: d.Get("name").(string), + Protocol: types.ProtocolType(d.Get("protocol").(string)), + LBPoolAlgorithm: types.LoadBalancerAlgorithm(d.Get("lb_algorithm").(string)), + LoadBalancerID: d.Get("loadbalancer_id").(string), + ListenerID: d.Get("listener_id").(string), + HealthMonitor: healthOpts, + SessionPersistence: sessionOpts, + } + + results, err := lbpools.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + lbPoolID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + lbPoolID, err := lbpools.ExtractPoolIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LBPool ID from task info: %w", err) + } + return lbPoolID, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(lbPoolID.(string)) + resourceLBPoolRead(ctx, d, m) + + log.Printf("[DEBUG] Finish LBPool creating (%s)", lbPoolID) + + return diags +} + +func resourceLBPoolRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBPool reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + lb, err := lbpools.Get(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + d.Set("name", lb.Name) + d.Set("lb_algorithm", lb.LoadBalancerAlgorithm.String()) + d.Set("protocol", lb.Protocol.String()) + + if len(lb.LoadBalancers) > 0 { + d.Set("loadbalancer_id", lb.LoadBalancers[0].ID) + } + + if len(lb.Listeners) > 0 { + d.Set("listener_id", lb.Listeners[0].ID) + } + + if lb.HealthMonitor != nil { + healthMonitor := map[string]interface{}{ + "id": lb.HealthMonitor.ID, + "type": lb.HealthMonitor.Type.String(), + "delay": lb.HealthMonitor.Delay, + "timeout": lb.HealthMonitor.Timeout, + "max_retries": lb.HealthMonitor.MaxRetries, + "max_retries_down": lb.HealthMonitor.MaxRetriesDown, + "url_path": lb.HealthMonitor.URLPath, + "expected_codes": lb.HealthMonitor.ExpectedCodes, + } + if lb.HealthMonitor.HTTPMethod != nil { + healthMonitor["http_method"] = lb.HealthMonitor.HTTPMethod.String() + } + + if err := d.Set("health_monitor", []interface{}{healthMonitor}); err != nil { + return diag.FromErr(err) + } + } + + if lb.SessionPersistence != nil { + sessionPersistence := map[string]interface{}{ + "type": lb.SessionPersistence.Type.String(), + "cookie_name": lb.SessionPersistence.CookieName, + "persistence_granularity": lb.SessionPersistence.PersistenceGranularity, + "persistence_timeout": lb.SessionPersistence.PersistenceTimeout, + } + + if err := d.Set("session_persistence", []interface{}{sessionPersistence}); err != nil { + return diag.FromErr(err) + } + } + + fields := []string{"project_id", "region_id"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish LBPool reading") + + return diags +} + +func resourceLBPoolUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBPool updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var change bool + opts := lbpools.UpdateOpts{Name: d.Get("name").(string)} + + if d.HasChange("lb_algorithm") { + opts.LBPoolAlgorithm = types.LoadBalancerAlgorithm(d.Get("lb_algorithm").(string)) + change = true + } + + if d.HasChange("health_monitor") { + opts.HealthMonitor = extractHealthMonitorMap(d) + change = true + } + + if d.HasChange("session_persistence") { + opts.SessionPersistence = extractSessionPersistenceMap(d) + change = true + } + + if !change { + log.Println("[DEBUG] Finish LBPool updating") + return resourceLBPoolRead(ctx, d, m) + } + + results, err := lbpools.Update(client, d.Id(), opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + return nil, nil + }) + + if err != nil { + return diag.FromErr(err) + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish LBPool updating") + + return resourceLBPoolRead(ctx, d, m) +} + +func resourceLBPoolDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LBPool deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LBPoolsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + results, err := lbpools.Delete(client, id).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if !errors.As(err, &errDefault404) { + return diag.FromErr(err) + } + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := lbpools.Get(client, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete LBPool with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting LBPool resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of LBPool deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_lifecyclepolicy.go b/edgecenter/resource_edgecenter_lifecyclepolicy.go new file mode 100644 index 00000000..d5cdfa56 --- /dev/null +++ b/edgecenter/resource_edgecenter_lifecyclepolicy.go @@ -0,0 +1,585 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "regexp" + "strconv" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/lifecyclepolicy/v1/lifecyclepolicy" +) + +const ( + LifecyclePolicyPoint = "lifecycle_policies" + // Maybe move to utils and use for other resources. + nameRegexString = `^[a-zA-Z0-9][a-zA-Z 0-9._\-]{1,61}[a-zA-Z0-9._]$` +) + +// Maybe move to utils and use for other resources. +var nameRegex = regexp.MustCompile(nameRegexString) + +func resourceLifecyclePolicy() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceLifecyclePolicyCreate, + ReadContext: resourceLifecyclePolicyRead, + UpdateContext: resourceLifecyclePolicyUpdate, + DeleteContext: resourceLifecyclePolicyDelete, + Description: "Represent lifecycle policy. Use to periodically take snapshots", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, lcpID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(lcpID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringMatch(nameRegex, ""), + }, + "status": { + Type: schema.TypeString, + Optional: true, + Default: lifecyclepolicy.PolicyStatusActive.String(), + ValidateFunc: validation.StringInSlice(lifecyclepolicy.PolicyStatus("").StringList(), false), + }, + "action": { + Type: schema.TypeString, + Optional: true, + Default: lifecyclepolicy.PolicyActionVolumeSnapshot.String(), + ForceNew: true, + ValidateFunc: validation.StringInSlice(lifecyclepolicy.PolicyAction("").StringList(), false), + }, + "volume": { + Type: schema.TypeSet, + Optional: true, + Description: "List of managed volumes", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.IsUUID, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "schedule": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "max_quantity": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 10000), + Description: "Maximum number of stored resources", + }, + "interval": { + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Description: "Use for taking actions with equal time intervals between them. Exactly one of interval and cron blocks should be provided", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "weeks": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: intervalScheduleParamDescription("week"), + }, + "days": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: intervalScheduleParamDescription("day"), + }, + "hours": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: intervalScheduleParamDescription("hour"), + }, + "minutes": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: intervalScheduleParamDescription("minute"), + }, + }, + }, + Optional: true, + }, + "cron": { + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Description: "Use for taking actions at specified moments of time. Exactly one of interval and cron blocks should be provided", + Elem: &schema.Resource{ // TODO: validate? + Schema: map[string]*schema.Schema{ + "timezone": { + Type: schema.TypeString, + Optional: true, + Default: "UTC", + }, + "month": { + Type: schema.TypeString, + Optional: true, + Default: "*", + Description: cronScheduleParamDescription(1, 12), + }, + "week": { + Type: schema.TypeString, + Optional: true, + Default: "*", + Description: cronScheduleParamDescription(1, 53), + }, + "day": { + Type: schema.TypeString, + Optional: true, + Default: "*", + Description: cronScheduleParamDescription(1, 31), + }, + "day_of_week": { + Type: schema.TypeString, + Optional: true, + Default: "*", + Description: cronScheduleParamDescription(0, 6), + }, + "hour": { + Type: schema.TypeString, + Optional: true, + Default: "*", + Description: cronScheduleParamDescription(0, 23), + }, + "minute": { + Type: schema.TypeString, + Optional: true, + Default: "0", + Description: cronScheduleParamDescription(0, 59), + }, + }, + }, + Optional: true, + }, + "resource_name_template": { + Type: schema.TypeString, + Optional: true, + Default: "reserve snap of the volume {volume_id}", + Description: "Used to name snapshots. {volume_id} is substituted with volume.id on creation", + }, + "retention_time": { + Type: schema.TypeList, + MinItems: 1, + MaxItems: 1, + Description: "If it is set, new resource will be deleted after time", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "weeks": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: retentionTimerParamDescription("week"), + }, + "days": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: retentionTimerParamDescription("day"), + }, + "hours": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: retentionTimerParamDescription("hour"), + }, + "minutes": { + Type: schema.TypeInt, + Optional: true, + Default: 0, + Description: retentionTimerParamDescription("minute"), + }, + }, + }, + Optional: true, + }, + "id": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "user_id": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func resourceLifecyclePolicyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := CreateClient(m.(*Config).Provider, d, LifecyclePolicyPoint, VersionPointV1) + if err != nil { + return diag.Errorf("Error creating client: %s", err) + } + + log.Printf("[DEBUG] Start of LifecyclePolicy creating") + opts, err := buildLifecyclePolicyCreateOpts(d) + if err != nil { + return diag.FromErr(err) + } + policy, err := lifecyclepolicy.Create(client, *opts).Extract() + if err != nil { + return diag.Errorf("Error creating lifecycle policy: %s", err) + } + d.SetId(strconv.Itoa(policy.ID)) + log.Printf("[DEBUG] Finish of LifecyclePolicy %s creating", d.Id()) + + return resourceLifecyclePolicyRead(ctx, d, m) +} + +func resourceLifecyclePolicyRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := CreateClient(m.(*Config).Provider, d, LifecyclePolicyPoint, VersionPointV1) + if err != nil { + return diag.Errorf("Error creating client: %s", err) + } + id := d.Id() + integerID, err := strconv.Atoi(id) + if err != nil { + return diag.Errorf("Error converting lifecycle policy ID to integer: %s", err) + } + + log.Printf("[DEBUG] Start of LifecyclePolicy %s reading", id) + policy, err := lifecyclepolicy.Get(client, integerID, lifecyclepolicy.GetOpts{NeedVolumes: true}).Extract() + if err != nil { + return diag.Errorf("Error getting lifecycle policy: %s", err) + } + + _ = d.Set("name", policy.Name) + _ = d.Set("status", policy.Status) + _ = d.Set("action", policy.Action) + _ = d.Set("user_id", policy.UserID) + if err = d.Set("volume", flattenVolumes(policy.Volumes)); err != nil { + return diag.Errorf("error setting lifecycle policy volumes: %s", err) + } + if err = d.Set("schedule", flattenSchedules(policy.Schedules)); err != nil { + return diag.Errorf("error setting lifecycle policy schedules: %s", err) + } + + log.Printf("[DEBUG] Finish of LifecyclePolicy %s reading", id) + + return nil +} + +func resourceLifecyclePolicyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := CreateClient(m.(*Config).Provider, d, LifecyclePolicyPoint, VersionPointV1) + if err != nil { + return diag.Errorf("Error creating client: %s", err) + } + id := d.Id() + integerID, err := strconv.Atoi(id) + if err != nil { + return diag.Errorf("Error converting lifecycle policy ID to integer: %s", err) + } + + log.Printf("[DEBUG] Start of LifecyclePolicy updating") + _, err = lifecyclepolicy.Update(client, integerID, buildLifecyclePolicyUpdateOpts(d)).Extract() + if err != nil { + return diag.Errorf("Error updating lifecycle policy: %s", err) + } + + if d.HasChange("volume") { + oldVolumes, newVolumes := d.GetChange("volume") + toRemove, toAdd := volumeSymmetricDifference(oldVolumes.(*schema.Set), newVolumes.(*schema.Set)) + _, err = lifecyclepolicy.RemoveVolumes(client, integerID, lifecyclepolicy.RemoveVolumesOpts{VolumeIds: toRemove}).Extract() + if err != nil { + return diag.Errorf("Error removing volumes from lifecycle policy: %s", err) + } + _, err = lifecyclepolicy.AddVolumes(client, integerID, lifecyclepolicy.AddVolumesOpts{VolumeIds: toAdd}).Extract() + if err != nil { + return diag.Errorf("Error adding volumes to lifecycle policy: %s", err) + } + } + log.Printf("[DEBUG] Finish of LifecyclePolicy %v updating", integerID) + + return resourceLifecyclePolicyRead(ctx, d, m) +} + +func resourceLifecyclePolicyDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + client, err := CreateClient(m.(*Config).Provider, d, LifecyclePolicyPoint, VersionPointV1) + if err != nil { + return diag.Errorf("Error creating client: %s", err) + } + id := d.Id() + integerID, err := strconv.Atoi(id) + if err != nil { + return diag.Errorf("Error converting lifecycle policy ID to integer: %s", err) + } + + log.Printf("[DEBUG] Start of LifecyclePolicy %s deleting", id) + err = lifecyclepolicy.Delete(client, integerID) + if err != nil { + return diag.Errorf("Error deleting lifecycle policy: %s", err) + } + d.SetId("") + log.Printf("[DEBUG] Finish of LifecyclePolicy %s deleting", id) + + return nil +} + +func expandIntervalSchedule(flat map[string]interface{}) *lifecyclepolicy.CreateIntervalScheduleOpts { + return &lifecyclepolicy.CreateIntervalScheduleOpts{ + Weeks: flat["weeks"].(int), + Days: flat["days"].(int), + Hours: flat["hours"].(int), + Minutes: flat["minutes"].(int), + } +} + +func expandCronSchedule(flat map[string]interface{}) *lifecyclepolicy.CreateCronScheduleOpts { + return &lifecyclepolicy.CreateCronScheduleOpts{ + Timezone: flat["timezone"].(string), + Week: flat["week"].(string), + DayOfWeek: flat["day_of_week"].(string), + Month: flat["month"].(string), + Day: flat["day"].(string), + Hour: flat["hour"].(string), + Minute: flat["minute"].(string), + } +} + +func expandRetentionTimer(flat []interface{}) *lifecyclepolicy.RetentionTimer { + if len(flat) > 0 { + rawRetention := flat[0].(map[string]interface{}) + return &lifecyclepolicy.RetentionTimer{ + Weeks: rawRetention["weeks"].(int), + Days: rawRetention["days"].(int), + Hours: rawRetention["hours"].(int), + Minutes: rawRetention["minutes"].(int), + } + } + return nil +} + +func expandSchedule(flat map[string]interface{}) (lifecyclepolicy.CreateScheduleOpts, error) { + t := lifecyclepolicy.ScheduleType("") + intervalSlice := flat["interval"].([]interface{}) + cronSlice := flat["cron"].([]interface{}) + if len(intervalSlice)+len(cronSlice) != 1 { + return nil, fmt.Errorf("exactly one of interval and cron blocks should be provided") + } + var expanded lifecyclepolicy.CreateScheduleOpts + if len(intervalSlice) > 0 { + t = lifecyclepolicy.ScheduleTypeInterval + expanded = expandIntervalSchedule(intervalSlice[0].(map[string]interface{})) + } else { + t = lifecyclepolicy.ScheduleTypeCron + expanded = expandCronSchedule(cronSlice[0].(map[string]interface{})) + } + expanded.SetCommonCreateScheduleOpts(lifecyclepolicy.CommonCreateScheduleOpts{ + Type: t, + ResourceNameTemplate: flat["resource_name_template"].(string), + MaxQuantity: flat["max_quantity"].(int), + RetentionTime: expandRetentionTimer(flat["retention_time"].([]interface{})), + }) + + return expanded, nil +} + +func expandSchedules(flat []interface{}) ([]lifecyclepolicy.CreateScheduleOpts, error) { + expanded := make([]lifecyclepolicy.CreateScheduleOpts, len(flat)) + for i, x := range flat { + exp, err := expandSchedule(x.(map[string]interface{})) + if err != nil { + return nil, err + } + expanded[i] = exp + } + return expanded, nil +} + +func expandVolumeIds(flat []interface{}) []string { + expanded := make([]string, len(flat)) + for i, x := range flat { + expanded[i] = x.(map[string]interface{})["id"].(string) + } + return expanded +} + +func buildLifecyclePolicyCreateOpts(d *schema.ResourceData) (*lifecyclepolicy.CreateOpts, error) { + schedules, err := expandSchedules(d.Get("schedule").([]interface{})) + if err != nil { + return nil, err + } + opts := &lifecyclepolicy.CreateOpts{ + Name: d.Get("name").(string), + Status: lifecyclepolicy.PolicyStatus(d.Get("status").(string)), + Schedules: schedules, + VolumeIds: expandVolumeIds(d.Get("volume").(*schema.Set).List()), + } + + // Action is required field from API point of view, but optional for us + if action, ok := d.GetOk("action"); ok { + opts.Action = lifecyclepolicy.PolicyAction(action.(string)) + } else { + opts.Action = lifecyclepolicy.PolicyActionVolumeSnapshot + } + + return opts, nil +} + +func volumeSymmetricDifference(oldVolumes, newVolumes *schema.Set) ([]string, []string) { + toRemove := make([]string, 0) + for _, v := range oldVolumes.List() { + if !newVolumes.Contains(v) { + toRemove = append(toRemove, v.(map[string]interface{})["id"].(string)) + } + } + toAdd := make([]string, 0) + for _, v := range newVolumes.List() { + if !oldVolumes.Contains(v) { + toAdd = append(toAdd, v.(map[string]interface{})["id"].(string)) + } + } + + return toRemove, toAdd +} + +func buildLifecyclePolicyUpdateOpts(d *schema.ResourceData) lifecyclepolicy.UpdateOpts { + opts := lifecyclepolicy.UpdateOpts{ + Name: d.Get("name").(string), + Status: lifecyclepolicy.PolicyStatus(d.Get("status").(string)), + } + return opts +} + +func flattenIntervalSchedule(expanded lifecyclepolicy.IntervalSchedule) interface{} { + return []map[string]int{{ + "weeks": expanded.Weeks, + "days": expanded.Days, + "hours": expanded.Hours, + "minutes": expanded.Minutes, + }} +} + +func flattenCronSchedule(expanded lifecyclepolicy.CronSchedule) interface{} { + return []map[string]string{{ + "timezone": expanded.Timezone, + "week": expanded.Week, + "day_of_week": expanded.DayOfWeek, + "month": expanded.Month, + "day": expanded.Day, + "hour": expanded.Hour, + "minute": expanded.Minute, + }} +} + +func flattenRetentionTimer(expanded *lifecyclepolicy.RetentionTimer) interface{} { + if expanded != nil { + return []map[string]int{{ + "weeks": expanded.Weeks, + "days": expanded.Days, + "hours": expanded.Hours, + "minutes": expanded.Minutes, + }} + } + return []interface{}{} +} + +func flattenSchedule(expanded lifecyclepolicy.Schedule) map[string]interface{} { + common := expanded.GetCommonSchedule() + flat := map[string]interface{}{ + "max_quantity": common.MaxQuantity, + "resource_name_template": common.ResourceNameTemplate, + "retention_time": flattenRetentionTimer(common.RetentionTime), + "id": common.ID, + "type": common.Type, + } + switch common.Type { + case lifecyclepolicy.ScheduleTypeInterval: + flat["interval"] = flattenIntervalSchedule(expanded.(lifecyclepolicy.IntervalSchedule)) + case lifecyclepolicy.ScheduleTypeCron: + flat["cron"] = flattenCronSchedule(expanded.(lifecyclepolicy.CronSchedule)) + } + + return flat +} + +func flattenSchedules(expanded []lifecyclepolicy.Schedule) []map[string]interface{} { + flat := make([]map[string]interface{}, len(expanded)) + for i, x := range expanded { + flat[i] = flattenSchedule(x) + } + return flat +} + +func flattenVolumes(expanded []lifecyclepolicy.Volume) []map[string]string { + flat := make([]map[string]string, len(expanded)) + for i, volume := range expanded { + flat[i] = map[string]string{"id": volume.ID, "name": volume.Name} + } + return flat +} + +func cronScheduleParamDescription(min, max int) string { + return fmt.Sprintf("Either single asterisk or comma-separated list of integers (%v-%v)", min, max) +} + +func intervalScheduleParamDescription(unit string) string { + return fmt.Sprintf("Number of %ss to wait between actions", unit) +} + +func retentionTimerParamDescription(unit string) string { + return fmt.Sprintf("Number of %ss to wait before deleting snapshot", unit) +} diff --git a/edgecenter/resource_edgecenter_loadbalancer.go b/edgecenter/resource_edgecenter_loadbalancer.go new file mode 100644 index 00000000..b3df2c52 --- /dev/null +++ b/edgecenter/resource_edgecenter_loadbalancer.go @@ -0,0 +1,469 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +const ( + LoadBalancersPoint = "loadbalancers" + LoadBalancerCreateTimeout = 2400 +) + +func resourceLoadBalancer() *schema.Resource { + return &schema.Resource{ + DeprecationMessage: "!> **WARNING:** This resource is deprecated and will be removed in the next major version. Use edgecenter_loadbalancerv2 resource instead", + CreateContext: resourceLoadBalancerCreate, + ReadContext: resourceLoadBalancerRead, + UpdateContext: resourceLoadBalancerUpdate, + DeleteContext: resourceLoadBalancerDelete, + Description: "Represent load balancer", + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, lbID, listenerID, err := ImportStringParserExtended(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(lbID) + + config := m.(*Config) + provider := config.Provider + + listenersClient, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return nil, err + } + + listener, err := listeners.Get(listenersClient, listenerID).Extract() + if err != nil { + return nil, fmt.Errorf("extracting Listener resource error: %w", err) + } + + l := extractListenerIntoMap(listener) + if err := d.Set("listener", []interface{}{l}); err != nil { + return nil, fmt.Errorf("set listener error: %w", err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer.", + }, + "flavor": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "vip_network_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "vip_subnet_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "vip_address": { + Type: schema.TypeString, + Description: "Load balancer IP address", + Computed: true, + }, + "listener": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "certificate": { + Type: schema.TypeString, + Optional: true, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available values is '%s' (currently work, other do not work on ed-8), '%s', '%s', '%s'", types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch types.ProtocolType(v) { + case types.ProtocolTypeHTTP, types.ProtocolTypeHTTPS, types.ProtocolTypeTCP, types.ProtocolTypeUDP: + return diag.Diagnostics{} + case types.ProtocolTypeTerminatedHTTPS, types.ProtocolTypePROXY: + } + return diag.Errorf("wrong protocol %s, available values is 'HTTP', 'HTTPS', 'TCP', 'UDP'", v) + }, + }, + "certificate_chain": { + Type: schema.TypeString, + Optional: true, + }, + "protocol_port": { + Type: schema.TypeInt, + Required: true, + }, + "private_key": { + Type: schema.TypeString, + Optional: true, + }, + "insert_x_forwarded": { + Type: schema.TypeBool, + Optional: true, + }, + "secret_id": { + Type: schema.TypeString, + Optional: true, + }, + "sni_secret_id": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceLoadBalancerCreate(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return diag.FromErr(fmt.Errorf("use edgecenter_loadbalancerv2 resource instead")) +} + +func resourceLoadBalancerRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + lb, err := loadbalancers.Get(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + d.Set("project_id", lb.ProjectID) + d.Set("region_id", lb.RegionID) + d.Set("name", lb.Name) + d.Set("flavor", lb.Flavor.FlavorName) + + if lb.VipAddress != nil { + d.Set("vip_address", lb.VipAddress.String()) + } + + fields := []string{"vip_network_id", "vip_subnet_id"} + revertState(d, &fields) + + listenersClient, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + var ok bool + currentL := make(map[string]interface{}) + // we need to find correct listener because after upgrade some of them could be nil + // but still in terraform.state + cls := d.Get("listener").([]interface{}) + for _, cl := range cls { + if currentL, ok = cl.(map[string]interface{}); ok { + break + } + } + + for _, l := range lb.Listeners { + listener, err := listeners.Get(listenersClient, l.ID).Extract() + if err != nil { + return diag.FromErr(err) + } + port, _ := currentL["protocol_port"].(int) + if (listener.ProtocolPort == port && listener.Protocol.String() == currentL["protocol"]) || len(cls) == 0 { + currentL = extractListenerIntoMap(listener) + break + } + } + if err := d.Set("listener", []interface{}{currentL}); err != nil { + diag.FromErr(err) + } + + metadataMap, metadataReadOnly := PrepareMetadata(lb.Metadata) + + if err = d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish LoadBalancer reading") + + return diags +} + +func resourceLoadBalancerUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + opts := loadbalancers.UpdateOpts{ + Name: d.Get("name").(string), + } + if _, err = loadbalancers.Update(client, d.Id(), opts).Extract(); err != nil { + return diag.FromErr(err) + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + } + + if d.HasChange("listener") { + client, err := CreateClient(provider, d, LBListenersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + oldListenerRaw, newListenerRaw := d.GetChange("listener") + oldListener := oldListenerRaw.([]interface{})[0].(map[string]interface{}) + newListener := newListenerRaw.([]interface{})[0].(map[string]interface{}) + + listenerID := oldListener["id"].(string) + if oldListener["protocol"].(string) != newListener["protocol"].(string) || + oldListener["protocol_port"].(int) != newListener["protocol_port"].(int) { + // if protocol or port changed listener need to be recreated + // delete at first + results, err := listeners.Delete(client, listenerID).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBListenerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := listeners.Get(client, listenerID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete LBListener with ID: %s", listenerID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Listener resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + opts := listeners.CreateOpts{ + Name: newListener["name"].(string), + Protocol: types.ProtocolType(newListener["protocol"].(string)), + ProtocolPort: newListener["protocol_port"].(int), + LoadBalancerID: d.Id(), + InsertXForwarded: newListener["insert_x_forwarded"].(bool), + SecretID: newListener["secret_id"].(string), + } + sniSecretIDRaw := newListener["sni_secret_id"].([]interface{}) + if len(sniSecretIDRaw) != 0 { + sniSecretID := make([]string, len(sniSecretIDRaw)) + for i, s := range sniSecretIDRaw { + sniSecretID[i] = s.(string) + } + opts.SNISecretID = sniSecretID + } + + results, err = listeners.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID = results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LBListenerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + listenerID, err := listeners.ExtractListenerIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LBListener ID from task info: %w", err) + } + return listenerID, nil + }) + if err != nil { + return diag.FromErr(err) + } + } else { + opts := listeners.UpdateOpts{ + Name: newListener["name"].(string), + SecretID: newListener["secret_id"].(string), + } + sniSecretIDRaw := newListener["sni_secret_id"].([]interface{}) + sniSecretID := make([]string, len(sniSecretIDRaw)) + for i, s := range sniSecretIDRaw { + sniSecretID[i] = s.(string) + } + opts.SNISecretID = sniSecretID + if _, err := listeners.Update(client, listenerID, opts).Extract(); err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + + meta, err := utils.MapInterfaceToMapString(nmd.(map[string]interface{})) + if err != nil { + return diag.Errorf("cannot get metadata. Error: %s", err) + } + + err = metadata.ResourceMetadataReplace(client, d.Id(), meta).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + log.Println("[DEBUG] Finish LoadBalancer updating") + + return resourceLoadBalancerRead(ctx, d, m) +} + +func resourceLoadBalancerDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + results, err := loadbalancers.Delete(client, id).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, LoadBalancerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := loadbalancers.Get(client, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete loadbalancer with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Load Balancer resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of LoadBalancer deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_loadbalancerv2.go b/edgecenter/resource_edgecenter_loadbalancerv2.go new file mode 100644 index 00000000..c6456b36 --- /dev/null +++ b/edgecenter/resource_edgecenter_loadbalancerv2.go @@ -0,0 +1,294 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +func resourceLoadBalancerV2() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceLoadBalancerV2Create, + ReadContext: resourceLoadBalancerV2Read, + UpdateContext: resourceLoadBalancerV2Update, + DeleteContext: resourceLoadBalancerDelete, + Description: "Represent load balancer without nested listener", + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, lbID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(lbID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the load balancer.", + }, + "flavor": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The flavor or specification of the load balancer to be created.", + }, + "vip_port_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"vip_network_id"}, + Description: "Attaches the created reserved IP.", + }, + "vip_network_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"vip_port_id"}, + Description: "Attaches the created network.", + }, + "vip_subnet_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + RequiredWith: []string{"vip_network_id"}, + Description: "The ID of the subnet in which to allocate the VIP address for the load balancer.", + }, + "vip_address": { + Type: schema.TypeString, + Computed: true, + Description: "Load balancer IP address", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceLoadBalancerV2Create(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := loadbalancers.CreateOpts{ + Name: d.Get("name").(string), + VipPortID: d.Get("vip_port_id").(string), + VipNetworkID: d.Get("vip_network_id").(string), + VipSubnetID: d.Get("vip_subnet_id").(string), + } + + if metadataRaw, ok := d.GetOk("metadata_map"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + opts.Metadata = meta + } + + lbFlavor := d.Get("flavor").(string) + if len(lbFlavor) != 0 { + opts.Flavor = &lbFlavor + } + + results, err := loadbalancers.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + lbID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, LoadBalancerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + lbID, err := loadbalancers.ExtractLoadBalancerIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LoadBalancer ID from task info: %w", err) + } + return lbID, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(lbID.(string)) + + resourceLoadBalancerV2Read(ctx, d, m) + + log.Printf("[DEBUG] Finish LoadBalancer creating (%s)", lbID) + + return diags +} + +func resourceLoadBalancerV2Read(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + lb, err := loadbalancers.Get(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + d.Set("project_id", lb.ProjectID) + d.Set("region_id", lb.RegionID) + d.Set("name", lb.Name) + d.Set("flavor", lb.Flavor.FlavorName) + + if lb.VipAddress != nil { + d.Set("vip_address", lb.VipAddress.String()) + } + + fields := []string{"vip_network_id", "vip_subnet_id"} + revertState(d, &fields) + + metadataList, err := metadata.ResourceMetadataListAll(client, d.Id()) + if err != nil { + return diag.FromErr(err) + } + metadataMap, metadataReadOnly := PrepareMetadata(metadataList) + + if err = d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish LoadBalancer reading") + + return diags +} + +func resourceLoadBalancerV2Update(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start LoadBalancer updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, LoadBalancersPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + opts := loadbalancers.UpdateOpts{ + Name: d.Get("name").(string), + } + _, err = loadbalancers.Update(client, d.Id(), opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + + meta, err := utils.MapInterfaceToMapString(nmd.(map[string]interface{})) + if err != nil { + return diag.Errorf("cannot get metadata. Error: %s", err) + } + + err = metadata.ResourceMetadataReplace(client, d.Id(), meta).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + log.Println("[DEBUG] Finish LoadBalancer updating") + + return resourceLoadBalancerV2Read(ctx, d, m) +} diff --git a/edgecenter/resource_edgecenter_network.go b/edgecenter/resource_edgecenter_network.go new file mode 100644 index 00000000..b34d2884 --- /dev/null +++ b/edgecenter/resource_edgecenter_network.go @@ -0,0 +1,313 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +const ( + NetworkDeleting int = 1200 + NetworkCreatingTimeout int = 1200 + NetworksPoint = "networks" + SharedNetworksPoint = "availablenetworks" +) + +func resourceNetwork() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceNetworkCreate, + ReadContext: resourceNetworkRead, + UpdateContext: resourceNetworkUpdate, + DeleteContext: resourceNetworkDelete, + Description: "Represent network. A network is a software-defined network in a cloud computing infrastructure", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, NetworkID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(NetworkID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the network.", + }, + "mtu": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum Transmission Unit (MTU) for the network. It determines the maximum packet size that can be transmitted without fragmentation.", + }, + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "'vlan' or 'vxlan' network type is allowed. Default value is 'vxlan'", + }, + "create_router": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Create external router to the network, default true", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceNetworkCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Network creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, NetworksPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + createOpts := networks.CreateOpts{ + Name: d.Get("name").(string), + Type: d.Get("type").(string), + CreateRouter: d.Get("create_router").(bool), + } + + if metadataRaw, ok := d.GetOk("metadata_map"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + + createOpts.Metadata = meta + } + + log.Printf("Create network ops: %+v", createOpts) + results, err := networks.Create(client, createOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + networkID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, NetworkCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + NetworkID, err := networks.ExtractNetworkIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Network ID from task info: %w", err) + } + return NetworkID, nil + }, + ) + log.Printf("[DEBUG] Network id (%s)", networkID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(networkID.(string)) + resourceNetworkRead(ctx, d, m) + + log.Printf("[DEBUG] Finish Network creating (%s)", networkID) + + return diags +} + +func resourceNetworkRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start network reading") + log.Printf("[DEBUG] Start network reading%s", d.State()) + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + networkID := d.Id() + log.Printf("[DEBUG] Network id = %s", networkID) + + client, err := CreateClient(provider, d, NetworksPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + network, err := networks.Get(client, networkID).Extract() + if err != nil { + return diag.Errorf("cannot get network with ID: %s. Error: %s", networkID, err) + } + + d.Set("name", network.Name) + d.Set("mtu", network.MTU) + d.Set("type", network.Type) + d.Set("region_id", network.RegionID) + d.Set("project_id", network.ProjectID) + + metadataMap, metadataReadOnly := PrepareMetadata(network.Metadata) + + if err = d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + fields := []string{"create_router"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish network reading") + + return diags +} + +func resourceNetworkUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start network updating") + networkID := d.Id() + log.Printf("[DEBUG] Volume id = %s", networkID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, NetworksPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + newName := d.Get("name").(string) + _, err := networks.Update(client, networkID, networks.UpdateOpts{Name: newName}).Extract() + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + + meta, err := utils.MapInterfaceToMapString(nmd.(map[string]interface{})) + if err != nil { + return diag.Errorf("cannot get metadata. Error: %s", err) + } + + err = metadata.ResourceMetadataReplace(client, networkID, meta).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish network updating") + + return resourceNetworkRead(ctx, d, m) +} + +func resourceNetworkDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start network deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + networkID := d.Id() + log.Printf("[DEBUG] Network id = %s", networkID) + + client, err := CreateClient(provider, d, NetworksPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + results, err := networks.Delete(client, networkID).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, NetworkDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := networks.Get(client, networkID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete network with ID: %s", networkID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Network resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of network deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_reservedfixedip.go b/edgecenter/resource_edgecenter_reservedfixedip.go new file mode 100644 index 00000000..f9c61741 --- /dev/null +++ b/edgecenter/resource_edgecenter_reservedfixedip.go @@ -0,0 +1,382 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "net" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/port/v1/ports" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/reservedfixedip/v1/reservedfixedips" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + ReservedFixedIPsPoint = "reserved_fixed_ips" + portsPoint = "ports" + ReservedFixedIPCreateTimeout = 1200 +) + +func resourceReservedFixedIP() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceReservedFixedIPCreate, + ReadContext: resourceReservedFixedIPRead, + UpdateContext: resourceReservedFixedIPUpdate, + DeleteContext: resourceReservedFixedIPDelete, + Description: "Represent reserved ips", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, ipID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(ipID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: fmt.Sprintf("The type of reserved fixed IP. Valid values are '%s', '%s', '%s', and '%s'", reservedfixedips.External, reservedfixedips.Subnet, reservedfixedips.AnySubnet, reservedfixedips.IPAddress), + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + switch reservedfixedips.ReservedFixedIPType(v) { + case reservedfixedips.External, reservedfixedips.Subnet, reservedfixedips.AnySubnet, reservedfixedips.IPAddress: + return diag.Diagnostics{} + } + return diag.Errorf("wrong type %s, available values is '%s', '%s', '%s', '%s'", v, reservedfixedips.External, reservedfixedips.Subnet, reservedfixedips.AnySubnet, reservedfixedips.IPAddress) + }, + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the reserved fixed IP.", + }, + "fixed_ip_address": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: "The IP address that is associated with the reserved IP.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + ip := net.ParseIP(v) + if ip != nil { + return diag.Diagnostics{} + } + + return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) + }, + }, + "subnet_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: "ID of the subnet from which the fixed IP should be reserved.", + }, + "network_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + Description: "ID of the network to which the reserved fixed IP is associated.", + }, + "is_vip": { + Type: schema.TypeBool, + Required: true, + Description: "Flag to determine if the reserved fixed IP should be treated as a Virtual IP (VIP).", + }, + "port_id": { + Type: schema.TypeString, + Description: "ID of the port_id underlying the reserved fixed IP.", + Computed: true, + }, + "allowed_address_pairs": { + Type: schema.TypeList, + Optional: true, + Description: "Group of IP addresses that share the current IP as VIP.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_address": { + Type: schema.TypeString, + Optional: true, + }, + "mac_address": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceReservedFixedIPCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ReservedFixedIP creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ReservedFixedIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := reservedfixedips.CreateOpts{ + IsVip: d.Get("is_vip").(bool), + } + + portType := d.Get("type").(string) + switch reservedfixedips.ReservedFixedIPType(portType) { + case reservedfixedips.External: + case reservedfixedips.Subnet: + subnetID := d.Get("subnet_id").(string) + if subnetID == "" { + return diag.Errorf("'subnet_id' required if the type is 'subnet'") + } + + opts.SubnetID = subnetID + case reservedfixedips.AnySubnet: + networkID := d.Get("network_id").(string) + if networkID == "" { + return diag.Errorf("'network_id' required if the type is 'any_subnet'") + } + opts.NetworkID = networkID + case reservedfixedips.IPAddress: + networkID := d.Get("network_id").(string) + ipAddress := d.Get("fixed_ip_address").(string) + if networkID == "" || ipAddress == "" { + return diag.Errorf("'network_id' and 'fixed_ip_address' required if the type is 'ip_address'") + } + + opts.NetworkID = networkID + opts.IPAddress = net.ParseIP(ipAddress) + default: + return diag.Errorf("wrong type %s, available values is 'external', 'subnet', 'any_subnet', 'ip_address'", portType) + } + + opts.Type = reservedfixedips.ReservedFixedIPType(portType) + results, err := reservedfixedips.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + reservedFixedIPID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, ReservedFixedIPCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + reservedFixedIPID, err := reservedfixedips.ExtractReservedFixedIPIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve reservedFixedIP ID from task info: %w", err) + } + return reservedFixedIPID, nil + }) + + log.Printf("[DEBUG] ReservedFixedIP id (%s)", reservedFixedIPID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(reservedFixedIPID.(string)) + resourceReservedFixedIPRead(ctx, d, m) + + log.Printf("[DEBUG] Finish ReservedFixedIP creating (%s)", reservedFixedIPID) + + return diags +} + +func resourceReservedFixedIPRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ReservedFixedIP reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ReservedFixedIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + reservedFixedIP, err := reservedfixedips.Get(client, d.Id()).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + log.Printf("[WARN] Removing reserved fixed ip %s because resource doesn't exist anymore", d.Id()) + d.SetId("") + return nil + } + return diag.FromErr(err) + } + + d.Set("project_id", reservedFixedIP.ProjectID) + d.Set("region_id", reservedFixedIP.RegionID) + d.Set("status", reservedFixedIP.Status) + d.Set("fixed_ip_address", reservedFixedIP.FixedIPAddress.String()) + d.Set("subnet_id", reservedFixedIP.SubnetID) + d.Set("network_id", reservedFixedIP.NetworkID) + d.Set("is_vip", reservedFixedIP.IsVip) + d.Set("port_id", reservedFixedIP.PortID) + + allowedPairs := make([]map[string]interface{}, len(reservedFixedIP.AllowedAddressPairs)) + for i, p := range reservedFixedIP.AllowedAddressPairs { + pair := make(map[string]interface{}) + + pair["ip_address"] = p.IPAddress + pair["mac_address"] = p.MacAddress + + allowedPairs[i] = pair + } + if err := d.Set("allowed_address_pairs", allowedPairs); err != nil { + return diag.FromErr(err) + } + fields := []string{"type"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish ReservedFixedIP reading") + + return diags +} + +func resourceReservedFixedIPUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ReservedFixedIP updating") + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ReservedFixedIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + id := d.Id() + if d.HasChange("is_vip") { + opts := reservedfixedips.SwitchVIPOpts{IsVip: d.Get("is_vip").(bool)} + _, err := reservedfixedips.SwitchVIP(client, id, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("allowed_address_pairs") { + aap := d.Get("allowed_address_pairs").([]interface{}) + allowedAddressPairs := make([]reservedfixedips.AllowedAddressPairs, len(aap)) + for i, p := range aap { + pair := p.(map[string]interface{}) + allowedAddressPairs[i] = reservedfixedips.AllowedAddressPairs{ + IPAddress: pair["ip_address"].(string), + MacAddress: pair["mac_address"].(string), + } + } + + clientPort, err := CreateClient(provider, d, portsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := ports.AllowAddressPairsOpts{AllowedAddressPairs: allowedAddressPairs} + if _, err := ports.AllowAddressPairs(clientPort, id, opts).Extract(); err != nil { + return diag.FromErr(err) + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish ReservedFixedIP updating") + + return resourceReservedFixedIPRead(ctx, d, m) +} + +func resourceReservedFixedIPDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ReservedFixedIP deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ReservedFixedIPsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + // only is_vip == false + isVip := d.Get("is_vip").(bool) + if isVip { + return diag.Errorf("could not delete reserved fixed ip with is_vip=true") + } + + id := d.Id() + results, err := reservedfixedips.Delete(client, id).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + d.SetId("") + log.Printf("[DEBUG] Finish of ReservedFixedIP deleting") + return diags + } + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, ReservedFixedIPCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + _, err := reservedfixedips.Get(client, id).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete reserved fixed ip with ID: %s", id) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting FixedIP resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of ReservedFixedIP deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_router.go b/edgecenter/resource_edgecenter_router.go new file mode 100644 index 00000000..917d732b --- /dev/null +++ b/edgecenter/resource_edgecenter_router.go @@ -0,0 +1,468 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/router/v1/routers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + RouterDeleting int = 1200 + RouterCreatingTimeout int = 1200 + RouterPoint = "routers" +) + +func resourceRouter() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceRouterCreate, + ReadContext: resourceRouterRead, + UpdateContext: resourceRouterUpdate, + DeleteContext: resourceRouterDelete, + Description: "Represent router. Router enables you to dynamically exchange routes between networks", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, routerID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(routerID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the router.", + }, + "external_gateway_info": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Description: "Information related to the external gateway.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Description: "Must be 'manual' or 'default'", + Optional: true, + Computed: true, + }, + "enable_snat": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + }, + "network_id": { + Type: schema.TypeString, + Description: "Id of the external network", + Optional: true, + Computed: true, + }, + "external_fixed_ips": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ip_address": { + Type: schema.TypeString, + Required: true, + }, + "subnet_id": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "interfaces": { + Type: schema.TypeSet, + Optional: true, + Set: routerInterfaceUniqueID, + Description: "Set of interfaces associated with the router.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": { + Type: schema.TypeString, + Description: "must be 'subnet'", + Required: true, + }, + "subnet_id": { + Type: schema.TypeString, + Description: "Subnet for router interface must have a gateway IP", + Required: true, + }, + "port_id": { + Type: schema.TypeString, + Computed: true, + }, + "network_id": { + Type: schema.TypeString, + Computed: true, + }, + "mac_address": { + Type: schema.TypeString, + Computed: true, + }, + "ip_address": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "routes": { + Type: schema.TypeList, + Optional: true, + Description: "List of static routes to be applied to the router.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination": { + Type: schema.TypeString, + Required: true, + }, + "nexthop": { + Type: schema.TypeString, + Required: true, + Description: "IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR", + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceRouterCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start router creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, RouterPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + createOpts := routers.CreateOpts{} + + createOpts.Name = d.Get("name").(string) + + egi := d.Get("external_gateway_info") + if len(egi.([]interface{})) > 0 { + gws, err := extractExternalGatewayInfoMap(egi.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + createOpts.ExternalGatewayInfo = gws + } + + ifs := d.Get("interfaces").(*schema.Set) + if ifs.Len() > 0 { + ifaces, err := extractInterfacesMap(ifs.List()) + if err != nil { + return diag.FromErr(err) + } + createOpts.Interfaces = ifaces + } + + rs := d.Get("routes") + if len(rs.([]interface{})) > 0 { + routes, err := extractHostRoutesMap(rs.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + createOpts.Routes = routes + } + + results, err := routers.Create(client, createOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + routerID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, RouterCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Router, err := routers.ExtractRouterIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Router ID from task info: %w", err) + } + return Router, nil + }, + ) + log.Printf("[DEBUG] Router id (%s)", routerID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(routerID.(string)) + resourceRouterRead(ctx, d, m) + + log.Printf("[DEBUG] Finish router creating (%s)", routerID) + + return diags +} + +func resourceRouterRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start router reading") + log.Printf("[DEBUG] Start router reading%s", d.State()) + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + routerID := d.Id() + log.Printf("[DEBUG] Router id = %s", routerID) + + client, err := CreateClient(provider, d, RouterPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + router, err := routers.Get(client, routerID).Extract() + if err != nil { + return diag.Errorf("cannot get router with ID: %s. Error: %s", routerID, err) + } + + d.Set("name", router.Name) + + if len(router.ExternalGatewayInfo.ExternalFixedIPs) > 0 { + egi := make(map[string]interface{}, 4) + egilst := make([]map[string]interface{}, 1) + egi["enable_snat"] = router.ExternalGatewayInfo.EnableSNat + egi["network_id"] = router.ExternalGatewayInfo.NetworkID + + egist := d.Get("external_gateway_info") + if len(egist.([]interface{})) > 0 { + gws, err := extractExternalGatewayInfoMap(egist.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + egi["type"] = gws.Type + } + + efip := make([]map[string]string, len(router.ExternalGatewayInfo.ExternalFixedIPs)) + for i, fip := range router.ExternalGatewayInfo.ExternalFixedIPs { + tmpfip := make(map[string]string, 1) + tmpfip["ip_address"] = fip.IPAddress + tmpfip["subnet_id"] = fip.SubnetID + efip[i] = tmpfip + } + egi["external_fixed_ips"] = efip + + egilst[0] = egi + d.Set("external_gateway_info", egilst) + } + + ifs := make([]interface{}, 0, len(router.Interfaces)) + for _, iface := range router.Interfaces { + for _, subnet := range iface.IPAssignments { + smap := make(map[string]interface{}, 6) + smap["port_id"] = iface.PortID + smap["network_id"] = iface.NetworkID + smap["mac_address"] = iface.MacAddress.String() + smap["type"] = "subnet" + smap["subnet_id"] = subnet.SubnetID + smap["ip_address"] = subnet.IPAddress.String() + ifs = append(ifs, smap) + } + } + if err := d.Set("interfaces", schema.NewSet(routerInterfaceUniqueID, ifs)); err != nil { + return diag.FromErr(err) + } + + rs := make([]map[string]string, len(router.Routes)) + for i, r := range router.Routes { + rmap := make(map[string]string, 2) + rmap["destination"] = r.Destination.String() + rmap["nexthop"] = r.NextHop.String() + rs[i] = rmap + } + d.Set("routes", rs) + + log.Println("[DEBUG] Finish router reading") + + return diags +} + +func resourceRouterUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start router updating") + routerID := d.Id() + log.Printf("[DEBUG] Router id = %s", routerID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, RouterPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + updateOpts := routers.UpdateOpts{} + + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + + // Only one kind of update is supported when external manual gateway is set. + if d.HasChange("external_gateway_info") { + egi := d.Get("external_gateway_info") + if len(egi.([]interface{})) > 0 { + gws, err := extractExternalGatewayInfoMap(egi.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + if gws.Type == "manual" { + updateOpts.ExternalGatewayInfo = gws + } + } + } + + if d.HasChange("interfaces") { + oldValue, newValue := d.GetChange("interfaces") + oifs, err := extractInterfacesMap(oldValue.(*schema.Set).List()) + if err != nil { + return diag.FromErr(err) + } + nifs, err := extractInterfacesMap(newValue.(*schema.Set).List()) + if err != nil { + return diag.FromErr(err) + } + + omap := make(map[string]int, len(oifs)) + + for index, iface := range oifs { + omap[iface.SubnetID] = index + } + + for _, niface := range nifs { + _, ok := omap[niface.SubnetID] + if ok { + delete(omap, niface.SubnetID) + } else { + _, err = routers.Attach(client, routerID, niface.SubnetID).Extract() + if err != nil { + return diag.FromErr(err) + } + } + } + + for _, v := range omap { + oiface := oifs[v] + _, err = routers.Detach(client, routerID, oiface.SubnetID).Extract() + if err != nil { + return diag.FromErr(err) + } + } + } + + if d.HasChange("routes") { + rs := d.Get("routes") + updateOpts.Routes = make([]subnets.HostRoute, 0) + if len(rs.([]interface{})) > 0 { + routes, err := extractHostRoutesMap(rs.([]interface{})) + if err != nil { + return diag.FromErr(err) + } + updateOpts.Routes = routes + } + } + + _, err = routers.Update(client, routerID, updateOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish router updating") + + return resourceRouterRead(ctx, d, m) +} + +func resourceRouterDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start router deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + routerID := d.Id() + log.Printf("[DEBUG] Router id = %s", routerID) + + client, err := CreateClient(provider, d, RouterPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + results, err := routers.Delete(client, routerID).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, RouterDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := routers.Get(client, routerID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete router with ID: %s", routerID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Router resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of router deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_secret.go b/edgecenter/resource_edgecenter_secret.go new file mode 100644 index 00000000..f4cf9c01 --- /dev/null +++ b/edgecenter/resource_edgecenter_secret.go @@ -0,0 +1,285 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/secret/v1/secrets" + secretsV2 "github.com/Edge-Center/edgecentercloud-go/edgecenter/secret/v2/secrets" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + SecretDeleting int = 1200 + SecretCreatingTimeout int = 1200 + SecretPoint = "secrets" +) + +func resourceSecret() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSecretCreate, + ReadContext: resourceSecretRead, + DeleteContext: resourceSecretDelete, + Description: "Represent secret", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, secretID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(secretID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the secret.", + }, + "private_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "SSL private key in PEM format", + }, + "certificate_chain": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "SSL certificate chain of intermediates and root certificates in PEM format", + }, + "certificate": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "SSL certificate in PEM format", + }, + "algorithm": { + Type: schema.TypeString, + Computed: true, + Description: "The encryption algorithm used for the secret.", + }, + "bit_length": { + Type: schema.TypeInt, + Computed: true, + Description: "The bit length of the encryption algorithm.", + }, + "mode": { + Type: schema.TypeString, + Computed: true, + Description: "The mode of the encryption algorithm.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the secret.", + }, + "content_types": { + Type: schema.TypeMap, + Computed: true, + Description: "The content types associated with the secret's payload.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "expiration": { + Type: schema.TypeString, + Description: "Datetime when the secret will expire. The format is 2025-12-28T19:14:44", + Optional: true, + Computed: true, + StateFunc: func(val interface{}) string { + expTime, _ := time.Parse(edgecloud.RFC3339NoZ, val.(string)) + return expTime.Format(edgecloud.RFC3339NoZ) + }, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + rawTime := i.(string) + _, err := time.Parse(edgecloud.RFC3339NoZ, rawTime) + if err != nil { + return diag.FromErr(err) + } + return nil + }, + }, + "created": { + Type: schema.TypeString, + Description: "Datetime when the secret was created. The format is 2025-12-28T19:14:44.180394", + Computed: true, + }, + }, + } +} + +func resourceSecretCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Secret creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SecretPoint, VersionPointV2) + if err != nil { + return diag.FromErr(err) + } + + opts := secretsV2.CreateOpts{ + Name: d.Get("name").(string), + Payload: secretsV2.PayloadOpts{ + CertificateChain: d.Get("certificate_chain").(string), + Certificate: d.Get("certificate").(string), + PrivateKey: d.Get("private_key").(string), + }, + } + if rawTime := d.Get("expiration").(string); rawTime != "" { + expiration, err := time.Parse(edgecloud.RFC3339NoZ, rawTime) + if err != nil { + return diag.FromErr(err) + } + opts.Expiration = &expiration + } + + results, err := secretsV2.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + + clientV1, err := CreateClient(provider, d, SecretPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + secretID, err := tasks.WaitTaskAndReturnResult(clientV1, taskID, true, SecretCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(clientV1, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Secret, err := secrets.ExtractSecretIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Secret ID from task info: %w", err) + } + return Secret, nil + }, + ) + if err != nil { + return diag.FromErr(err) + } + log.Printf("[DEBUG] Secret id (%s)", secretID) + + d.SetId(secretID.(string)) + + resourceSecretRead(ctx, d, m) + + log.Printf("[DEBUG] Finish Secret creating (%s)", secretID) + + return diags +} + +func resourceSecretRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start secret reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + secretID := d.Id() + log.Printf("[DEBUG] Secret id = %s", secretID) + + client, err := CreateClient(provider, d, SecretPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + secret, err := secrets.Get(client, secretID).Extract() + if err != nil { + return diag.Errorf("cannot get secret with ID: %s. Error: %s", secretID, err.Error()) + } + d.Set("name", secret.Name) + d.Set("algorithm", secret.Algorithm) + d.Set("bit_length", secret.BitLength) + d.Set("mode", secret.Mode) + d.Set("status", secret.Status) + d.Set("expiration", secret.Expiration.Format(edgecloud.RFC3339NoZ)) + d.Set("created", secret.CreatedAt.Format(edgecloud.RFC3339MilliNoZ)) + if err := d.Set("content_types", secret.ContentTypes); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish secret reading") + + return diags +} + +func resourceSecretDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start secret deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + secretID := d.Id() + log.Printf("[DEBUG] Secret id = %s", secretID) + + client, err := CreateClient(provider, d, SecretPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + results, err := secrets.Delete(client, secretID).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, SecretDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := secrets.Get(client, secretID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete secret with ID: %s", secretID) + } + return nil, nil + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of secret deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_securitygroup.go b/edgecenter/resource_edgecenter_securitygroup.go new file mode 100644 index 00000000..6c73156e --- /dev/null +++ b/edgecenter/resource_edgecenter_securitygroup.go @@ -0,0 +1,482 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygrouprules" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygroups" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/types" +) + +const ( + SecurityGroupPoint = "securitygroups" + securityGroupRulesPoint = "securitygrouprules" +) + +func resourceSecurityGroup() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSecurityGroupCreate, + ReadContext: resourceSecurityGroupRead, + UpdateContext: resourceSecurityGroupUpdate, + DeleteContext: resourceSecurityGroupDelete, + Description: "Represent SecurityGroups(Firewall)", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, sgID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(sgID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the security group.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "A detailed description of the security group.", + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "security_group_rules": { + Type: schema.TypeSet, + Required: true, + Description: "Firewall rules control what inbound(ingress) and outbound(egress) traffic is allowed to enter or leave a Instance. At least one 'egress' rule should be set", + Set: secGroupUniqueID, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + }, + "direction": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available value is '%s', '%s'", types.RuleDirectionIngress, types.RuleDirectionEgress), + ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics { + val := v.(string) + switch types.RuleDirection(val) { + case types.RuleDirectionIngress, types.RuleDirectionEgress: + return nil + } + return diag.Errorf("wrong direction '%s', available value is '%s', '%s'", val, types.RuleDirectionIngress, types.RuleDirectionEgress) + }, + }, + "ethertype": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available value is '%s', '%s'", types.EtherTypeIPv4, types.EtherTypeIPv6), + ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics { + val := v.(string) + switch types.EtherType(val) { + case types.EtherTypeIPv4, types.EtherTypeIPv6: + return nil + } + return diag.Errorf("wrong ethertype '%s', available value is '%s', '%s'", val, types.EtherTypeIPv4, types.EtherTypeIPv6) + }, + }, + "protocol": { + Type: schema.TypeString, + Required: true, + Description: fmt.Sprintf("Available value is %s", strings.Join(types.Protocol("").StringList(), ",")), + }, + "port_range_min": { + Type: schema.TypeInt, + Optional: true, + Default: 1, + ValidateFunc: validation.IntBetween(1, 65535), + }, + "port_range_max": { + Type: schema.TypeInt, + Optional: true, + Default: 65535, + ValidateFunc: validation.IntBetween(1, 65535), + }, + "description": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "remote_ip_prefix": { + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceSecurityGroupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start SecurityGroup creating") + + var valid bool + vals := d.Get("security_group_rules").(*schema.Set).List() + for _, val := range vals { + rule := val.(map[string]interface{}) + if types.RuleDirection(rule["direction"].(string)) == types.RuleDirectionEgress { + valid = true + break + } + } + if !valid { + return diag.Errorf("at least one 'egress' rule should be set") + } + + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + rawRules := d.Get("security_group_rules").(*schema.Set).List() + rules := make([]securitygroups.CreateSecurityGroupRuleOpts, len(rawRules)) + for i, r := range rawRules { + rule := r.(map[string]interface{}) + + descr := rule["description"].(string) + remoteIPPrefix := rule["remote_ip_prefix"].(string) + + sgrOpts := securitygroups.CreateSecurityGroupRuleOpts{ + Direction: types.RuleDirection(rule["direction"].(string)), + EtherType: types.EtherType(rule["ethertype"].(string)), + Protocol: types.Protocol(rule["protocol"].(string)), + Description: &descr, + } + + if remoteIPPrefix != "" { + sgrOpts.RemoteIPPrefix = &remoteIPPrefix + } + + portRangeMin := rule["port_range_min"].(int) + portRangeMax := rule["port_range_max"].(int) + + if portRangeMin > portRangeMax { + return diag.FromErr(fmt.Errorf("value of the port_range_min cannot be greater than port_range_max")) + } + + sgrOpts.PortRangeMax = &portRangeMax + sgrOpts.PortRangeMin = &portRangeMin + + rules[i] = sgrOpts + } + + createSecurityGroupOpts := &securitygroups.CreateSecurityGroupOpts{} + createSecurityGroupOpts.Name = d.Get("name").(string) + createSecurityGroupOpts.SecurityGroupRules = rules + + if metadataRaw, ok := d.GetOk("metadata_map"); ok { + createSecurityGroupOpts.Metadata = metadataRaw.(map[string]interface{}) + } + + opts := securitygroups.CreateOpts{ + SecurityGroup: *createSecurityGroupOpts, + } + descr := d.Get("description").(string) + if descr != "" { + opts.SecurityGroup.Description = &descr + } + + sg, err := securitygroups.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(sg.ID) + + resourceSecurityGroupRead(ctx, d, m) + log.Printf("[DEBUG] Finish SecurityGroup creating (%s)", sg.ID) + + return diags +} + +func resourceSecurityGroupRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start SecurityGroup reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + sg, err := securitygroups.Get(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("project_id", sg.ProjectID) + d.Set("region_id", sg.RegionID) + d.Set("name", sg.Name) + d.Set("description", sg.Description) + + metadataMap := make(map[string]string) + metadataReadOnly := make([]map[string]interface{}, 0, len(sg.Metadata)) + + if len(sg.Metadata) > 0 { + for _, metadataItem := range sg.Metadata { + metadataMap[metadataItem.Key] = metadataItem.Value + metadataReadOnly = append(metadataReadOnly, map[string]interface{}{ + "key": metadataItem.Key, + "value": metadataItem.Value, + "read_only": metadataItem.ReadOnly, + }) + } + } + + if err := d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + if err := d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + newSgRules := make([]interface{}, len(sg.SecurityGroupRules)) + for i, sgr := range sg.SecurityGroupRules { + log.Printf("rules: %+v", sgr) + r := make(map[string]interface{}) + r["id"] = sgr.ID + r["direction"] = sgr.Direction.String() + + if sgr.EtherType != nil { + r["ethertype"] = sgr.EtherType.String() + } + + r["protocol"] = types.ProtocolAny + if sgr.Protocol != nil { + r["protocol"] = sgr.Protocol.String() + } + + r["port_range_max"] = 65535 + if sgr.PortRangeMax != nil { + r["port_range_max"] = *sgr.PortRangeMax + } + r["port_range_min"] = 1 + if sgr.PortRangeMin != nil { + r["port_range_min"] = *sgr.PortRangeMin + } + + r["description"] = "" + if sgr.Description != nil { + r["description"] = *sgr.Description + } + + r["remote_ip_prefix"] = "" + if sgr.RemoteIPPrefix != nil { + r["remote_ip_prefix"] = *sgr.RemoteIPPrefix + } + + r["updated_at"] = sgr.UpdatedAt.String() + r["created_at"] = sgr.CreatedAt.String() + + newSgRules[i] = r + } + + if err := d.Set("security_group_rules", schema.NewSet(secGroupUniqueID, newSgRules)); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish SecurityGroup reading") + + return diags +} + +func resourceSecurityGroupUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start SecurityGroup updating") + var valid bool + vals := d.Get("security_group_rules").(*schema.Set).List() + for _, val := range vals { + rule := val.(map[string]interface{}) + if types.RuleDirection(rule["direction"].(string)) == types.RuleDirectionEgress { + valid = true + break + } + } + if !valid { + return diag.Errorf("at least one 'egress' rule should be set") + } + + config := m.(*Config) + provider := config.Provider + clientCreate, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + clientUpdateDelete, err := CreateClient(provider, d, securityGroupRulesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + gid := d.Id() + + if d.HasChange("security_group_rules") { + oldRulesRaw, newRulesRaw := d.GetChange("security_group_rules") + oldRules := oldRulesRaw.(*schema.Set) + newRules := newRulesRaw.(*schema.Set) + + changedRule := make(map[string]bool) + for _, r := range newRules.List() { + rule := r.(map[string]interface{}) + rid := rule["id"].(string) + if !oldRules.Contains(r) && rid == "" { + opts := extractSecurityGroupRuleMap(r, gid) + _, err := securitygroups.AddRule(clientCreate, gid, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + continue + } + if rid != "" && !oldRules.Contains(r) { + changedRule[rid] = true + opts := extractSecurityGroupRuleMap(r, gid) + _, err := securitygrouprules.Replace(clientUpdateDelete, rid, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + } + } + + for _, r := range oldRules.List() { + rule := r.(map[string]interface{}) + rid := rule["id"].(string) + if !newRules.Contains(r) && !changedRule[rid] { + // todo patch lib, should be task instead of DeleteResult + err := securitygrouprules.Delete(clientUpdateDelete, rid).ExtractErr() + if err != nil { + return diag.FromErr(err) + } + // todo remove after patch lib + time.Sleep(time.Second * 2) + continue + } + } + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + + err := securitygroups.MetadataReplace(clientCreate, gid, nmd.(map[string]interface{})).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish SecurityGroup updating") + + return resourceSecurityGroupRead(ctx, d, m) +} + +func resourceSecurityGroupDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start SecurityGroup deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + sgID := d.Id() + + client, err := CreateClient(provider, d, SecurityGroupPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + err = securitygroups.Delete(client, sgID).Err + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of SecurityGroup deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_servergroup.go b/edgecenter/resource_edgecenter_servergroup.go new file mode 100644 index 00000000..36d2b667 --- /dev/null +++ b/edgecenter/resource_edgecenter_servergroup.go @@ -0,0 +1,184 @@ +package edgecenter + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/servergroup/v1/servergroups" +) + +const ( + ServerGroupsPoint = "servergroups" +) + +func resourceServerGroup() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceServerGroupCreate, + ReadContext: resourceServerGroupRead, + DeleteContext: resourceServerGroupDelete, + Description: "Represent server group resource", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, sgID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(sgID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Description: "Displayed server group name", + Required: true, + ForceNew: true, + }, + "policy": { + Type: schema.TypeString, + Description: "Server group policy. Available value is 'affinity', 'anti-affinity'", + Required: true, + ForceNew: true, + }, + "instances": { + Type: schema.TypeList, + Description: "Instances in this server group", + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "instance_id": { + Type: schema.TypeString, + Computed: true, + }, + "instance_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceServerGroupCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ServerGroup creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ServerGroupsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := servergroups.CreateOpts{ + Name: d.Get("name").(string), + Policy: servergroups.ServerGroupPolicy(d.Get("policy").(string)), + } + + serverGroup, err := servergroups.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(serverGroup.ServerGroupID) + resourceServerGroupRead(ctx, d, m) + log.Println("[DEBUG] Finish ServerGroup creating") + + return diags +} + +func resourceServerGroupRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ServerGroup reading") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ServerGroupsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + serverGroup, err := servergroups.Get(client, d.Id()).Extract() + if err != nil { + return diag.FromErr(err) + } + + d.Set("name", serverGroup.Name) + d.Set("project_id", serverGroup.ProjectID) + d.Set("region_id", serverGroup.RegionID) + d.Set("policy", serverGroup.Policy.String()) + + instances := make([]map[string]string, len(serverGroup.Instances)) + for i, instance := range serverGroup.Instances { + rawInstance := make(map[string]string) + rawInstance["instance_id"] = instance.InstanceID + rawInstance["instance_name"] = instance.InstanceName + instances[i] = rawInstance + } + if err := d.Set("instances", instances); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish ServerGroup reading") + + return diags +} + +func resourceServerGroupDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start ServerGroup deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, ServerGroupsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + err = servergroups.Delete(client, d.Id()).ExtractErr() + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Println("[DEBUG] Finish ServerGroup deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_snapshot.go b/edgecenter/resource_edgecenter_snapshot.go new file mode 100644 index 00000000..c3274b6d --- /dev/null +++ b/edgecenter/resource_edgecenter_snapshot.go @@ -0,0 +1,279 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/snapshot/v1/snapshots" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +const ( + snapshotDeleting int = 1200 + snapshotCreatingTimeout int = 1200 + SnapshotsPoint = "snapshots" +) + +func resourceSnapshot() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSnapshotCreate, + ReadContext: resourceSnapshotRead, + UpdateContext: resourceSnapshotUpdate, + DeleteContext: resourceSnapshotDelete, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, snapshotID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(snapshotID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the snapshot.", + }, + "size": { + Type: schema.TypeInt, + Computed: true, + Description: "The size of the snapshot in GB.", + }, + "status": { + Type: schema.TypeString, + Computed: true, + Description: "The current status of the snapshot.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "A detailed description of the snapshot.", + }, + "volume_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The ID of the volume from which the snapshot was created.", + }, + "metadata": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceSnapshotCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start snapshot creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SnapshotsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := getSnapshotData(d) + results, err := snapshots.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + SnapshotID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, snapshotCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + snapshotID, err := snapshots.ExtractSnapshotIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve snapshot ID from task info: %w", err) + } + return snapshotID, nil + }, + ) + log.Printf("[DEBUG] Snapshot id (%s)", SnapshotID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(SnapshotID.(string)) + resourceSnapshotRead(ctx, d, m) + + log.Printf("[DEBUG] Finish snapshot creating (%s)", SnapshotID) + + return diags +} + +func resourceSnapshotRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start snapshot reading") + log.Printf("[DEBUG] Start snapshot reading %s", d.State()) + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + snapshotID := d.Id() + log.Printf("[DEBUG] Snapshot id = %s", snapshotID) + + client, err := CreateClient(provider, d, SnapshotsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + snapshot, err := snapshots.Get(client, snapshotID).Extract() + if err != nil { + return diag.Errorf("cannot get snapshot with ID: %s. Error: %s", snapshotID, err) + } + + d.Set("name", snapshot.Name) + d.Set("description", snapshot.Description) + d.Set("status", snapshot.Status) + d.Set("size", snapshot.Size) + d.Set("volume_id", snapshot.VolumeID) + d.Set("region_id", snapshot.RegionID) + d.Set("project_id", snapshot.ProjectID) + if err := d.Set("metadata", snapshot.Metadata); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish snapshot reading") + + return diags +} + +func resourceSnapshotUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start snapshot updating") + snapshotID := d.Id() + if d.HasChange("metadata") { + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, SnapshotsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + newMeta := prepareRawMetadata(d.Get("metadata").(map[string]interface{})) + metadata := make([]snapshots.MetadataOpts, 0, len(newMeta)) + for k, v := range newMeta { + metadata = append(metadata, snapshots.MetadataOpts{Key: k, Value: v}) + } + opts := snapshots.MetadataSetOpts{Metadata: metadata} + if _, err := snapshots.MetadataReplace(client, snapshotID, opts).Extract(); err != nil { + return diag.FromErr(err) + } + } + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish snapshot updating") + + return resourceSnapshotRead(ctx, d, m) +} + +func resourceSnapshotDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start snapshot deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + snapshotID := d.Id() + log.Printf("[DEBUG] Snapshot id = %s", snapshotID) + + client, err := CreateClient(provider, d, SnapshotsPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + results, err := snapshots.Delete(client, snapshotID).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, snapshotDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := snapshots.Get(client, snapshotID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete snapshot with ID: %s", snapshotID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Shapshot resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of snapshot deleting") + + return diags +} + +func getSnapshotData(d *schema.ResourceData) *snapshots.CreateOpts { + snapshotData := snapshots.CreateOpts{} + snapshotData.Name = d.Get("name").(string) + snapshotData.VolumeID = d.Get("volume_id").(string) + snapshotData.Description = d.Get("description").(string) + metadataRaw := d.Get("metadata").(map[string]interface{}) + if len(metadataRaw) > 0 { + snapshotData.Metadata = prepareRawMetadata(metadataRaw) + } + + return &snapshotData +} + +func prepareRawMetadata(raw map[string]interface{}) map[string]string { + meta := make(map[string]string, len(raw)) + for k, v := range raw { + meta[k] = v.(string) + } + return meta +} diff --git a/edgecenter/resource_edgecenter_storage_s3.go b/edgecenter/resource_edgecenter_storage_s3.go new file mode 100644 index 00000000..cf9de633 --- /dev/null +++ b/edgecenter/resource_edgecenter_storage_s3.go @@ -0,0 +1,242 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "regexp" + "strconv" + "strings" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecenter-storage-sdk-go/swagger/client/storages" +) + +const ( + StorageS3SchemaGenerateAccessKey = "generated_access_key" + StorageS3SchemaGenerateSecretKey = "generated_secret_key" + StorageSchemaGenerateHTTPEndpoint = "generated_http_endpoint" + StorageSchemaGenerateS3Endpoint = "generated_s3_endpoint" + StorageSchemaGenerateEndpoint = "generated_endpoint" + + StorageSchemaLocation = "location" + StorageSchemaName = "name" + StorageSchemaID = "storage_id" + StorageSchemaClientID = "client_id" +) + +func resourceStorageS3() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + StorageSchemaID: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "An id of new storage resource.", + }, + StorageSchemaClientID: { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "An client id of new storage resource.", + }, + StorageSchemaName: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + storageName := i.(string) + if !regexp.MustCompile(`^[\w\-]+$`).MatchString(storageName) || len(storageName) > 255 { + return diag.Errorf("storage name can't be empty and can have only letters, numbers, dashes and underscores, it also should be less than 256 symbols") + } + return nil + }, + Description: "A name of new storage resource.", + }, + StorageSchemaLocation: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(v interface{}, path cty.Path) diag.Diagnostics { + val := v.(string) + allowed := []string{"s-ed1", "s-darz1", "s-ws1", "s-dt2", "s-drc2"} + for _, el := range allowed { + if el == val { + return nil + } + } + return diag.Errorf(`must be one of %+v`, allowed) + }, + Description: "A location of new storage resource. One of (s-ed1, s-darz1, s-ws1, s-dt2, s-drc2)", + }, + StorageS3SchemaGenerateAccessKey: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A s3 access key for new storage resource.", + }, + StorageS3SchemaGenerateSecretKey: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A s3 secret key for new storage resource.", + }, + StorageSchemaGenerateHTTPEndpoint: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A http s3 entry point for new storage resource.", + }, + StorageSchemaGenerateS3Endpoint: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A s3 endpoint for new storage resource.", + }, + StorageSchemaGenerateEndpoint: { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "A s3 entry point for new storage resource.", + }, + }, + CreateContext: resourceStorageS3Create, + ReadContext: resourceStorageS3Read, + DeleteContext: resourceStorageS3Delete, + Description: "Represent s3 storage resource. https://storage.edgecenter.ru/storage/list", + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceStorageS3Create(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + id := new(int) + log.Println("[DEBUG] Start S3 Storage Resource creating") + defer log.Printf("[DEBUG] Finish S3 Storage Resource creating (id=%d)\n", *id) + config := m.(*Config) + client := config.StorageClient + + opts := []func(opt *storages.StorageCreateHTTPParams){ + func(opt *storages.StorageCreateHTTPParams) { opt.Context = ctx }, + func(opt *storages.StorageCreateHTTPParams) { opt.Body.Type = "s3" }, + } + location := strings.TrimSpace(d.Get(StorageSchemaLocation).(string)) + if location != "" { + opts = append(opts, func(opt *storages.StorageCreateHTTPParams) { opt.Body.Location = location }) + } + name := strings.TrimSpace(d.Get(StorageSchemaName).(string)) + if name != "" { + opts = append(opts, func(opt *storages.StorageCreateHTTPParams) { opt.Body.Name = name }) + } + + result, err := client.CreateStorage(opts...) + if err != nil { + return diag.FromErr(fmt.Errorf("create storage: %w", err)) + } + d.SetId(fmt.Sprintf("%d", result.ID)) + *id = int(result.ID) + if result.Credentials.S3.AccessKey != "" { + _ = d.Set(StorageS3SchemaGenerateAccessKey, result.Credentials.S3.AccessKey) + } + if result.Credentials.S3.SecretKey != "" { + _ = d.Set(StorageS3SchemaGenerateSecretKey, result.Credentials.S3.SecretKey) + } + + return resourceStorageS3Read(ctx, d, m) +} + +func resourceStorageS3Read(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + resourceID := storageResourceID(d) + log.Printf("[DEBUG] Start S3 Storage Resource reading (id=%s)\n", resourceID) + defer log.Println("[DEBUG] Finish S3 Storage Resource reading") + + config := m.(*Config) + client := config.StorageClient + + opts := []func(opt *storages.StorageListHTTPV2Params){ + func(opt *storages.StorageListHTTPV2Params) { opt.Context = ctx }, + func(opt *storages.StorageListHTTPV2Params) { opt.ShowDeleted = new(bool) }, + } + if resourceID != "" { + opts = append(opts, func(opt *storages.StorageListHTTPV2Params) { opt.ID = &resourceID }) + } + name := d.Get(StorageSchemaName).(string) + if name != "" { + opts = append(opts, func(opt *storages.StorageListHTTPV2Params) { opt.Name = &name }) + } + if resourceID == "" && name == "" { + return diag.Errorf("get storage: empty storage id/name") + } + + result, err := client.StoragesList(opts...) + if err != nil { + return diag.FromErr(fmt.Errorf("storages list: %w", err)) + } + + if (len(result) == 0) || (name == "" && len(result) != 1) { + return diag.Errorf("get storage: wrong length of search result (%d), want 1", len(result)) + } + st := result[0] + + d.SetId(fmt.Sprint(st.ID)) + nameParts := strings.Split(st.Name, "-") + if len(nameParts) > 1 { + clientID, _ := strconv.ParseInt(nameParts[0], 10, 64) + _ = d.Set(StorageSchemaClientID, int(clientID)) + _ = d.Set(StorageSchemaName, strings.Join(nameParts[1:], "-")) + } else { + _ = d.Set(StorageSchemaName, st.Name) + } + _ = d.Set(StorageSchemaID, st.ID) + _ = d.Set(StorageSchemaLocation, st.Location) + _ = d.Set(StorageSchemaGenerateEndpoint, fmt.Sprintf("%s.cloud.edgecenter.ru/%s", st.Location, st.Name)) + _ = d.Set(StorageSchemaGenerateHTTPEndpoint, fmt.Sprintf("https://%s.cloud.edgecenter.ru/{bucket_name}", st.Location)) + _ = d.Set(StorageSchemaGenerateS3Endpoint, fmt.Sprintf("https://%s.cloud.edgecenter.ru", st.Location)) + + return nil +} + +func resourceStorageS3Delete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + resourceID := storageResourceID(d) + log.Printf("[DEBUG] Start S3 Storage Resource deleting (id=%s)\n", resourceID) + defer log.Println("[DEBUG] Finish S3 Storage Resource deleting") + if resourceID == "" { + return diag.Errorf("empty storage id") + } + + config := m.(*Config) + client := config.StorageClient + + id, err := strconv.ParseInt(resourceID, 10, 64) + if err != nil { + return diag.FromErr(fmt.Errorf("get resource id: %w", err)) + } + + opts := []func(opt *storages.StorageDeleteHTTPParams){ + func(opt *storages.StorageDeleteHTTPParams) { opt.Context = ctx }, + func(opt *storages.StorageDeleteHTTPParams) { opt.ID = id }, + } + err = client.DeleteStorage(opts...) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return nil +} + +func storageResourceID(d *schema.ResourceData) string { + resourceID := d.Id() + if resourceID == "" { + id := d.Get(StorageSchemaID).(int) + if id > 0 { + resourceID = fmt.Sprint(id) + } + } + return resourceID +} diff --git a/edgecenter/resource_edgecenter_storage_s3_bucket.go b/edgecenter/resource_edgecenter_storage_s3_bucket.go new file mode 100644 index 00000000..fdca2f7d --- /dev/null +++ b/edgecenter/resource_edgecenter_storage_s3_bucket.go @@ -0,0 +1,161 @@ +package edgecenter + +import ( + "context" + "fmt" + "log" + "regexp" + "strconv" + "strings" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecenter-storage-sdk-go/swagger/client/buckets" +) + +const ( + StorageS3BucketSchemaName = "name" + StorageS3BucketSchemaStorageID = "storage_id" +) + +func resourceStorageS3Bucket() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + StorageS3BucketSchemaStorageID: { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "An id of existing storage resource.", + }, + StorageS3BucketSchemaName: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { + storageName := i.(string) + if !regexp.MustCompile(`^[\w\-]+$`).MatchString(storageName) || + len(storageName) > 63 || + len(storageName) < 3 { + return diag.Errorf("bucket name can't be empty and can have only letters & numbers. it also should be less than 63 symbols") + } + return nil + }, + Description: "A name of new storage bucket resource.", + }, + }, + CreateContext: resourceStorageS3BucketCreate, + ReadContext: resourceStorageS3BucketRead, + DeleteContext: resourceStorageS3BucketDelete, + Description: "Represent s3 storage bucket resource. https://storage.edgecenter.ru/storage/list", + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + } +} + +func resourceStorageS3BucketCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + id := d.Get(StorageSchemaID).(int) + log.Println("[DEBUG] Start S3 Storage Bucket Resource creating") + defer log.Printf("[DEBUG] Finish S3 Storage Bucket Resource creating (id=%d)\n", id) + config := m.(*Config) + client := config.StorageClient + + opts := []func(opt *buckets.StorageBucketCreateHTTPParams){ + func(opt *buckets.StorageBucketCreateHTTPParams) { + opt.Context = ctx + opt.ID = int64(id) + }, + } + name := strings.TrimSpace(d.Get(StorageS3BucketSchemaName).(string)) + if name != "" { + opts = append(opts, func(opt *buckets.StorageBucketCreateHTTPParams) { opt.Name = name }) + } + + err := client.CreateBucket(opts...) + if err != nil { + return diag.FromErr(fmt.Errorf("create storage bucket: %w", err)) + } + d.SetId(fmt.Sprintf("%d:%s", id, name)) + + return resourceStorageS3BucketRead(ctx, d, m) +} + +func resourceStorageS3BucketRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + storageID, bucketName := storageBucketResourceID(d) + log.Printf("[DEBUG] Start S3 Storage Bucket Resource reading (id=%d, name=%s)\n", storageID, bucketName) + defer log.Println("[DEBUG] Finish S3 Storage Bucket Resource reading") + + config := m.(*Config) + client := config.StorageClient + + opts := []func(opt *buckets.StorageListBucketsHTTPParams){ + func(opt *buckets.StorageListBucketsHTTPParams) { opt.Context = ctx }, + func(opt *buckets.StorageListBucketsHTTPParams) { opt.ID = int64(storageID) }, + } + + result, err := client.BucketsList(opts...) + if err != nil { + return diag.FromErr(fmt.Errorf("storage buckets list: %w", err)) + } + if len(result) == 0 { + return diag.Errorf("get buckets: wrong length of search result (%d), want more", len(result)) + } + for _, bucket := range result { + if bucket.Name == bucketName { + d.SetId(fmt.Sprintf("%d:%s", storageID, bucketName)) + _ = d.Set(StorageS3BucketSchemaStorageID, storageID) + _ = d.Set(StorageS3BucketSchemaName, bucketName) + return nil + } + } + + return diag.FromErr(fmt.Errorf("storage buckets list has not this bucket")) +} + +func resourceStorageS3BucketDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + storageID, bucketName := storageBucketResourceID(d) + log.Printf("[DEBUG] Start S3 Storage Bucket Resource deleting (id=%d,name=%s)\n", storageID, bucketName) + defer log.Println("[DEBUG] Finish S3 Storage Bucket Resource deleting") + if bucketName == "" { + return diag.Errorf("empty bucket") + } + + config := m.(*Config) + client := config.StorageClient + + opts := []func(opt *buckets.StorageBucketRemoveHTTPParams){ + func(opt *buckets.StorageBucketRemoveHTTPParams) { opt.Context = ctx }, + func(opt *buckets.StorageBucketRemoveHTTPParams) { opt.ID = int64(storageID) }, + func(opt *buckets.StorageBucketRemoveHTTPParams) { opt.Name = bucketName }, + } + err := client.DeleteBucket(opts...) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + + return nil +} + +func storageBucketResourceID(d *schema.ResourceData) (int, string) { + var storageID int + var bucketName string + resourceID := d.Id() + if resourceID == "" { + storageID = d.Get(StorageS3BucketSchemaStorageID).(int) + bucketName = strings.TrimSpace(d.Get(StorageS3BucketSchemaName).(string)) + return storageID, bucketName + } + parts := strings.Split(resourceID, ":") + if len(parts) != 2 { + return storageID, bucketName + } + id, _ := strconv.ParseInt(parts[0], 10, 64) + storageID = int(id) + bucketName = parts[1] + + return storageID, bucketName +} diff --git a/edgecenter/resource_edgecenter_subnet.go b/edgecenter/resource_edgecenter_subnet.go new file mode 100644 index 00000000..4c0c6b30 --- /dev/null +++ b/edgecenter/resource_edgecenter_subnet.go @@ -0,0 +1,455 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "net" + "regexp" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" +) + +const ( + SubnetDeleting int = 1200 + SubnetCreatingTimeout int = 1200 + SubnetPoint = "subnets" + disable = "disable" +) + +func resourceSubnet() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceSubnetCreate, + ReadContext: resourceSubnetRead, + UpdateContext: resourceSubnetUpdate, + DeleteContext: resourceSubnetDelete, + Description: "Represent subnets. Subnetwork is a range of IP addresses in a cloud network. Addresses from this range will be assigned to machines in the cloud", + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, subnetID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(subnetID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the subnet.", + }, + "enable_dhcp": { + Type: schema.TypeBool, + Optional: true, + Computed: true, + Description: "Enable DHCP for this subnet. If true, DHCP will be used to assign IP addresses to instances within this subnet.", + }, + "cidr": { + Type: schema.TypeString, + Required: true, + Description: "Represents the IP address range of the subnet.", + }, + "network_id": { + Type: schema.TypeString, + Required: true, + Description: "The ID of the network to which this subnet belongs.", + }, + "connect_to_network_router": { + Type: schema.TypeBool, + Description: "True if the network's router should get a gateway in this subnet. Must be explicitly 'false' when gateway_ip is null. Default true.", + Optional: true, + Default: true, + }, + "dns_nameservers": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: "List of DNS name servers for the subnet.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "host_routes": { + Type: schema.TypeList, + Optional: true, + Description: "List of additional routes to be added to instances that are part of this subnet.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination": { + Type: schema.TypeString, + Required: true, + }, + "nexthop": { + Type: schema.TypeString, + Required: true, + Description: "IPv4 address to forward traffic to if it's destination IP matches 'destination' CIDR", + }, + }, + }, + }, + "gateway_ip": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The IP address of the gateway for this subnet.", + ValidateDiagFunc: func(val interface{}, key cty.Path) diag.Diagnostics { + v := val.(string) + IP := regexp.MustCompile(`(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`) + if v == disable || IP.MatchString(v) { + return nil + } + return diag.FromErr(fmt.Errorf("%q must be a valid ip, got: %s", key, v)) + }, + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + }, + } +} + +func resourceSubnetCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start Subnet creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, SubnetPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + createOpts := subnets.CreateOpts{} + + var eccidr edgecloud.CIDR + cidr := d.Get("cidr").(string) + if cidr != "" { + _, netIPNet, err := net.ParseCIDR(cidr) + if err != nil { + return diag.FromErr(err) + } + eccidr.IP = netIPNet.IP + eccidr.Mask = netIPNet.Mask + createOpts.CIDR = eccidr + } + + dnsNameservers := d.Get("dns_nameservers").([]interface{}) + createOpts.DNSNameservers = make([]net.IP, 0) + if len(dnsNameservers) > 0 { + ns := dnsNameservers + dns := make([]net.IP, len(ns)) + for i, s := range ns { + dns[i] = net.ParseIP(s.(string)) + } + createOpts.DNSNameservers = dns + } + + hostRoutes := d.Get("host_routes").([]interface{}) + createOpts.HostRoutes = make([]subnets.HostRoute, 0) + if len(hostRoutes) > 0 { + createOpts.HostRoutes, err = extractHostRoutesMap(hostRoutes) + if err != nil { + return diag.FromErr(err) + } + } + + createOpts.Name = d.Get("name").(string) + createOpts.EnableDHCP = d.Get("enable_dhcp").(bool) + createOpts.NetworkID = d.Get("network_id").(string) + createOpts.ConnectToNetworkRouter = d.Get("connect_to_network_router").(bool) + gatewayIP := d.Get("gateway_ip").(string) + gw := net.ParseIP(gatewayIP) + if gatewayIP == disable { + createOpts.ConnectToNetworkRouter = false + } else { + createOpts.GatewayIP = &gw + } + + if metadataRaw, ok := d.GetOk("metadata_map"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return diag.FromErr(err) + } + createOpts.Metadata = meta + } + + log.Printf("Create subnet ops: %+v", createOpts) + results, err := subnets.Create(client, createOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + subnetID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, SubnetCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Subnet, err := subnets.ExtractSubnetIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Subnet ID from task info: %w", err) + } + return Subnet, nil + }, + ) + log.Printf("[DEBUG] Subnet id (%s)", subnetID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(subnetID.(string)) + resourceSubnetRead(ctx, d, m) + + log.Printf("[DEBUG] Finish Subnet creating (%s)", subnetID) + + return diags +} + +func resourceSubnetRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start subnet reading") + log.Printf("[DEBUG] Start subnet reading%s", d.State()) + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + subnetID := d.Id() + log.Printf("[DEBUG] Subnet id = %s", subnetID) + + client, err := CreateClient(provider, d, SubnetPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + subnet, err := subnets.Get(client, subnetID).Extract() + if err != nil { + return diag.Errorf("cannot get subnet with ID: %s. Error: %s", subnetID, err) + } + + d.Set("name", subnet.Name) + d.Set("enable_dhcp", subnet.EnableDHCP) + d.Set("cidr", subnet.CIDR.String()) + d.Set("network_id", subnet.NetworkID) + + dns := make([]string, len(subnet.DNSNameservers)) + for i, ns := range subnet.DNSNameservers { + dns[i] = ns.String() + } + d.Set("dns_nameservers", dns) + + hrs := make([]map[string]string, len(subnet.HostRoutes)) + for i, hr := range subnet.HostRoutes { + hR := map[string]string{"destination": "", "nexthop": ""} + hR["destination"] = hr.Destination.String() + hR["nexthop"] = hr.NextHop.String() + hrs[i] = hR + } + d.Set("host_routes", hrs) + d.Set("region_id", subnet.RegionID) + d.Set("project_id", subnet.ProjectID) + d.Set("gateway_ip", subnet.GatewayIP.String()) + + fields := []string{"connect_to_network_router"} + revertState(d, &fields) + + if subnet.GatewayIP == nil { + d.Set("connect_to_network_router", false) + d.Set("gateway_ip", disable) + } + + metadataMap, metadataReadOnly := PrepareMetadata(subnet.Metadata) + + if err = d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + log.Println("[DEBUG] Finish subnet reading") + + return diags +} + +func resourceSubnetUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start subnet updating") + subnetID := d.Id() + log.Printf("[DEBUG] Subnet id = %s", subnetID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, SubnetPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + updateOpts := subnets.UpdateOpts{} + + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + updateOpts.EnableDHCP = d.Get("enable_dhcp").(bool) + + // In the structure, the field is mandatory for the ability to transfer the absence of data, + // if you do not initialize it with a empty list, marshalling will send null and receive a validation error. + dnsNameservers := d.Get("dns_nameservers").([]interface{}) + updateOpts.DNSNameservers = make([]net.IP, 0) + if len(dnsNameservers) > 0 { + ns := dnsNameservers + dns := make([]net.IP, len(ns)) + for i, s := range ns { + dns[i] = net.ParseIP(s.(string)) + } + updateOpts.DNSNameservers = dns + } + + hostRoutes := d.Get("host_routes").([]interface{}) + updateOpts.HostRoutes = make([]subnets.HostRoute, 0) + if len(hostRoutes) > 0 { + updateOpts.HostRoutes, err = extractHostRoutesMap(hostRoutes) + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("gateway_ip") { + _, newValue := d.GetChange("gateway_ip") + if newValue.(string) != disable { + gatewayIP := net.ParseIP(newValue.(string)) + updateOpts.GatewayIP = &gatewayIP + } + } + + _, err = subnets.Update(client, subnetID, updateOpts).Extract() + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + meta, err := utils.MapInterfaceToMapString(nmd) + if err != nil { + return diag.Errorf("metadata wrong fmt. Error: %s", err) + } + err = metadata.ResourceMetadataReplace(client, subnetID, meta).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish subnet updating") + + return resourceSubnetRead(ctx, d, m) +} + +func resourceSubnetDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start subnet deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + subnetID := d.Id() + log.Printf("[DEBUG] Subnet id = %s", subnetID) + + client, err := CreateClient(provider, d, SubnetPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + results, err := subnets.Delete(client, subnetID).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, SubnetDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := subnets.Get(client, subnetID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete subnet with ID: %s", subnetID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Subnet resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of subnet deleting") + + return diags +} diff --git a/edgecenter/resource_edgecenter_volume.go b/edgecenter/resource_edgecenter_volume.go new file mode 100644 index 00000000..7c589456 --- /dev/null +++ b/edgecenter/resource_edgecenter_volume.go @@ -0,0 +1,430 @@ +package edgecenter + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/utils/metadata" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" +) + +const ( + volumeDeleting int = 1200 + VolumeCreatingTimeout int = 1200 + volumeExtending int = 1200 + VolumesPoint = "volumes" +) + +func resourceVolume() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceVolumeCreate, + ReadContext: resourceVolumeRead, + UpdateContext: resourceVolumeUpdate, + DeleteContext: resourceVolumeDelete, + Description: `A volume is a detachable block storage device akin to a USB hard drive or SSD, but located remotely in the cloud. +Volumes can be attached to a virtual machine and manipulated like a physical hard drive.`, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + projectID, regionID, volumeID, err := ImportStringParser(d.Id()) + if err != nil { + return nil, err + } + d.Set("project_id", projectID) + d.Set("region_id", regionID) + d.SetId(volumeID) + + config := meta.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return nil, err + } + + volume, err := volumes.Get(client, volumeID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get volume with ID: %s. Error: %w", volumeID, err) + } + d.Set("image_id", volume.VolumeImageMetadata.ImageID) + + return []*schema.ResourceData{d}, nil + }, + }, + + Schema: map[string]*schema.Schema{ + "project_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "project_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the project. Either 'project_id' or 'project_name' must be specified.", + ExactlyOneOf: []string{"project_id", "project_name"}, + }, + "region_id": { + Type: schema.TypeInt, + Optional: true, + Description: "The uuid of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "region_name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the region. Either 'region_id' or 'region_name' must be specified.", + ExactlyOneOf: []string{"region_id", "region_name"}, + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the volume.", + }, + "size": { + Type: schema.TypeInt, + Required: true, + Description: "The size of the volume, specified in gigabytes (GB).", + }, + "type_name": { + Type: schema.TypeString, + Optional: true, + Description: "The type of volume to create. Valid values are 'ssd_hiiops', 'standard', 'cold', and 'ultra'. Defaults to 'standard'.", + }, + "image_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "(ForceNew) The ID of the image to create the volume from. This field is mandatory if creating a volume from an image.", + }, + "snapshot_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "(ForceNew) The ID of the snapshot to create the volume from. This field is mandatory if creating a volume from a snapshot.", + }, + "last_updated": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "The timestamp of the last update (use with update context).", + }, + "metadata_map": { + Type: schema.TypeMap, + Optional: true, + Computed: true, + Description: "A map containing metadata, for example tags.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "metadata_read_only": { + Type: schema.TypeList, + Computed: true, + Description: `A list of read-only metadata items, e.g. tags.`, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key": { + Type: schema.TypeString, + Computed: true, + }, + "value": { + Type: schema.TypeString, + Computed: true, + }, + "read_only": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceVolumeCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start volume creating") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + + client, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts, err := getVolumeData(d) + if err != nil { + return diag.FromErr(err) + } + results, err := volumes.Create(client, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + VolumeID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, VolumeCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + volumeID, err := volumes.ExtractVolumeIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve volume ID from task info: %w", err) + } + return volumeID, nil + }, + ) + log.Printf("[DEBUG] Volume id (%s)", VolumeID) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(VolumeID.(string)) + resourceVolumeRead(ctx, d, m) + + log.Printf("[DEBUG] Finish volume creating (%s)", VolumeID) + + return diags +} + +func resourceVolumeRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start volume reading") + log.Printf("[DEBUG] Start volume reading%s", d.State()) + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + volumeID := d.Id() + log.Printf("[DEBUG] Volume id = %s", volumeID) + + client, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + volume, err := volumes.Get(client, volumeID).Extract() + if err != nil { + return diag.Errorf("cannot get volume with ID: %s. Error: %s", volumeID, err) + } + + d.Set("name", volume.Name) + d.Set("size", volume.Size) + d.Set("type_name", volume.VolumeType) + d.Set("region_id", volume.RegionID) + d.Set("project_id", volume.ProjectID) + + metadataMap, metadataReadOnly := PrepareMetadata(volume.Metadata) + + if err = d.Set("metadata_map", metadataMap); err != nil { + return diag.FromErr(err) + } + + if err = d.Set("metadata_read_only", metadataReadOnly); err != nil { + return diag.FromErr(err) + } + + fields := []string{"image_id", "snapshot_id"} + revertState(d, &fields) + + log.Println("[DEBUG] Finish volume reading") + + return diags +} + +func resourceVolumeUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start volume updating") + volumeID := d.Id() + log.Printf("[DEBUG] Volume id = %s", volumeID) + config := m.(*Config) + provider := config.Provider + client, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + volume, err := volumes.Get(client, volumeID).Extract() + if err != nil { + return diag.FromErr(err) + } + + if d.HasChange("name") { + name := d.Get("name").(string) + _, err := volumes.Update(client, volumeID, volumes.UpdateOpts{Name: name}).Extract() + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("size") { + newValue := d.Get("size") + newSize := newValue.(int) + if newSize != 0 { + if volume.Size < newSize { + err = ExtendVolume(client, volumeID, newSize) + if err != nil { + return diag.FromErr(err) + } + } else { + return diag.Errorf("Validation error: unable to update size field because new volume size must be greater than current size") + } + } + } + + if d.HasChange("type_name") { + newTN := d.Get("type_name") + newVolumeType, err := volumes.VolumeType(newTN.(string)).ValidOrNil() + if err != nil { + return diag.FromErr(err) + } + + opts := volumes.VolumeTypePropertyOperationOpts{ + VolumeType: *newVolumeType, + } + _, err = volumes.Retype(client, volumeID, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + } + + if d.HasChange("metadata_map") { + _, nmd := d.GetChange("metadata_map") + + meta, err := utils.MapInterfaceToMapString(nmd.(map[string]interface{})) + if err != nil { + return diag.Errorf("cannot get metadata. Error: %s", err) + } + + err = metadata.ResourceMetadataReplace(client, d.Id(), meta).Err + if err != nil { + return diag.Errorf("cannot update metadata. Error: %s", err) + } + } + + d.Set("last_updated", time.Now().Format(time.RFC850)) + log.Println("[DEBUG] Finish volume updating") + + return resourceVolumeRead(ctx, d, m) +} + +func resourceVolumeDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + log.Println("[DEBUG] Start volume deleting") + var diags diag.Diagnostics + config := m.(*Config) + provider := config.Provider + volumeID := d.Id() + log.Printf("[DEBUG] Volume id = %s", volumeID) + + client, err := CreateClient(provider, d, VolumesPoint, VersionPointV1) + if err != nil { + return diag.FromErr(err) + } + + opts := volumes.DeleteOpts{ + Snapshots: [](string){d.Get("snapshot_id").(string)}, + } + results, err := volumes.Delete(client, volumeID, opts).Extract() + if err != nil { + return diag.FromErr(err) + } + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, volumeDeleting, func(task tasks.TaskID) (interface{}, error) { + _, err := volumes.Get(client, volumeID).Extract() + if err == nil { + return nil, fmt.Errorf("cannot delete volume with ID: %s", volumeID) + } + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil, nil + } + return nil, fmt.Errorf("extracting Volume resource error: %w", err) + }) + if err != nil { + return diag.FromErr(err) + } + + d.SetId("") + log.Printf("[DEBUG] Finish of volume deleting") + + return diags +} + +func getVolumeData(d *schema.ResourceData) (*volumes.CreateOpts, error) { + volumeData := volumes.CreateOpts{} + volumeData.Source = volumes.NewVolume + volumeData.Name = d.Get("name").(string) + volumeData.Size = d.Get("size").(int) + + imageID := d.Get("image_id").(string) + if imageID != "" { + volumeData.Source = volumes.Image + volumeData.ImageID = imageID + } + + snapshotID := d.Get("snapshot_id").(string) + if snapshotID != "" { + volumeData.Source = volumes.Snapshot + volumeData.SnapshotID = snapshotID + if volumeData.Size != 0 { + log.Println("[DEBUG] Size must be equal or larger to respective snapshot size") + } + } + + typeName := d.Get("type_name").(string) + if typeName != "" { + modifiedTypeName, err := volumes.VolumeType(typeName).ValidOrNil() + if err != nil { + return nil, fmt.Errorf("checking Volume validation error: %w", err) + } + volumeData.TypeName = *modifiedTypeName + } + + if metadataRaw, ok := d.GetOk("metadata_map"); ok { + meta, err := utils.MapInterfaceToMapString(metadataRaw) + if err != nil { + return nil, fmt.Errorf("volume metadata error: %w", err) + } + + volumeData.Metadata = meta + } + + return &volumeData, nil +} + +func ExtendVolume(client *edgecloud.ServiceClient, volumeID string, newSize int) error { + opts := volumes.SizePropertyOperationOpts{ + Size: newSize, + } + results, err := volumes.Extend(client, volumeID, opts).Extract() + if err != nil { + return fmt.Errorf("extracting Volume resource error: %w", err) + } + + taskID := results.Tasks[0] + log.Printf("[DEBUG] Task id (%s)", taskID) + _, err = tasks.WaitTaskAndReturnResult(client, taskID, true, volumeExtending, func(task tasks.TaskID) (interface{}, error) { + _, err := volumes.Get(client, volumeID).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get volume with ID: %s. Error: %w", volumeID, err) + } + return nil, nil + }) + + if err != nil { + return fmt.Errorf("checking Volume state error: %w", err) + } + log.Printf("[DEBUG] Finish waiting.") + + return nil +} diff --git a/edgecenter/test/.env b/edgecenter/test/.env new file mode 100644 index 00000000..fb0a07e5 --- /dev/null +++ b/edgecenter/test/.env @@ -0,0 +1,11 @@ +VAULT_ADDR=https://vault.p.ecnl.ru/ +EC_API=https://api.edgecenter.online/cloud +EC_CDN_URL=https://cdn.edgecenter.online +EC_DNS_API=https://api.edgecenter.online/dns +EC_PASSWORD=Test-1234 +EC_PLATFORM=https://api.edgecenter.online/iam +EC_STORAGE_API=https://api.edgecenter.online/storage +EC_USERNAME=test-cloud-common@edgecenter.ru +TEST_PROJECT_ID=31203 +TEST_REGION_ID=8 + diff --git a/edgecenter/test/data_source_edgecenter_floatingip_test.go b/edgecenter/test/data_source_edgecenter_floatingip_test.go new file mode 100644 index 00000000..f78b884f --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_floatingip_test.go @@ -0,0 +1,83 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/floatingip/v1/floatingips" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccFloatingIPDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.FloatingIPsPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := floatingips.CreateOpts{} + + res, err := floatingips.Create(client, opts).Extract() + if err != nil { + t.Fatal(err) + } + + taskID := res.Tasks[0] + floatingIPID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.FloatingIPCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + floatingIPID, err := floatingips.ExtractFloatingIPIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve FloatingIP ID from task info: %w", err) + } + return floatingIPID, nil + }) + if err != nil { + t.Fatal(err) + } + + defer floatingips.Delete(client, floatingIPID.(string)) + + fip, err := floatingips.Get(client, floatingIPID.(string)).Extract() + if err != nil { + t.Fatal(err) + } + + resourceName := "data.edgecenter_floatingip.acctest" + tpl := func(ip string) string { + return fmt.Sprintf(` + data "edgecenter_floatingip" "acctest" { + %s + %s + floating_ip_address = "%s" + } + `, projectInfo(), regionInfo(), ip) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(fip.FloatingIPAddress.String()), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "id", floatingIPID.(string)), + resource.TestCheckResourceAttr(resourceName, "floating_ip_address", fip.FloatingIPAddress.String()), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_image_test.go b/edgecenter/test/data_source_edgecenter_image_test.go new file mode 100644 index 00000000..bbc58911 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_image_test.go @@ -0,0 +1,68 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/image/v1/images" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccImageDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.ImagesPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + imgs, err := images.ListAll(client, images.ListOpts{}) + if err != nil { + t.Fatal(err) + } + + if len(imgs) == 0 { + t.Fatal("images not found") + } + + img := imgs[0] + + resourceName := "data.edgecenter_image.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_image" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(img.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", img.Name), + resource.TestCheckResourceAttr(resourceName, "id", img.ID), + resource.TestCheckResourceAttr(resourceName, "min_disk", strconv.Itoa(img.MinDisk)), + resource.TestCheckResourceAttr(resourceName, "min_ram", strconv.Itoa(img.MinRAM)), + resource.TestCheckResourceAttr(resourceName, "os_distro", img.OsDistro), + resource.TestCheckResourceAttr(resourceName, "os_version", img.OsVersion), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_instance_test.go b/edgecenter/test/data_source_edgecenter_instance_test.go new file mode 100644 index 00000000..541a3eb8 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_instance_test.go @@ -0,0 +1,140 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/image/v1/images" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/instances" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccInstanceDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + clientVolume, err := createTestClient(cfg.Provider, edgecenter.VolumesPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + clientImage, err := createTestClient(cfg.Provider, edgecenter.ImagesPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + imgs, err := images.ListAll(clientImage, nil) + if err != nil { + t.Fatal(err) + } + + var img images.Image + for _, i := range imgs { + if i.OsDistro == osDistroTest { + img = i + break + } + } + if img.ID == "" { + t.Fatalf("images with os_distro='%s' does not exist", osDistroTest) + } + + optsV := volumes.CreateOpts{ + Name: volumeTestName, + Size: volumeSizeTest * 5, + Source: volumes.Image, + TypeName: volumes.Standard, + ImageID: img.ID, + } + volumeID, err := createTestVolume(clientVolume, optsV) + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.InstancePoint, edgecenter.VersionPointV2) + if err != nil { + t.Fatal(err) + } + + clientV1, err := createTestClient(cfg.Provider, edgecenter.InstancePoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := instances.CreateOpts{ + Names: []string{instanceTestName}, + Flavor: flavorTest, + Volumes: []instances.CreateVolumeOpts{{ + Source: types.ExistingVolume, + BootIndex: 0, + VolumeID: volumeID, + }}, + Interfaces: []instances.InterfaceInstanceCreateOpts{ + { + InterfaceOpts: instances.InterfaceOpts{Type: types.ExternalInterfaceType}, + SecurityGroups: []edgecloud.ItemID{}, + }, + }, + } + + res, err := instances.Create(client, opts).Extract() + if err != nil { + t.Fatal(err) + } + + taskID := res.Tasks[0] + instanceID, err := tasks.WaitTaskAndReturnResult(clientVolume, taskID, true, edgecenter.InstanceCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(clientVolume, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + id, err := instances.ExtractInstanceIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve instance ID from task info: %w", err) + } + return id, nil + }, + ) + if err != nil { + t.Fatal(err) + } + + defer instances.Delete(clientV1, instanceID.(string), instances.DeleteOpts{Volumes: []string{volumeID}}) + + resourceName := "data.edgecenter_instance.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_instance" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(instanceTestName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", instanceTestName), + resource.TestCheckResourceAttr(resourceName, "id", instanceID.(string)), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_k8s_pool_test.go b/edgecenter/test/data_source_edgecenter_k8s_pool_test.go new file mode 100644 index 00000000..1c0268b1 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_k8s_pool_test.go @@ -0,0 +1,153 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "net" + "os" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/pools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/keypair/v2/keypairs" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccK8sPoolDataSource(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + k8sClient, err := createTestClient(cfg.Provider, edgecenter.K8sPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + netClient, err := createTestClient(cfg.Provider, edgecenter.NetworksPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + subnetClient, err := createTestClient(cfg.Provider, edgecenter.SubnetPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + kpClient, err := createTestClient(cfg.Provider, edgecenter.KeypairsPoint, edgecenter.VersionPointV2) + if err != nil { + t.Fatal(err) + } + + netOpts := networks.CreateOpts{ + Name: networkTestName, + CreateRouter: true, + } + networkID, err := createTestNetwork(netClient, netOpts) + if err != nil { + t.Fatal(err) + } + defer deleteTestNetwork(netClient, networkID) + + gw := net.ParseIP("") + subnetOpts := subnets.CreateOpts{ + Name: subnetTestName, + NetworkID: networkID, + ConnectToNetworkRouter: true, + EnableDHCP: true, + GatewayIP: &gw, + } + + subnetID, err := createTestSubnet(subnetClient, subnetOpts) + if err != nil { + t.Fatal(err) + } + + // update our new network router so that the k8s nodes will have access to the Nexus + // registry to download images + if err := patchRouterForK8S(cfg.Provider, networkID); err != nil { + t.Fatal(err) + } + + pid, err := strconv.Atoi(os.Getenv("TEST_PROJECT_ID")) + if err != nil { + t.Fatal(err) + } + + kpOpts := keypairs.CreateOpts{ + Name: kpTestName, + PublicKey: pkTest, + ProjectID: pid, + } + keyPair, err := keypairs.Create(kpClient, kpOpts).Extract() + if err != nil { + t.Fatal(err) + } + defer keypairs.Delete(kpClient, keyPair.ID) + + nodeCountTestPtr := nodeCountTest + dockerVolumeSizeTestPtr := dockerVolumeSizeTest + maxNodeCountTestPtr := maxNodeCountTest + k8sOpts := clusters.CreateOpts{ + Name: clusterTestName, + FixedNetwork: networkID, + FixedSubnet: subnetID, + AutoHealingEnabled: true, + KeyPair: keyPair.ID, + Version: clusterVersionTest, + Pools: []pools.CreateOpts{{ + Name: poolTestName, + FlavorID: flavorTest, + NodeCount: &nodeCountTestPtr, + DockerVolumeSize: &dockerVolumeSizeTestPtr, + DockerVolumeType: ockerVolumeTypeTest, + MinNodeCount: minNodeCountTest, + MaxNodeCount: &maxNodeCountTestPtr, + }}, + } + clusterID, err := createTestCluster(k8sClient, k8sOpts) + if err != nil { + t.Fatal(err) + } + defer deleteTestCluster(k8sClient, clusterID) + + cluster, err := clusters.Get(k8sClient, clusterID).Extract() + if err != nil { + t.Fatal(err) + } + pool := cluster.Pools[0] + + resourceName := "data.edgecenter_k8s_pool.acctest" + ipTemplate := fmt.Sprintf(` + data "edgecenter_k8s_pool" "acctest" { + %s + %s + cluster_id = "%s" + pool_id = "%s" + } + `, projectInfo(), regionInfo(), clusterID, pool.UUID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ipTemplate, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cluster_id", clusterID), + resource.TestCheckResourceAttr(resourceName, "pool_id", pool.UUID), + resource.TestCheckResourceAttr(resourceName, "name", pool.Name), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_k8s_test.go b/edgecenter/test/data_source_edgecenter_k8s_test.go new file mode 100644 index 00000000..a694ad52 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_k8s_test.go @@ -0,0 +1,146 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "net" + "os" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/pools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/keypair/v2/keypairs" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccK8sDataSource(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + resourceName := "data.edgecenter_k8s.acctest" + + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + k8sClient, err := createTestClient(cfg.Provider, edgecenter.K8sPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + netClient, err := createTestClient(cfg.Provider, edgecenter.NetworksPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + subnetClient, err := createTestClient(cfg.Provider, edgecenter.SubnetPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + kpClient, err := createTestClient(cfg.Provider, edgecenter.KeypairsPoint, edgecenter.VersionPointV2) + if err != nil { + t.Fatal(err) + } + + netOpts := networks.CreateOpts{ + Name: networkTestName, + CreateRouter: true, + } + networkID, err := createTestNetwork(netClient, netOpts) + if err != nil { + t.Fatal(err) + } + defer deleteTestNetwork(netClient, networkID) + + gw := net.ParseIP("") + subnetOpts := subnets.CreateOpts{ + Name: subnetTestName, + NetworkID: networkID, + ConnectToNetworkRouter: true, + EnableDHCP: true, + GatewayIP: &gw, + } + + subnetID, err := createTestSubnet(subnetClient, subnetOpts) + if err != nil { + t.Fatal(err) + } + + // update our new network router so that the k8s nodes will have access to the Nexus + // registry to download images + if err := patchRouterForK8S(cfg.Provider, networkID); err != nil { + t.Fatal(err) + } + + pid, err := strconv.Atoi(os.Getenv("TEST_PROJECT_ID")) + if err != nil { + t.Fatal(err) + } + + kpOpts := keypairs.CreateOpts{ + Name: kpTestName, + PublicKey: pkTest, + ProjectID: pid, + } + keyPair, err := keypairs.Create(kpClient, kpOpts).Extract() + if err != nil { + t.Fatal(err) + } + defer keypairs.Delete(kpClient, keyPair.ID) + + nodeCountTestPtr := nodeCountTest + dockerVolumeSizeTestPtr := dockerVolumeSizeTest + maxNodeCountTestPtr := maxNodeCountTest + k8sOpts := clusters.CreateOpts{ + Name: clusterTestName, + FixedNetwork: networkID, + FixedSubnet: subnetID, + AutoHealingEnabled: true, + KeyPair: keyPair.ID, + Version: clusterVersionTest, + Pools: []pools.CreateOpts{{ + Name: poolTestName, + FlavorID: flavorTest, + NodeCount: &nodeCountTestPtr, + DockerVolumeSize: &dockerVolumeSizeTestPtr, + DockerVolumeType: ockerVolumeTypeTest, + MinNodeCount: minNodeCountTest, + MaxNodeCount: &maxNodeCountTestPtr, + }}, + } + clusterID, err := createTestCluster(k8sClient, k8sOpts) + if err != nil { + t.Fatal(err) + } + defer deleteTestCluster(k8sClient, clusterID) + + ipTemplate := fmt.Sprintf(` + data "edgecenter_k8s" "acctest" { + %s + %s + cluster_id = "%s" + } + `, projectInfo(), regionInfo(), clusterID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: ipTemplate, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cluster_id", clusterID), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_lblistener_test.go b/edgecenter/test/data_source_edgecenter_lblistener_test.go new file mode 100644 index 00000000..2f7d3092 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_lblistener_test.go @@ -0,0 +1,82 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccLBListenerDataSource(t *testing.T) { + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.LoadBalancersPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + clientListener, err := createTestClient(cfg.Provider, edgecenter.LBListenersPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := loadbalancers.CreateOpts{ + Name: lbTestName, + Listeners: []loadbalancers.CreateListenerOpts{{ + Name: lbListenerTestName, + ProtocolPort: 80, + Protocol: types.ProtocolTypeHTTP, + AllowedCIDRs: []string{"127.0.0.0/24"}, + }}, + } + + lbID, err := createTestLoadBalancerWithListener(client, opts) + if err != nil { + t.Fatal(err) + } + defer loadbalancers.Delete(client, lbID) + + ls, err := listeners.ListAll(clientListener, listeners.ListOpts{LoadBalancerID: &lbID}) + if err != nil { + t.Fatal(err) + } + listener := ls[0] + + resourceName := "data.edgecenter_lblistener.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_lblistener" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(lbListenerTestName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", lbListenerTestName), + resource.TestCheckResourceAttr(resourceName, "id", listener.ID), + resource.TestCheckResourceAttr(resourceName, "allowed_cidrs.#", strconv.Itoa(len(listener.AllowedCIDRs))), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_lbpool_test.go b/edgecenter/test/data_source_edgecenter_lbpool_test.go new file mode 100644 index 00000000..8226a7ac --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_lbpool_test.go @@ -0,0 +1,132 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/lbpools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccLBPoolDataSource(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.LoadBalancersPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + clientListener, err := createTestClient(cfg.Provider, edgecenter.LBListenersPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + clientPools, err := createTestClient(cfg.Provider, edgecenter.LBPoolsPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := loadbalancers.CreateOpts{ + Name: lbTestName, + Listeners: []loadbalancers.CreateListenerOpts{{ + Name: lbListenerTestName, + ProtocolPort: 80, + Protocol: types.ProtocolTypeHTTP, + }}, + } + + lbID, err := createTestLoadBalancerWithListener(client, opts) + if err != nil { + t.Fatal(err) + } + defer loadbalancers.Delete(client, lbID) + + ls, err := listeners.ListAll(clientListener, listeners.ListOpts{LoadBalancerID: &lbID}) + if err != nil { + t.Fatal(err) + } + listener := ls[0] + + optsPool := lbpools.CreateOpts{ + Name: poolTestName, + Protocol: types.ProtocolTypeHTTP, + LoadBalancerID: lbID, + ListenerID: listener.ID, + LBPoolAlgorithm: types.LoadBalancerAlgorithmRoundRobin, + HealthMonitor: &lbpools.CreateHealthMonitorOpts{ + Type: types.HealthMonitorTypeHTTP, + Delay: 5, + MaxRetries: 10, + Timeout: 10, + MaxRetriesDown: 10, + HTTPMethod: types.HTTPMethodPointer(types.HTTPMethodGET), + URLPath: "/", + ExpectedCodes: "123,321", + }, + } + res, err := lbpools.Create(clientPools, optsPool).Extract() + if err != nil { + t.Fatal(err) + } + + taskID := res.Tasks[0] + lbPoolID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.LBPoolsCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + lbPoolID, err := lbpools.ExtractPoolIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LBPool ID from task info: %w", err) + } + return lbPoolID, nil + }) + if err != nil { + t.Fatal(err) + } + + pool, err := lbpools.Get(clientPools, lbPoolID.(string)).Extract() + if err != nil { + t.Fatal(err) + } + + resourceName := "data.edgecenter_lbpool.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_lbpool" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(poolTestName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", poolTestName), + resource.TestCheckResourceAttr(resourceName, "id", pool.ID), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_loadbalancer_test.go b/edgecenter/test/data_source_edgecenter_loadbalancer_test.go new file mode 100644 index 00000000..ef6d3f5f --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_loadbalancer_test.go @@ -0,0 +1,68 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccLoadBalancerDataSource(t *testing.T) { + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.LoadBalancersPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := loadbalancers.CreateOpts{ + Name: lbTestName, + Listeners: []loadbalancers.CreateListenerOpts{{ + Name: lbListenerTestName, + ProtocolPort: 80, + Protocol: types.ProtocolTypeHTTP, + }}, + } + + lbID, err := createTestLoadBalancerWithListener(client, opts) + if err != nil { + t.Fatal(err) + } + + defer loadbalancers.Delete(client, lbID) + + resourceName := "data.edgecenter_loadbalancer.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_loadbalancer" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(opts.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", opts.Name), + resource.TestCheckResourceAttr(resourceName, "id", lbID), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_network_test.go b/edgecenter/test/data_source_edgecenter_network_test.go new file mode 100644 index 00000000..873d5238 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_network_test.go @@ -0,0 +1,100 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccNetworkDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.NetworksPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts1 := networks.CreateOpts{ + Name: "test-network1", + Metadata: map[string]string{"key1": "val1", "key2": "val2"}, + } + + network1ID, err := createTestNetwork(client, opts1) + if err != nil { + t.Fatal(err) + } + opts2 := networks.CreateOpts{ + Name: "test-network2", + Metadata: map[string]string{"key1": "val1", "key3": "val3"}, + } + + network2ID, err := createTestNetwork(client, opts2) + if err != nil { + t.Fatal(err) + } + + defer deleteTestNetwork(client, network1ID) + defer deleteTestNetwork(client, network2ID) + + resourceName := "data.edgecenter_network.acctest" + tpl1 := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_network" "acctest" { + %s + %s + name = "%s" + metadata_k="key1" + } + `, projectInfo(), regionInfo(), name) + } + tpl2 := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_network" "acctest" { + %s + %s + name = "%s" + metadata_kv={ + key3 = "val3" + } + } + `, projectInfo(), regionInfo(), name) + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl1(opts1.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", opts1.Name), + resource.TestCheckResourceAttr(resourceName, "id", network1ID), + testAccCheckMetadata(t, resourceName, true, map[string]string{ + "key1": "val1", "key2": "val2", + }), + ), + }, + { + Config: tpl2(opts2.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", opts2.Name), + resource.TestCheckResourceAttr(resourceName, "id", network2ID), + testAccCheckMetadata(t, resourceName, true, map[string]string{ + "key3": "val3", + }), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_project_test.go b/edgecenter/test/data_source_edgecenter_project_test.go new file mode 100644 index 00000000..11c58cca --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_project_test.go @@ -0,0 +1,62 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/project/v1/projects" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccProjectDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.ProjectPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + prjs, err := projects.ListAll(client) + if err != nil { + t.Fatal(err) + } + + if len(prjs) == 0 { + t.Fatal("projects not found") + } + + project := prjs[0] + + resourceName := "data.edgecenter_project.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_project" "acctest" { + name = "%s" + } + `, name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(project.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", project.Name), + resource.TestCheckResourceAttr(resourceName, "id", strconv.Itoa(project.ID)), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_region_test.go b/edgecenter/test/data_source_edgecenter_region_test.go new file mode 100644 index 00000000..8c69923c --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_region_test.go @@ -0,0 +1,62 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/region/v1/regions" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccRegionDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.RegionPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + rs, err := regions.ListAll(client) + if err != nil { + t.Fatal(err) + } + + if len(rs) == 0 { + t.Fatal("regions not found") + } + + region := rs[0] + + resourceName := "data.edgecenter_region.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_region" "acctest" { + name = "%s" + } + `, name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(region.DisplayName), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", region.DisplayName), + resource.TestCheckResourceAttr(resourceName, "id", strconv.Itoa(region.ID)), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_reservedfixedip_test.go b/edgecenter/test/data_source_edgecenter_reservedfixedip_test.go new file mode 100644 index 00000000..8db0b88b --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_reservedfixedip_test.go @@ -0,0 +1,85 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/reservedfixedip/v1/reservedfixedips" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccReservedFixedIPDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.ReservedFixedIPsPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := reservedfixedips.CreateOpts{ + Type: reservedfixedips.External, + } + + res, err := reservedfixedips.Create(client, opts).Extract() + if err != nil { + t.Fatal(err) + } + + taskID := res.Tasks[0] + reservedFixedIPID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.ReservedFixedIPCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + reservedFixedIPID, err := reservedfixedips.ExtractReservedFixedIPIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve reservedFixedIP ID from task info: %w", err) + } + return reservedFixedIPID, nil + }) + if err != nil { + t.Fatal(err) + } + + defer reservedfixedips.Delete(client, reservedFixedIPID.(string)) + + fip, err := reservedfixedips.Get(client, reservedFixedIPID.(string)).Extract() + if err != nil { + t.Fatal(err) + } + + resourceName := "data.edgecenter_reservedfixedip.acctest" + tpl := func(ip string) string { + return fmt.Sprintf(` + data "edgecenter_reservedfixedip" "acctest" { + %s + %s + fixed_ip_address = "%s" + } + `, projectInfo(), regionInfo(), ip) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(fip.FixedIPAddress.String()), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "id", reservedFixedIPID.(string)), + resource.TestCheckResourceAttr(resourceName, "fixed_ip_address", fip.FixedIPAddress.String()), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_router_test.go b/edgecenter/test/data_source_edgecenter_router_test.go new file mode 100644 index 00000000..c03de8b9 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_router_test.go @@ -0,0 +1,75 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/router/v1/routers" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccRouterDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + clientNet, err := createTestClient(cfg.Provider, edgecenter.NetworksPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + clientRouter, err := createTestClient(cfg.Provider, edgecenter.RouterPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := networks.CreateOpts{ + Name: networkTestName, + CreateRouter: true, + } + + networkID, err := createTestNetwork(clientNet, opts) + if err != nil { + t.Fatal(err) + } + defer networks.Delete(clientNet, networkID) + + rs, err := routers.ListAll(clientRouter, routers.ListOpts{}) + if err != nil { + t.Fatal(err) + } + router := rs[0] + + resourceName := "data.edgecenter_router.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_router" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(router.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", router.Name), + resource.TestCheckResourceAttr(resourceName, "id", router.ID), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_secret_test.go b/edgecenter/test/data_source_edgecenter_secret_test.go new file mode 100644 index 00000000..6c71ded1 --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_secret_test.go @@ -0,0 +1,86 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/secret/v1/secrets" + secretsV2 "github.com/Edge-Center/edgecentercloud-go/edgecenter/secret/v2/secrets" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSecretDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.SecretPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + clientV2, err := createTestClient(cfg.Provider, edgecenter.SecretPoint, edgecenter.VersionPointV2) + if err != nil { + t.Fatal(err) + } + + opts := secretsV2.CreateOpts{ + Name: secretTestName, + Payload: secretsV2.PayloadOpts{ + CertificateChain: certificateChain, + Certificate: certificate, + PrivateKey: privateKey, + }, + } + results, err := secretsV2.Create(clientV2, opts).Extract() + if err != nil { + t.Fatal(err) + } + + taskID := results.Tasks[0] + secretID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.SecretCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + Secret, err := secrets.ExtractSecretIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Secret ID from task info: %w", err) + } + return Secret, nil + }, + ) + if err != nil { + t.Fatal(err) + } + defer secrets.Delete(client, secretID.(string)) + + resourceName := "data.edgecenter_secret.acctest" + kpTemplate := fmt.Sprintf(` + data "edgecenter_secret" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), secretTestName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: kpTemplate, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", secretTestName), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_securitygroup_test.go b/edgecenter/test/data_source_edgecenter_securitygroup_test.go new file mode 100644 index 00000000..13f4cebe --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_securitygroup_test.go @@ -0,0 +1,108 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygroups" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSecurityGroupDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.SecurityGroupPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts1 := securitygroups.CreateOpts{ + SecurityGroup: securitygroups.CreateSecurityGroupOpts{ + Name: "test-sg1", + SecurityGroupRules: []securitygroups.CreateSecurityGroupRuleOpts{}, + Metadata: map[string]interface{}{"key1": "val1", "key2": "val2"}, + }, + } + + sg1, err := securitygroups.Create(client, opts1).Extract() + if err != nil { + t.Fatal(err) + } + + opts2 := securitygroups.CreateOpts{ + SecurityGroup: securitygroups.CreateSecurityGroupOpts{ + Name: "test-sg2", + SecurityGroupRules: []securitygroups.CreateSecurityGroupRuleOpts{}, + Metadata: map[string]interface{}{"key1": "val1", "key3": "val3"}, + }, + } + + sg2, err := securitygroups.Create(client, opts2).Extract() + if err != nil { + t.Fatal(err) + } + defer securitygroups.Delete(client, sg1.ID) + defer securitygroups.Delete(client, sg2.ID) + + resourceName := "data.edgecenter_securitygroup.acctest" + + tpl1 := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_securitygroup" "acctest" { + %s + %s + name = "%s" + metadata_k="key1" + } + `, projectInfo(), regionInfo(), name) + } + + tpl2 := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_securitygroup" "acctest" { + %s + %s + name = "%s" + metadata_kv={ + key3 = "val3" + } + } + `, projectInfo(), regionInfo(), name) + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl1(sg1.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", sg1.Name), + resource.TestCheckResourceAttr(resourceName, "id", sg1.ID), + testAccCheckMetadata(t, resourceName, true, map[string]interface{}{ + "key1": "val1", "key2": "val2", + }), + ), + }, + { + Config: tpl2(sg2.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", sg2.Name), + resource.TestCheckResourceAttr(resourceName, "id", sg2.ID), + testAccCheckMetadata(t, resourceName, true, map[string]interface{}{ + "key3": "val3", + }), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_servergroup_test.go b/edgecenter/test/data_source_edgecenter_servergroup_test.go new file mode 100644 index 00000000..e3605cfc --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_servergroup_test.go @@ -0,0 +1,61 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/servergroup/v1/servergroups" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccServerGroupDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.ServerGroupsPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := servergroups.CreateOpts{Name: "name", Policy: servergroups.AntiAffinityPolicy} + serverGroup, err := servergroups.Create(client, opts).Extract() + if err != nil { + t.Fatal(err) + } + + resourceName := "data.edgecenter_servergroup.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_servergroup" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + defer servergroups.Delete(client, serverGroup.ServerGroupID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(opts.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", serverGroup.Name), + resource.TestCheckResourceAttr(resourceName, "id", serverGroup.ServerGroupID), + resource.TestCheckResourceAttr(resourceName, "policy", serverGroup.Policy.String()), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_storage_s3_test.go b/edgecenter/test/data_source_edgecenter_storage_s3_test.go new file mode 100644 index 00000000..f56606fb --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_storage_s3_test.go @@ -0,0 +1,94 @@ +//go:build storage + +package edgecenter_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecenter-storage-sdk-go/swagger/client/storages" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +const ( + s3Storage = "edgecenter_storage_s3" +) + +func TestStorageS3DataSource(t *testing.T) { + t.Parallel() + random := time.Now().Nanosecond() + resourceName := fmt.Sprintf("edgecenter_storage_s3.terraformtest%d_s3", random) + dataSourceName := fmt.Sprintf("data.edgecenter_storage_s3.terraformtest%d_s3_data", random) + + templateCreate := func() string { + return fmt.Sprintf(` +resource "%s" "terraformtest%d_s3" { + name = "terraformtest%d" + location = "s-ed1" +} + `, s3Storage, random, random) + } + + templateRead := func() string { + return fmt.Sprintf(` +data "%s" "terraformtest%d_s3_data" { + name = "terraformtest%d" +} + `, s3Storage, random, random) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckVars(t, EC_USERNAME_VAR, EC_PASSWORD_VAR, EC_STORAGE_URL_VAR) + }, + CheckDestroy: func(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + for _, rs := range s.RootModule().Resources { + if rs.Type != s3Storage { + continue + } + opts := []func(opt *storages.StorageListHTTPV2Params){ + func(opt *storages.StorageListHTTPV2Params) { opt.Context = ctx }, + func(opt *storages.StorageListHTTPV2Params) { opt.ID = &rs.Primary.ID }, + } + storages, err := config.StorageClient.StoragesList(opts...) + if err != nil { + return fmt.Errorf("find storage: %w", err) + } + if len(storages) == 0 { + return nil + } + if storages[0].ProvisioningStatus == "ok" { + return fmt.Errorf("storage #%s wasn't deleted correctrly", rs.Primary.ID) + } + } + + return nil + }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: templateCreate(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, edgecenter.StorageSchemaLocation, "s-ed1"), + ), + }, + { + Config: templateRead(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(dataSourceName), + resource.TestCheckResourceAttr(dataSourceName, edgecenter.StorageSchemaLocation, "s-ed1"), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_subnet_test.go b/edgecenter/test/data_source_edgecenter_subnet_test.go new file mode 100644 index 00000000..3794828c --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_subnet_test.go @@ -0,0 +1,120 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSubnetDataSource(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + clientNet, err := createTestClient(cfg.Provider, edgecenter.NetworksPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + clientSubnet, err := createTestClient(cfg.Provider, edgecenter.SubnetPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := networks.CreateOpts{ + Name: networkTestName, + } + + networkID, err := createTestNetwork(clientNet, opts) + if err != nil { + t.Fatal(err) + } + + defer deleteTestNetwork(clientNet, networkID) + + optsSubnet1 := subnets.CreateOpts{ + Name: "test-subnet1", + NetworkID: networkID, + Metadata: map[string]string{"key1": "val1", "key2": "val2"}, + } + + subnet1ID, err := createTestSubnet(clientSubnet, optsSubnet1, "192.168.41.0/24") + if err != nil { + t.Fatal(err) + } + + optsSubnet2 := subnets.CreateOpts{ + Name: "test-subnet2", + NetworkID: networkID, + Metadata: map[string]string{"key1": "val1", "key3": "val3"}, + } + + subnet2ID, err := createTestSubnet(clientSubnet, optsSubnet2, "192.168.43.0/24") + if err != nil { + t.Fatal(err) + } + + resourceName := "data.edgecenter_subnet.acctest" + tpl1 := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_subnet" "acctest" { + %s + %s + name = "%s" + metadata_k="key1" + } + `, projectInfo(), regionInfo(), name) + } + + tpl2 := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_subnet" "acctest" { + %s + %s + name = "%s" + metadata_kv={ + key3 = "val3" + } + } + `, projectInfo(), regionInfo(), name) + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl1(optsSubnet1.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", optsSubnet1.Name), + resource.TestCheckResourceAttr(resourceName, "id", subnet1ID), + resource.TestCheckResourceAttr(resourceName, "network_id", networkID), + testAccCheckMetadata(t, resourceName, true, map[string]string{ + "key1": "val1", "key2": "val2", + }), + ), + }, + { + Config: tpl2(optsSubnet2.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", optsSubnet2.Name), + resource.TestCheckResourceAttr(resourceName, "id", subnet2ID), + // resource.TestCheckResourceAttr(resourceName, "network_id", networkID), + testAccCheckMetadata(t, resourceName, true, map[string]string{ + "key3": "val3", + }), + ), + }, + }, + }) +} diff --git a/edgecenter/test/data_source_edgecenter_volume_test.go b/edgecenter/test/data_source_edgecenter_volume_test.go new file mode 100644 index 00000000..97d3c07f --- /dev/null +++ b/edgecenter/test/data_source_edgecenter_volume_test.go @@ -0,0 +1,67 @@ +//go:build cloud_data_source + +package edgecenter_test + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccVolumeDataSource(t *testing.T) { + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.VolumesPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := volumes.CreateOpts{ + Name: volumeTestName, + Size: volumeSizeTest, + Source: volumes.NewVolume, + TypeName: volumes.Standard, + } + + volumeID, err := createTestVolume(client, opts) + if err != nil { + t.Fatal(err) + } + + defer volumes.Delete(client, volumeID, volumes.DeleteOpts{}) + + resourceName := "data.edgecenter_volume.acctest" + tpl := func(name string) string { + return fmt.Sprintf(` + data "edgecenter_volume" "acctest" { + %s + %s + name = "%s" + } + `, projectInfo(), regionInfo(), name) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: tpl(opts.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", opts.Name), + resource.TestCheckResourceAttr(resourceName, "id", volumeID), + resource.TestCheckResourceAttr(resourceName, "size", strconv.Itoa(opts.Size)), + ), + }, + }, + }) +} diff --git a/edgecenter/test/helper_test.go b/edgecenter/test/helper_test.go new file mode 100644 index 00000000..aa7249e4 --- /dev/null +++ b/edgecenter/test/helper_test.go @@ -0,0 +1,250 @@ +//go:build cloud_data_source || cloud_resource + +package edgecenter_test + +import ( + "errors" + "fmt" + "net" + "strings" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/k8s/v1/clusters" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/loadbalancers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/availablenetworks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/router/v1/routers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func createTestNetwork(client *edgecloud.ServiceClient, opts networks.CreateOpts) (string, error) { + result, err := networks.Create(client, opts).Extract() + if err != nil { + return "", err + } + + taskID := result.Tasks[0] + networkID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.NetworkCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + networkID, err := networks.ExtractNetworkIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve network ID from task info: %w", err) + } + return networkID, nil + }) + + if err != nil { + return "", err + } + + return networkID.(string), nil +} + +func deleteTestNetwork(client *edgecloud.ServiceClient, networkID string) error { + result, err := networks.Delete(client, networkID).Extract() + if err != nil { + return err + } + + taskID := result.Tasks[0] + err = tasks.WaitTaskAndProcessResult(client, taskID, true, edgecenter.NetworkDeleting, func(task tasks.TaskID) error { + _, err := networks.Get(client, networkID).Extract() + if err == nil { + return fmt.Errorf("cannot delete network with ID: %s", networkID) + } + + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil + } + return fmt.Errorf("extracting Network resource error: %w", err) + }) + + return err +} + +func createTestSubnet(client *edgecloud.ServiceClient, opts subnets.CreateOpts, extra ...string) (string, error) { + subCidr := cidrTest + if extra != nil { + subCidr = extra[0] + } + + var eccidr edgecloud.CIDR + _, netIPNet, err := net.ParseCIDR(subCidr) + if err != nil { + return "", err + } + eccidr.IP = netIPNet.IP + eccidr.Mask = netIPNet.Mask + opts.CIDR = eccidr + + result, err := subnets.Create(client, opts).Extract() + if err != nil { + return "", err + } + + taskID := result.Tasks[0] + subnetID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.SubnetCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + subnet, err := subnets.ExtractSubnetIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve Subnet ID from task info: %w", err) + } + return subnet, nil + }) + + return subnetID.(string), err +} + +func patchRouterForK8S(provider *edgecloud.ProviderClient, networkID string) error { + routersClient, err := createTestClient(provider, edgecenter.RouterPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + + aNetClient, err := createTestClient(provider, edgecenter.SharedNetworksPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + + availableNetworks, err := availablenetworks.ListAll(aNetClient, nil) + if err != nil { + return err + } + var extNet availablenetworks.Network + for _, an := range availableNetworks { + if an.External { + extNet = an + break + } + } + + rs, err := routers.ListAll(routersClient, nil) + if err != nil { + return err + } + + var router routers.Router + for _, r := range rs { + if strings.Contains(r.Name, networkID) { + router = r + break + } + } + + extSubnet := extNet.Subnets[0] + routerOpts := routers.UpdateOpts{Routes: extSubnet.HostRoutes} + if _, err = routers.Update(routersClient, router.ID, routerOpts).Extract(); err != nil { + return err + } + + return nil +} + +func createTestCluster(client *edgecloud.ServiceClient, opts clusters.CreateOpts) (string, error) { + result, err := clusters.Create(client, opts).Extract() + if err != nil { + return "", err + } + + taskID := result.Tasks[0] + clusterID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.K8sCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + clusterID, err := clusters.ExtractClusterIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve cluster ID from task info: %w", err) + } + return clusterID, nil + }) + + if err != nil { + return "", err + } + + return clusterID.(string), nil +} + +func deleteTestCluster(client *edgecloud.ServiceClient, clusterID string) error { + result, err := clusters.Delete(client, clusterID).Extract() + if err != nil { + return err + } + + taskID := result.Tasks[0] + err = tasks.WaitTaskAndProcessResult(client, taskID, true, edgecenter.K8sCreateTimeout, func(task tasks.TaskID) error { + _, err := clusters.Get(client, clusterID).Extract() + if err == nil { + return fmt.Errorf("cannot delete k8s cluster with ID: %s", clusterID) + } + + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return nil + } + return fmt.Errorf("extracting k8s cluster resource error: %w", err) + }) + + return err +} + +func createTestLoadBalancerWithListener(client *edgecloud.ServiceClient, opts loadbalancers.CreateOpts) (string, error) { + result, err := loadbalancers.Create(client, opts).Extract() + if err != nil { + return "", err + } + + taskID := result.Tasks[0] + lbID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.LoadBalancerCreateTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + lbID, err := loadbalancers.ExtractLoadBalancerIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve LoadBalancer ID from task info: %w", err) + } + return lbID, nil + }) + if err != nil { + return "", err + } + + return lbID.(string), nil +} + +func createTestVolume(client *edgecloud.ServiceClient, opts volumes.CreateOpts) (string, error) { + result, err := volumes.Create(client, opts).Extract() + if err != nil { + return "", err + } + + taskID := result.Tasks[0] + volumeID, err := tasks.WaitTaskAndReturnResult(client, taskID, true, edgecenter.VolumeCreatingTimeout, func(task tasks.TaskID) (interface{}, error) { + taskInfo, err := tasks.Get(client, string(task)).Extract() + if err != nil { + return nil, fmt.Errorf("cannot get task with ID: %s. Error: %w", task, err) + } + volumeID, err := volumes.ExtractVolumeIDFromTask(taskInfo) + if err != nil { + return nil, fmt.Errorf("cannot retrieve volume ID from task info: %w", err) + } + return volumeID, nil + }) + if err != nil { + return "", err + } + + return volumeID.(string), nil +} diff --git a/edgecenter/test/main_test.go b/edgecenter/test/main_test.go new file mode 100644 index 00000000..7fabe06b --- /dev/null +++ b/edgecenter/test/main_test.go @@ -0,0 +1,304 @@ +//go:build cloud_data_source || cloud_resource || dns || storage || cdn + +package edgecenter_test + +import ( + "fmt" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + dnssdk "github.com/Edge-Center/edgecenter-dns-sdk-go" + storageSDK "github.com/Edge-Center/edgecenter-storage-sdk-go" + cdn "github.com/Edge-Center/edgecentercdn-go" + eccdnProvider "github.com/Edge-Center/edgecentercdn-go/edgecenter/provider" + edgecloud "github.com/Edge-Center/edgecentercloud-go" + ec "github.com/Edge-Center/edgecentercloud-go/edgecenter" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +const ( + instanceTestName = "test-vm" + clusterTestName = "test-cluster" + poolTestName = "test-pool" + lbTestName = "test-lb" + lbListenerTestName = "test-listener" + networkTestName = "test-network" + subnetTestName = "test-subnet" + volumeTestName = "test-volume" + secretTestName = "test-secret" + kpTestName = "test-kp" + + flavorTest = "g1-standard-1-2" + osDistroTest = "ubuntu" + clusterVersionTest = "1.20.15" + cidrTest = "192.168.42.0/24" + nodeCountTest = 1 + dockerVolumeSizeTest = 10 + ockerVolumeTypeTest = volumes.Standard + minNodeCountTest = 1 + maxNodeCountTest = 1 + volumeSizeTest = 1 + + pkTest = `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC1bdbQYquD/swsZpFPXagY9KvhlNUTKYMdhRNtlGglAMgRxJS3Q0V74BNElJtP+UU/AbZD4H2ZAwW3PLLD/maclnLlrA48xg/ez9IhppBop0WADZ/nB4EcvQfR/Db7nHDTZERW6EiiGhV6CkHVasK2sY/WNRXqPveeWUlwCqtSnU90l/s9kQCoEfkM2auO6ppJkVrXbs26vcRclS8KL7Cff4HwdVpV7b+edT5seZdtrFUCbkEof9D9nGpahNvg8mYWf0ofx4ona4kaXm1NdPID+ljvE/dbYUX8WZRmyLjMvVQS+VxDJtsiDQIVtwbC4w+recqwDvHhLWwoeczsbEsp ondi@ds` + privateKey = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQ4E6U0vql4EST\n8o41TlHRz6MKmMhddVUjM2juTKjxv4WuB4T3z/wokznEjQg4H7gfYEKeCJqelrfq\ntdOtbPsznSceMOXB5uA2Sc9WVKwk7owoRJxPd4LQeOcarVOFdIzudzkgSK/oV7Za\nL8Y2hylsB4SX2cfbULtmW/WDePp3YZAL6zYV1fXJSnK+hL2iUSqikiViEGRta+47\nnaTKZnnmSgojdshzsw0wlF/PgRJ/Anf9j9J8ratdJP81yAG5daU3L2NdJ3qx9UbV\ntKnSq2z2u4yx6xdb4t4WFQBKNjC6+YZN/gI5lp96p3FNTNS4PKYxAAUrnCwf0EE3\n7dOR4eWlAgMBAAECggEBALPm3ge0h4li1e4PVYh4AmSRT74KxVgpfMCqwM+uWzyM\nVpkDhPTjwC06UOEHD3M3bqAninkOtA2vhoyzOrP+T4Wu70hDmUAemDJp9BhJKVNN\n2o28Olz/dD4WRAZoDq29Kr0hFqTFtiyJj1eyGihQ1c5j00HuowI0UJPi1Fz+T8uN\nPwukUtTPYwEds6SApii3v9VKjmvbRDmsbHU3KkUoaeqpRnRagyp1vtoLXigezUcK\nrQcoh6wlKtvj0YLR2lxq9Wmj1nn6m3F5Bom54X8o18tcOmFSRudRb+Fxjb0jnqSK\nAsyVlZg4alTBQUmx9gIKv0oSJAIh2nXdclECkGjs8WkCgYEA9xvdDWephsbv+X3k\nndnDG9JTxfrR6HMHPrUrTaZ8/VD+Qw4zuReoNGkcQbV3Cb26egprWQWfYc9+l6mU\nAWgOjFgeGie1uwOwkhv6CfhE/iVvotJ3hOOsC5pLEhz4vRpO75C9wSehjfTYkP1m\nXEAhRTRbgMnvzChWyh5CEjosX5sCgYEA2GRHrG0JVxsYSCugLPKf9fSK4CQDm0bK\nywBwZtAWX0xhiHO/BW6PeK1Mqx2nbiWl1hXNpZKJNS9bnrZWym/yUqOvg2XJKjb6\nhHBvwAD1MOQ8Ysby4JHGCrMBEwlcDpI2wpMpXkKhU3X0XWjkqrhqCH/TETFKkqLt\nfJX/c9PTQ78CgYAEPek0grQJST7zVHLpNsS/pIOloWGbEOZt8CQ3KAV7P7mtov/G\nTJ6pj6hZhGjvtN8Pm0Aufgc3YZ11swaEY6nkRNr3bfkTpcORLoPDSgy9JB1feSdu\nE45vgI2LWQ34CQyT1jM7rpd6XVqeWos4SC2KB5UOh+ji40piG9TchT0fwwKBgA/M\nmpMTTvhGKSqzzLkbaeR6W11sI7tFmu7hdFN9Y/THTeO5l7vcy6ri9FMWEjBvnUEZ\nTG+HWG9CquzWoVWcgNPZ0anFV7+2Teo3j2E0cLKGJ4aKwhb1bcFAOpbaOxdxQ4BH\nYGDaeo7ucM4VJ4TzfAJs2stJjwlPzgknpoQddjJfAoGBAIFfnU8x/SrNhAqZrG9d\n3kpJ5LmbVswOYtj01KHM+KpEwOQVF+s2NOeHqyC7QUIWrue00+1MT88F9cNHDeWk\n0dEOJNWCfzcV85l8A+0p6/4qAW7h7RNiFqeA8GyVKCT8f7fu/7WpYw8D0aq8w5X/\nKZl+AjB+MzYFs71+SC4ohTlI\n-----END PRIVATE KEY-----" + certificate = "-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIJAIUvym0uaBHbMA0GCSqGSIb3DQEBCwUAMD0xCzAJBgNV\nBAYTAlJVMQ8wDQYDVQQIDAZNT1NDT1cxCzAJBgNVBAoMAkNBMRAwDgYDVQQDDAdS\nT09UIENBMB4XDTIxMDczMDE1MTU0NVoXDTMxMDcyODE1MTU0NVowTDELMAkGA1UE\nBhMCQ0ExDTALBgNVBAgMBE5vbmUxCzAJBgNVBAcMAk5CMQ0wCwYDVQQKDAROb25l\nMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQDQ4E6U0vql4EST8o41TlHRz6MKmMhddVUjM2juTKjxv4WuB4T3z/wokznE\njQg4H7gfYEKeCJqelrfqtdOtbPsznSceMOXB5uA2Sc9WVKwk7owoRJxPd4LQeOca\nrVOFdIzudzkgSK/oV7ZaL8Y2hylsB4SX2cfbULtmW/WDePp3YZAL6zYV1fXJSnK+\nhL2iUSqikiViEGRta+47naTKZnnmSgojdshzsw0wlF/PgRJ/Anf9j9J8ratdJP81\nyAG5daU3L2NdJ3qx9UbVtKnSq2z2u4yx6xdb4t4WFQBKNjC6+YZN/gI5lp96p3FN\nTNS4PKYxAAUrnCwf0EE37dOR4eWlAgMBAAGjgZcwgZQwVwYDVR0jBFAwTqFBpD8w\nPTELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1PU0NPVzELMAkGA1UECgwCQ0ExEDAO\nBgNVBAMMB1JPT1QgQ0GCCQCectJTETy4lTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE\n8DAhBgNVHREEGjAYgglsb2NhbGhvc3SCCyoubG9jYWxob3N0MA0GCSqGSIb3DQEB\nCwUAA4IBAQBqzJcwygLsVCTPlReUpcKVn84aFqzfZA0m7hYvH+7PDH/FM8SbX3zg\nteBL/PgQAZw1amO8xjeMc2Pe2kvi9VrpfTeGqNia/9axhGu3q/NEP0tyDFXAE2bR\njBdGhd5gCmg+X4WdHigCgn51cz5r2k3fSOIWP+TQWHqc8Yt+vZXnkwnQkRA1Ki7N\nWOiJjj/ae5RWwma/kJNmShTZn754gbQn06bAjNbPjclsHRLkawmLqikd1rYUhIdk\nOr1Nrl+CWMx3CXg0TVVdJ6rH3dO31uyvb+3qEY7WnL+HhZyr08ay8gJsEKPuPFA2\nxvveXqt9ceU5qh+8T7mHwGALEUw96QcP\n-----END CERTIFICATE-----" + certificateChain = "-----BEGIN CERTIFICATE-----\nMIIC9jCCAd4CCQCectJTETy4lTANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJS\nVTEPMA0GA1UECAwGTU9TQ09XMQswCQYDVQQKDAJDQTEQMA4GA1UEAwwHUk9PVCBD\nQTAeFw0yMTA3MzAxNTExMzVaFw0yNDA1MTkxNTExMzVaMD0xCzAJBgNVBAYTAlJV\nMQ8wDQYDVQQIDAZNT1NDT1cxCzAJBgNVBAoMAkNBMRAwDgYDVQQDDAdST09UIENB\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo6tZ0NV6QIR/mvsqtAII\nzTTuBMrZR5OTwKvcGnhe4GVDwzJ/OgEWkghLAzOojcJvkfzJOtWwOXqwgphksc+7\n+vwIPTPt3iWjbQUzXK8pFLkjxrO8px/QxPuUrp+U6DTVvvgQesjMZ9jQRUFKOiCc\nu0st1N5Q/CJR4VOJxtYoLy1ZUlsABhwJ+6trkoOFTLRPlMUX1EIG57jYAotHvQFo\nc8UNx3KzvJsJJ56SniXCIkeu61IOt8aOXHU+3TLYhZnPiP311cMbXA0J3vGPRZwz\n25BZjF3IF/ShXlfzz76FjWUTAThc0+HA8lzx53xD4/n8HN+sGubGx9TvLyZimG/U\nGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAnK8Wzw33fR6R6pqV05XI9Yu8J+BwC\nCn2bKxxYwwQWZyX1as+UIlGuvyBRJba9W2UGMj95FQfWVdDyFC98spUur+O/5yL+\nNHH+dxGnkxIRc6RMIy+GXJwPrLiB/t70hSvwgVa249zNJVcwYN/5SGX5wLaJKnim\neY99xm75nr03O/RJK/DR8HvWysH7zxvrMWs0ppfwxkxrwOcg0Cb9xODVkg/wyClw\nLiHWlmH/eyC8nkiLYJKmV7566VWCV+gy+hC/DRstVVjIMG6LsqaPq6ycm7N8EV8s\nBb5uXIVHW6w5a20c40+W9G4EDYiQjdgEaf0FoMAWGDnOEaPsvjQk2/z5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDPDCCAiQCCQDxA75ydLHVoTANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJS\nVTEPMA0GA1UECAwGTU9TQ09XMQ8wDQYDVQQHDAZNT1NDT1cxFTATBgNVBAoMDElO\nVEVSTUVESUFURTEYMBYGA1UEAwwPSU5URVJNRURJQVRFIENBMB4XDTIxMDczMDE1\nMTIyMloXDTI0MDUxOTE1MTIyMlowYDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1P\nU0NPVzEPMA0GA1UEBwwGTU9TQ09XMRUwEwYDVQQKDAxJTlRFUk1FRElBVEUxGDAW\nBgNVBAMMD0lOVEVSTUVESUFURSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAKOrWdDVekCEf5r7KrQCCM007gTK2UeTk8Cr3Bp4XuBlQ8MyfzoBFpII\nSwMzqI3Cb5H8yTrVsDl6sIKYZLHPu/r8CD0z7d4lo20FM1yvKRS5I8azvKcf0MT7\nlK6flOg01b74EHrIzGfY0EVBSjognLtLLdTeUPwiUeFTicbWKC8tWVJbAAYcCfur\na5KDhUy0T5TFF9RCBue42AKLR70BaHPFDcdys7ybCSeekp4lwiJHrutSDrfGjlx1\nPt0y2IWZz4j99dXDG1wNCd7xj0WcM9uQWYxdyBf0oV5X88++hY1lEwE4XNPhwPJc\n8ed8Q+P5/BzfrBrmxsfU7y8mYphv1BsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA\ngOHvrh66+bQoG3Lo8bfp7D1Xvm/Md3gJq2nMotl2BH1TvNzMV93fCXygRX8J8rTL\n7xjUC2SbOrFDWFq2hNJQagdecAeuG+U55BY6Wi8SsHw+fhgxQyl9wtXWwotQPmsD\nuRhR1rL3vEphgPLbxNBzA7Lvj+P89Ar988Qy+o5AiUzHMUuqZbGOqs8UcKCQP7e/\nIX+zqqFwqyI8f90SVySGgs574jo8jQFy3l5fnp6yK0MPWg2cBCjpa5H1A+5DADF+\nnryV6Ie/m/wfxmitZZN+YCJu+8Bmmdl/FCwbmiH+HCLhrO8gonH3K21cQujMyFF5\nc7OFj86hvhqbr4kzz1J8lg==\n-----END CERTIFICATE-----" +) + +type VarName string + +const ( + EC_USERNAME_VAR VarName = "EC_USERNAME" + EC_PASSWORD_VAR VarName = "EC_PASSWORD" + EC_CDN_URL_VAR VarName = "EC_CDN_URL" + EC_STORAGE_URL_VAR VarName = "EC_STORAGE_API" + EC_DNS_URL_VAR VarName = "EC_DNS_API" + EC_IMAGE_VAR VarName = "EC_IMAGE" + EC_SECGROUP_VAR VarName = "EC_SECGROUP" + EC_EXT_NET_VAR VarName = "EC_EXT_NET" + EC_PRIV_NET_VAR VarName = "EC_PRIV_NET" + EC_PRIV_SUBNET_VAR VarName = "EC_PRIV_SUBNET" + EC_LB_ID_VAR VarName = "EC_LB_ID" + EC_LBLISTENER_ID_VAR VarName = "EC_LBLISTENER_ID" + EC_LBPOOL_ID_VAR VarName = "EC_LBPOOL_ID" + EC_VOLUME_ID_VAR VarName = "EC_VOLUME_ID" + EC_CDN_ORIGINGROUP_ID_VAR VarName = "EC_CDN_ORIGINGROUP_ID" + EC_CDN_RESOURCE_ID_VAR VarName = "EC_CDN_RESOURCE_ID" + EC_NETWORK_ID_VAR VarName = "EC_NETWORK_ID" + EC_SUBNET_ID_VAR VarName = "EC_SUBNET_ID" + EC_CLUSTER_ID_VAR VarName = "EC_CLUSTER_ID" + EC_CLUSTER_POOL_ID_VAR VarName = "EC_CLUSTER_POOL_ID" +) + +func getEnv(name VarName) string { + return os.Getenv(string(name)) +} + +var ( + EC_USERNAME = getEnv(EC_USERNAME_VAR) + EC_PASSWORD = getEnv(EC_PASSWORD_VAR) + EC_CDN_URL = getEnv(EC_CDN_URL_VAR) + EC_IMAGE = getEnv(EC_IMAGE_VAR) + EC_SECGROUP = getEnv(EC_SECGROUP_VAR) + EC_EXT_NET = getEnv(EC_EXT_NET_VAR) + EC_PRIV_NET = getEnv(EC_PRIV_NET_VAR) + EC_PRIV_SUBNET = getEnv(EC_PRIV_SUBNET_VAR) + EC_LB_ID = getEnv(EC_LB_ID_VAR) + EC_LBLISTENER_ID = getEnv(EC_LBLISTENER_ID_VAR) + EC_LBPOOL_ID = getEnv(EC_LBPOOL_ID_VAR) + EC_VOLUME_ID = getEnv(EC_VOLUME_ID_VAR) + EC_CDN_ORIGINGROUP_ID = getEnv(EC_CDN_ORIGINGROUP_ID_VAR) + EC_CDN_RESOURCE_ID = getEnv(EC_CDN_RESOURCE_ID_VAR) + EC_STORAGE_API = getEnv(EC_STORAGE_URL_VAR) + EC_DNS_API = getEnv(EC_DNS_URL_VAR) + EC_NETWORK_ID = getEnv(EC_NETWORK_ID_VAR) + EC_SUBNET_ID = getEnv(EC_SUBNET_ID_VAR) + EC_CLUSTER_ID = getEnv(EC_CLUSTER_ID_VAR) + EC_CLUSTER_POOL_ID = getEnv(EC_CLUSTER_POOL_ID_VAR) +) + +var varsMap = map[VarName]string{ + EC_USERNAME_VAR: EC_USERNAME, + EC_PASSWORD_VAR: EC_PASSWORD, + EC_CDN_URL_VAR: EC_CDN_URL, + EC_IMAGE_VAR: EC_IMAGE, + EC_SECGROUP_VAR: EC_SECGROUP, + EC_EXT_NET_VAR: EC_EXT_NET, + EC_PRIV_NET_VAR: EC_PRIV_NET, + EC_PRIV_SUBNET_VAR: EC_PRIV_SUBNET, + EC_LB_ID_VAR: EC_LB_ID, + EC_LBLISTENER_ID_VAR: EC_LBLISTENER_ID, + EC_LBPOOL_ID_VAR: EC_LBPOOL_ID, + EC_VOLUME_ID_VAR: EC_VOLUME_ID, + EC_CDN_ORIGINGROUP_ID_VAR: EC_CDN_ORIGINGROUP_ID, + EC_CDN_RESOURCE_ID_VAR: EC_CDN_RESOURCE_ID, + EC_STORAGE_URL_VAR: EC_STORAGE_API, + EC_DNS_URL_VAR: EC_DNS_API, + EC_NETWORK_ID_VAR: EC_NETWORK_ID, + EC_SUBNET_ID_VAR: EC_SUBNET_ID, + EC_CLUSTER_ID_VAR: EC_CLUSTER_ID, + EC_CLUSTER_POOL_ID_VAR: EC_CLUSTER_POOL_ID, +} + +func testAccPreCheckVars(t *testing.T, vars ...VarName) { + t.Helper() + for _, name := range vars { + if val := varsMap[name]; val == "" { + t.Fatalf("'%s' must be set for acceptance test", name) + } + } +} + +func testAccPreCheck(t *testing.T) { + t.Helper() + vars := map[string]interface{}{ + "EC_USERNAME": EC_USERNAME, + "EC_PASSWORD": EC_PASSWORD, + } + for k, v := range vars { + if v == "" { + t.Fatalf("'%s' must be set for acceptance test", k) + } + } + checkNameAndID(t, "PROJECT") + checkNameAndID(t, "REGION") +} + +func checkNameAndID(t *testing.T, resourceType string) { + // resourceType is a word in capital letters + t.Helper() + keyID := fmt.Sprintf("TEST_%s_ID", resourceType) + keyName := fmt.Sprintf("TEST_%s_NAME", resourceType) + _, haveID := os.LookupEnv(keyID) + _, haveName := os.LookupEnv(keyName) + if !haveID && !haveName { + t.Fatalf("%s or %s must be set for acceptance tests", keyID, keyName) + } + if haveID && haveName { + t.Fatalf("Use only one from environment variables: %s or %s", keyID, keyName) + } +} + +func regionInfo() string { + return objectInfo("REGION") +} + +func projectInfo() string { + return objectInfo("PROJECT") +} + +func objectInfo(resourceType string) string { + // resourceType is a word in capital letters + keyID := fmt.Sprintf("TEST_%s_ID", resourceType) + keyName := fmt.Sprintf("TEST_%s_NAME", resourceType) + if objectID, exists := os.LookupEnv(keyID); exists { + return fmt.Sprintf(`%s_id = %s`, strings.ToLower(resourceType), objectID) + } + return fmt.Sprintf(`%s_name = "%s"`, strings.ToLower(resourceType), os.Getenv(keyName)) +} + +func createTestClient(provider *edgecloud.ProviderClient, endpoint, version string) (*edgecloud.ServiceClient, error) { + projectID := 0 + var err error + if strProjectID, exists := os.LookupEnv("TEST_PROJECT_ID"); exists { + projectID, err = strconv.Atoi(strProjectID) + if err != nil { + return nil, err + } + } else { + projectID, err = edgecenter.GetProject(provider, 0, os.Getenv("TEST_PROJECT_NAME")) + if err != nil { + return nil, err + } + } + regionID := 0 + if strRegionID, exists := os.LookupEnv("TEST_REGION_ID"); exists { + regionID, err = strconv.Atoi(strRegionID) + if err != nil { + return nil, err + } + } else { + regionID, err = edgecenter.GetProject(provider, 0, os.Getenv("TEST_REGION_NAME")) + if err != nil { + return nil, err + } + } + + client, err := ec.ClientServiceFromProvider(provider, edgecloud.EndpointOpts{ + Name: endpoint, + Region: regionID, + Project: projectID, + Version: version, + }) + if err != nil { + return nil, err + } + + return client, nil +} + +func createTestConfig() (*edgecenter.Config, error) { + provider, err := ec.AuthenticatedClient(edgecloud.AuthOptions{ + APIURL: os.Getenv("EC_API"), + AuthURL: os.Getenv("EC_PLATFORM"), + Username: os.Getenv("EC_USERNAME"), + Password: os.Getenv("EC_PASSWORD"), + AllowReauth: true, + }) + if err != nil { + return nil, err + } + + cdnProvider := eccdnProvider.NewClient(EC_CDN_URL, eccdnProvider.WithSignerFunc(func(req *http.Request) error { + req.Header.Set("Authorization", "Bearer "+provider.AccessToken()) + return nil + })) + cdnService := cdn.NewService(cdnProvider) + + storageAPI := EC_STORAGE_API + stHost, stPath, err := edgecenter.ExtractHostAndPath(storageAPI) + var storageClient *storageSDK.SDK + if err == nil { + storageClient = storageSDK.NewSDK(stHost, stPath, storageSDK.WithBearerAuth(provider.AccessToken)) + } + + var dnsClient *dnssdk.Client + if EC_DNS_API != "" { + baseURL, err := url.Parse(EC_DNS_API) + if err == nil { + authorizer := dnssdk.BearerAuth(provider.AccessToken()) + dnsClient = dnssdk.NewClient(authorizer, func(client *dnssdk.Client) { + client.BaseURL = baseURL + }) + } + } + + config := edgecenter.Config{ + Provider: provider, + CDNClient: cdnService, + StorageClient: storageClient, + DNSClient: dnsClient, + } + + return &config, nil +} + +func testAccCheckResourceExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // retrieve the resource by name from state + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("not found: %s", resourceName) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("widget ID is not set") + } + return nil + } +} + +var ( + testAccProvider *schema.Provider + testAccProviders map[string]func() (*schema.Provider, error) +) + +func TestMain(m *testing.M) { + testAccProvider = edgecenter.Provider() + testAccProviders = map[string]func() (*schema.Provider, error){ + "edgecenter": func() (*schema.Provider, error) { + return testAccProvider, nil + }, + } + exitCode := m.Run() + os.Exit(exitCode) +} diff --git a/edgecenter/test/provider_test.go b/edgecenter/test/provider_test.go new file mode 100644 index 00000000..f2af548e --- /dev/null +++ b/edgecenter/test/provider_test.go @@ -0,0 +1,14 @@ +package edgecenter_test + +import ( + "testing" + + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestProvider(t *testing.T) { + t.Parallel() + if err := edgecenter.Provider().InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} diff --git a/edgecenter/test/resource_edgecenter_baremetal_test.go b/edgecenter/test/resource_edgecenter_baremetal_test.go new file mode 100644 index 00000000..b3b4cda6 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_baremetal_test.go @@ -0,0 +1,68 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/instances" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccBaremetal(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + resourceName := "edgecenter_baremetal.acctest" + + ipTemplate := fmt.Sprintf(` + resource "edgecenter_baremetal" "acctest" { + %s + %s + name = "test sg" + flavor_id = "bm1-infrastructure-small" + image_id = "1ee7ccee-5003-48c9-8ae0-d96063af75b2" + } + `, projectInfo(), regionInfo()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccBaremetalDestroy, + Steps: []resource.TestStep{ + { + Config: ipTemplate, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "test_sg"), + resource.TestCheckResourceAttr(resourceName, "flavor_id", "bm1-infrastructure-small"), + ), + }, + }, + }) +} + +func testAccBaremetalDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.InstancePoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_baremetal" { + continue + } + + _, err := instances.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("baremetal instance %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_cdn_origin_group_test.go b/edgecenter/test/resource_edgecenter_cdn_origin_group_test.go new file mode 100644 index 00000000..fe473ed2 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_cdn_origin_group_test.go @@ -0,0 +1,100 @@ +//go:build cdn + +package edgecenter_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccOriginGroup(t *testing.T) { + t.Parallel() + resourceName := "edgecenter_cdn_origingroup.acctest" + + type Params struct { + Source string + Enabled string + } + + create := Params{"google.com", "true"} + update := Params{"tut.by", "false"} + + template := func(params *Params) string { + return fmt.Sprintf(` + resource "edgecenter_cdn_origingroup" "acctest" { + name = "terraform_acctest_group" + use_next = true + + origin { + source = "%s" + enabled = %s + } + + origin { + source = "yandex.ru" + enabled = true + } + } + `, params.Source, params.Enabled) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckVars(t, EC_USERNAME_VAR, EC_PASSWORD_VAR, EC_CDN_URL_VAR) + }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: template(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "terraform_acctest_group"), + or( + resource.TestCheckResourceAttr(resourceName, "origin.0.source", create.Source), + resource.TestCheckResourceAttr(resourceName, "origin.1.source", create.Source), + ), + or( + resource.TestCheckResourceAttr(resourceName, "origin.0.enabled", create.Enabled), + resource.TestCheckResourceAttr(resourceName, "origin.1.enabled", create.Enabled), + ), + ), + }, + { + Config: template(&update), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", "terraform_acctest_group"), + or( + resource.TestCheckResourceAttr(resourceName, "origin.0.source", update.Source), + resource.TestCheckResourceAttr(resourceName, "origin.1.source", update.Source), + ), + or( + resource.TestCheckResourceAttr(resourceName, "origin.0.enabled", update.Enabled), + resource.TestCheckResourceAttr(resourceName, "origin.1.enabled", update.Enabled), + ), + ), + }, + }, + }) +} + +func or(checks ...resource.TestCheckFunc) resource.TestCheckFunc { + return func(t *terraform.State) error { + var composed string + + for _, check := range checks { + err := check(t) + if err == nil { + return nil + } + + composed += err.Error() + "; " + } + + return errors.New(composed) + } +} diff --git a/edgecenter/test/resource_edgecenter_cdn_resource_test.go b/edgecenter/test/resource_edgecenter_cdn_resource_test.go new file mode 100644 index 00000000..39d0cbf3 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_cdn_resource_test.go @@ -0,0 +1,62 @@ +//go:build cdn + +package edgecenter_test + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccCDNResource(t *testing.T) { + t.Parallel() + resourceName := "edgecenter_cdn_resource.acctest" + + type Params struct { + Proto string + } + + cname := fmt.Sprintf("cdn.terraform-%d.acctest", time.Now().Nanosecond()) + secondaryHostname := "secondary-" + cname + + create := Params{"HTTP"} + update := Params{"MATCH"} + + template := func(params *Params) string { + return fmt.Sprintf(` +resource "edgecenter_cdn_resource" "acctest" { + cname = "%s" + origin_group = %s + origin_protocol = "%s" + secondary_hostnames = ["%s"] +} + `, cname, EC_CDN_ORIGINGROUP_ID, params.Proto, secondaryHostname) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckVars(t, EC_USERNAME_VAR, EC_PASSWORD_VAR, EC_CDN_URL_VAR, EC_CDN_ORIGINGROUP_ID_VAR) + }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: template(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cname", cname), + resource.TestCheckResourceAttr(resourceName, "origin_protocol", create.Proto), + ), + }, + { + Config: template(&update), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "cname", cname), + resource.TestCheckResourceAttr(resourceName, "origin_protocol", update.Proto), + ), + }, + }, + }) +} diff --git a/edgecenter/test/resource_edgecenter_cdn_rule_test.go b/edgecenter/test/resource_edgecenter_cdn_rule_test.go new file mode 100644 index 00000000..51afad1c --- /dev/null +++ b/edgecenter/test/resource_edgecenter_cdn_rule_test.go @@ -0,0 +1,75 @@ +//go:build cdn + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccCDNRule(t *testing.T) { + t.Parallel() + resourceName := "edgecenter_cdn_rule.acctest" + + type Params struct { + Name string + Pattern string + RawPart string + } + + create := Params{ + Name: "All images", + Pattern: "/folder/images/*.png", + } + update := Params{ + Name: "All scripts", + Pattern: "/folder/scripts/*.js", + RawPart: ` + options { + host_header { + enabled = true + value = "rule-host.com" + } + } + `, + } + + template := func(params *Params) string { + return fmt.Sprintf(` +resource "edgecenter_cdn_rule" "acctest" { + resource_id = %s + name = "%s" + rule = "%s" + %s +} + `, EC_CDN_RESOURCE_ID, params.Name, params.Pattern, params.RawPart) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckVars(t, EC_USERNAME_VAR, EC_PASSWORD_VAR, EC_CDN_URL_VAR, EC_CDN_RESOURCE_ID_VAR) + }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: template(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", create.Name), + resource.TestCheckResourceAttr(resourceName, "rule", create.Pattern), + ), + }, + { + Config: template(&update), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", update.Name), + resource.TestCheckResourceAttr(resourceName, "rule", update.Pattern), + resource.TestCheckResourceAttr(resourceName, "options.0.host_header.0.value", "rule-host.com"), + ), + }, + }, + }) +} diff --git a/edgecenter/test/resource_edgecenter_cdn_sslcerts_test.go b/edgecenter/test/resource_edgecenter_cdn_sslcerts_test.go new file mode 100644 index 00000000..fbe1a06f --- /dev/null +++ b/edgecenter/test/resource_edgecenter_cdn_sslcerts_test.go @@ -0,0 +1,127 @@ +//go:build cdn + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccCDNCert(t *testing.T) { + t.Parallel() + resourceName := "edgecenter_cdn_sslcert.acctest" + template := fmt.Sprintf(` +resource "edgecenter_cdn_sslcert" "acctest" { + name = "Terraform acctest cert" + cert = < 0 { + for i, iface := range opts.Interfaces { + checksStore = append(checksStore, + resource.TestCheckResourceAttr(resourceName, fmt.Sprintf(`interfaces.%d.type`, i), iface.Type.String()), + resource.TestCheckResourceAttr(resourceName, fmt.Sprintf(`interfaces.%d.subnet_id`, i), iface.SubnetID), + ) + } + } + + for i, r := range opts.Routes { + checksStore = append(checksStore, + resource.TestCheckResourceAttr(resourceName, fmt.Sprintf(`routes.%d.destination`, i), r.Destination.String()), + resource.TestCheckResourceAttr(resourceName, fmt.Sprintf(`routes.%d.nexthop`, i), r.NextHop.String()), + ) + } + + return resource.ComposeTestCheckFunc(checksStore...)(s) + } +} + +func testAccRouterDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.RouterPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_router" { + continue + } + + _, err := routers.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("router still exists") + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_secret_test.go b/edgecenter/test/resource_edgecenter_secret_test.go new file mode 100644 index 00000000..00a5bfdf --- /dev/null +++ b/edgecenter/test/resource_edgecenter_secret_test.go @@ -0,0 +1,65 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/secret/v1/secrets" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSecret(t *testing.T) { + t.Parallel() + resourceName := "edgecenter_secret.acctest" + kpTemplate := fmt.Sprintf(` + resource "edgecenter_secret" "acctest" { + %s + %s + name = "%s" + private_key = %q + certificate = %q + certificate_chain = %q + expiration = "2025-12-28T19:14:44.213" + } + `, projectInfo(), regionInfo(), secretTestName, privateKey, certificate, certificateChain) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccSecretDestroy, + Steps: []resource.TestStep{ + { + Config: kpTemplate, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", secretTestName), + ), + }, + }, + }) +} + +func testAccSecretDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.SecretPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_secret" { + continue + } + + _, err := secrets.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("secret still exists") + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_securitygroup_test.go b/edgecenter/test/resource_edgecenter_securitygroup_test.go new file mode 100644 index 00000000..dd8ff9f1 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_securitygroup_test.go @@ -0,0 +1,108 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygroups" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSecurityGroup(t *testing.T) { + t.Parallel() + resourceName := "edgecenter_securitygroup.acctest" + + ipTemplate1 := fmt.Sprintf(` + resource "edgecenter_securitygroup" "acctest" { + %s + %s + name = "test" + metadata_map = { + key1 = "val1" + key2 = "val2" + } + security_group_rules { + direction = "egress" + ethertype = "IPv4" + protocol = "vrrp" + } + } + `, projectInfo(), regionInfo()) + + ipTemplate2 := fmt.Sprintf(` + resource "edgecenter_securitygroup" "acctest" { + %s + %s + name = "test" + metadata_map = { + key3 = "val3" + } + security_group_rules { + direction = "egress" + ethertype = "IPv4" + protocol = "vrrp" + } + } + `, projectInfo(), regionInfo()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccSecurityGroupDestroy, + Steps: []resource.TestStep{ + { + Config: ipTemplate1, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "metadata_map.key1", "val1"), + resource.TestCheckResourceAttr(resourceName, "metadata_map.key2", "val2"), + testAccCheckMetadata(t, resourceName, true, map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }), + ), + }, + { + Config: ipTemplate2, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "metadata_map.key3", "val3"), + testAccCheckMetadata(t, resourceName, true, map[string]interface{}{ + "key3": "val3", + }), + testAccCheckMetadata(t, resourceName, false, map[string]interface{}{ + "key1": "val1", + }), + testAccCheckMetadata(t, resourceName, false, map[string]interface{}{ + "key2": "val2", + }), + ), + }, + }, + }) +} + +func testAccSecurityGroupDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.SecurityGroupPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_securitygroup" { + continue + } + + _, err := securitygroups.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("SecurityGroup still exists") + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_servergroup_test.go b/edgecenter/test/resource_edgecenter_servergroup_test.go new file mode 100644 index 00000000..e26dca83 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_servergroup_test.go @@ -0,0 +1,76 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/servergroup/v1/servergroups" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccServerGroupResource(t *testing.T) { + t.Parallel() + type Params struct { + Name string + Policy string + } + + create := Params{ + Name: "test", + Policy: servergroups.AntiAffinityPolicy.String(), + } + + resourceName := "edgecenter_servergroup.acctest" + + kpTemplate := func(params *Params) string { + return fmt.Sprintf(` + resource "edgecenter_servergroup" "acctest" { + %s + %s + name = "%s" + policy = "%s" + } + `, projectInfo(), regionInfo(), params.Name, params.Policy) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccServerGroupDestroy, + Steps: []resource.TestStep{ + { + Config: kpTemplate(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", create.Name), + resource.TestCheckResourceAttr(resourceName, "policy", create.Policy), + ), + }, + }, + }) +} + +func testAccServerGroupDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.ServerGroupsPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_servergroup" { + continue + } + + _, err := servergroups.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("ServerGroup %s still exists", rs.Primary.ID) + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_snapshot_test.go b/edgecenter/test/resource_edgecenter_snapshot_test.go new file mode 100644 index 00000000..860eaa2a --- /dev/null +++ b/edgecenter/test/resource_edgecenter_snapshot_test.go @@ -0,0 +1,124 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/volume/v1/volumes" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSnapshot(t *testing.T) { + t.Parallel() + cfg, err := createTestConfig() + if err != nil { + t.Fatal(err) + } + + client, err := createTestClient(cfg.Provider, edgecenter.VolumesPoint, edgecenter.VersionPointV1) + if err != nil { + t.Fatal(err) + } + + opts := volumes.CreateOpts{ + Name: volumeTestName, + Size: volumeSizeTest, + Source: volumes.NewVolume, + TypeName: volumes.Standard, + } + + volumeID, err := createTestVolume(client, opts) + if err != nil { + t.Fatal(err) + } + + defer volumes.Delete(client, volumeID, volumes.DeleteOpts{}) + + type Params struct { + Name string + Description string + Status string + Size int + VolumeID string + } + + create := Params{ + Name: "test", + VolumeID: volumeID, + } + + update := Params{ + Name: "test", + VolumeID: volumeID, + } + + resourceName := "edgecenter_snapshot.acctest" + importStateIDPrefix := fmt.Sprintf("%s:%s:", os.Getenv("TEST_PROJECT_ID"), os.Getenv("TEST_REGION_ID")) + + SnapshotTemplate := func(params *Params) string { + additional := fmt.Sprintf("%s\n %s", regionInfo(), projectInfo()) + + template := fmt.Sprintf(` + resource "edgecenter_snapshot" "acctest" { + name = "%s" + volume_id = "%s" + %s + `, params.Name, params.VolumeID, additional) + + return template + "\n}" + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccSnapshotDestroy, + Steps: []resource.TestStep{ + { + Config: SnapshotTemplate(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "volume_id", create.VolumeID), + ), + }, + { + Config: SnapshotTemplate(&update), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "volume_id", update.VolumeID), + ), + }, + { + ImportStateIdPrefix: importStateIDPrefix, + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +func testAccSnapshotDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.SnapshotsPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_snapshot" { + continue + } + + _, err := networks.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("snapshot still exists") + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_storage_s3_bucket_test.go b/edgecenter/test/resource_edgecenter_storage_s3_bucket_test.go new file mode 100644 index 00000000..fa81d738 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_storage_s3_bucket_test.go @@ -0,0 +1,81 @@ +//go:build storage + +package edgecenter_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecenter-storage-sdk-go/swagger/client/storages" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccStorageS3Bucket(t *testing.T) { + t.Parallel() + random := time.Now().Nanosecond() + storageResourceName := fmt.Sprintf("edgecenter_storage_s3.terraform_test_%d_s3", random) + bucketResourceName := fmt.Sprintf("edgecenter_storage_s3_bucket.terraform_test_%d_s3_bucket", random) + name := fmt.Sprintf("terraform_test_%d", random) + + templateCreateBucket := func() string { + return fmt.Sprintf(` +resource "edgecenter_storage_s3" "terraform_test_%d_s3" { + name = "terraform_test_%d" + location = "s-ed1" +} + +resource "edgecenter_storage_s3_bucket" "terraform_test_%d_s3_bucket" { + name = "terraform_test_%d" + storage_id = %s.id +} + `, random, random, random, random, storageResourceName) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckVars(t, EC_USERNAME_VAR, EC_PASSWORD_VAR, EC_STORAGE_URL_VAR) + }, + CheckDestroy: func(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_storage_s3" { + continue + } + opts := []func(opt *storages.StorageListHTTPV2Params){ + func(opt *storages.StorageListHTTPV2Params) { opt.Context = ctx }, + func(opt *storages.StorageListHTTPV2Params) { opt.ID = &rs.Primary.ID }, + } + storages, err := config.StorageClient.StoragesList(opts...) + if err != nil { + return fmt.Errorf("find storage: %w", err) + } + if len(storages) == 0 { + return nil + } + if storages[0].ProvisioningStatus == "ok" { + return fmt.Errorf("storage #%s wasn't deleted correctrly", rs.Primary.ID) + } + } + + return nil + }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: templateCreateBucket(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(bucketResourceName), + resource.TestCheckResourceAttr(bucketResourceName, edgecenter.StorageS3BucketSchemaName, name), + ), + }, + }, + }) +} diff --git a/edgecenter/test/resource_edgecenter_storage_s3_test.go b/edgecenter/test/resource_edgecenter_storage_s3_test.go new file mode 100644 index 00000000..30a5f8a4 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_storage_s3_test.go @@ -0,0 +1,74 @@ +//go:build storage + +package edgecenter_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecenter-storage-sdk-go/swagger/client/storages" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccStorageS3(t *testing.T) { + t.Parallel() + random := time.Now().Nanosecond() + resourceName := fmt.Sprintf("edgecenter_storage_s3.terraform_test_%d_s3", random) + + templateCreate := func() string { + return fmt.Sprintf(` +resource "edgecenter_storage_s3" "terraform_test_%d_s3" { + name = "terraform_test_%d" + location = "s-ed1" +} + `, random, random) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheckVars(t, EC_USERNAME_VAR, EC_PASSWORD_VAR, EC_STORAGE_URL_VAR) + }, + CheckDestroy: func(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_storage_s3" { + continue + } + opts := []func(opt *storages.StorageListHTTPV2Params){ + func(opt *storages.StorageListHTTPV2Params) { opt.Context = ctx }, + func(opt *storages.StorageListHTTPV2Params) { opt.ID = &rs.Primary.ID }, + } + storages, err := config.StorageClient.StoragesList(opts...) + if err != nil { + return fmt.Errorf("find storage: %w", err) + } + if len(storages) == 0 { + return nil + } + if storages[0].ProvisioningStatus == "ok" { + return fmt.Errorf("storage #%s wasn't deleted correctrly", rs.Primary.ID) + } + } + + return nil + }, + ProviderFactories: testAccProviders, + Steps: []resource.TestStep{ + { + Config: templateCreate(), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, edgecenter.StorageSchemaLocation, "s-ed1"), + ), + }, + }, + }) +} diff --git a/edgecenter/test/resource_edgecenter_subnet_test.go b/edgecenter/test/resource_edgecenter_subnet_test.go new file mode 100644 index 00000000..2e519450 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_subnet_test.go @@ -0,0 +1,238 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "net" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccSubnet(t *testing.T) { + t.Parallel() + var dst1, dst2, cidr edgecloud.CIDR + + _, netIPNet, _ := net.ParseCIDR("10.0.3.0/24") + dst1.IP = netIPNet.IP + dst1.Mask = netIPNet.Mask + + _, netIPNet, _ = net.ParseCIDR("10.0.4.0/24") + dst2.IP = netIPNet.IP + dst2.Mask = netIPNet.Mask + + _, netIPNet, _ = net.ParseCIDR("192.168.10.0/24") + cidr.IP = netIPNet.IP + cidr.Mask = netIPNet.Mask + + createFixt := subnets.CreateOpts{ + Name: "create_subnet", + CIDR: cidr, + DNSNameservers: []net.IP{net.ParseIP("8.8.4.4"), net.ParseIP("1.1.1.1")}, + EnableDHCP: true, + HostRoutes: []subnets.HostRoute{ + { + Destination: dst1, + NextHop: net.ParseIP("192.168.10.1"), + }, + { + Destination: dst2, + NextHop: net.ParseIP("192.168.10.1"), + }, + }, + } + + gateway := net.ParseIP("disable") + + updateFixt := subnets.CreateOpts{ + Name: "update_subnet", + CIDR: cidr, + DNSNameservers: make([]net.IP, 0), + EnableDHCP: false, + HostRoutes: make([]subnets.HostRoute, 0), + GatewayIP: &gateway, + } + + type Params struct { + Name string + CIDR string + DNS []string + HRoutes []map[string]string + DHCP string + Gateway string + MetadataMap string + } + + create := Params{ + Name: "create_subnet", + CIDR: "192.168.10.0/24", + DNS: []string{"8.8.4.4", "1.1.1.1"}, + HRoutes: []map[string]string{ + {"destination": "10.0.3.0/24", "nexthop": "192.168.10.1"}, + {"destination": "10.0.4.0/24", "nexthop": "192.168.10.1"}, + }, + MetadataMap: `{ + key1 = "val1" + key2 = "val2" + }`, + } + + update := Params{ + Name: "update_subnet", + CIDR: "192.168.10.0/24", + DHCP: "false", + DNS: []string{}, + HRoutes: []map[string]string{}, + Gateway: "disable", + MetadataMap: `{ + key3 = "val3" + }`, + } + + SubnetTemplate := func(params *Params) string { + template := ` + locals { + dns_nameservers = [` + + for i := range params.DNS { + template += fmt.Sprintf(`"%s",`, params.DNS[i]) + } + + template += fmt.Sprint(`] + host_routes = [`) + + for i := range params.HRoutes { + template += fmt.Sprintf(` + { + destination = "%s" + nexthop = "%s" + },`, params.HRoutes[i]["destination"], params.HRoutes[i]["nexthop"]) + } + + template += fmt.Sprintf(`] + } + + resource "edgecenter_network" "acctest" { + name = "create_network" + type = "vxlan" + create_router = false + %[1]s + %[2]s + } + + resource "edgecenter_subnet" "acctest" { + name = "%s" + cidr = "%s" + network_id = edgecenter_network.acctest.id + dns_nameservers = local.dns_nameservers + connect_to_network_router = false + dynamic host_routes { + iterator = hr + for_each = local.host_routes + content { + destination = hr.value.destination + nexthop = hr.value.nexthop + } + } + metadata_map = %s + %[1]s + %[2]s + + + `, regionInfo(), projectInfo(), params.Name, params.CIDR, params.MetadataMap) + + if params.DHCP != "" { + template += fmt.Sprintf("enable_dhcp = %s\n", params.DHCP) + } + + if params.Gateway != "" { + template += fmt.Sprintf(`gateway_ip = "%s"`, params.Gateway) + } + + return template + "\n}" + } + + resourceName := "edgecenter_subnet.acctest" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccSubnetDestroy, + Steps: []resource.TestStep{ + { + Config: SubnetTemplate(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + checkSubnetAttrs(resourceName, &createFixt), + testAccCheckMetadata(t, resourceName, true, map[string]interface{}{ + "key1": "val1", + "key2": "val2", + }), + ), + }, + { + Config: SubnetTemplate(&update), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + checkSubnetAttrs(resourceName, &updateFixt), + ), + }, + }, + }) +} + +func checkSubnetAttrs(resourceName string, opts *subnets.CreateOpts) resource.TestCheckFunc { + return func(s *terraform.State) error { + if s.Empty() == true { + return fmt.Errorf("State not updated") + } + + checksStore := []resource.TestCheckFunc{ + resource.TestCheckResourceAttr(resourceName, "name", opts.Name), + resource.TestCheckResourceAttr(resourceName, "cidr", opts.CIDR.String()), + resource.TestCheckResourceAttr(resourceName, "enable_dhcp", strconv.FormatBool(opts.EnableDHCP)), + resource.TestCheckResourceAttr(resourceName, "dns_nameservers.#", strconv.Itoa(len(opts.DNSNameservers))), + resource.TestCheckResourceAttr(resourceName, "host_routes.#", strconv.Itoa(len(opts.HostRoutes))), + } + + if opts.GatewayIP == nil && !opts.EnableDHCP { + checksStore = append(checksStore, resource.TestCheckResourceAttr(resourceName, "gateway_ip", "disable")) + } + + for i, hr := range opts.HostRoutes { + checksStore = append(checksStore, + resource.TestCheckResourceAttr(resourceName, fmt.Sprintf(`host_routes.%d.destination`, i), hr.Destination.String()), + resource.TestCheckResourceAttr(resourceName, fmt.Sprintf(`host_routes.%d.nexthop`, i), hr.NextHop.String()), + ) + } + + return resource.ComposeTestCheckFunc(checksStore...)(s) + } +} + +func testAccSubnetDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.SubnetPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_subnet" { + continue + } + + _, err := subnets.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("subnet still exists") + } + } + + return nil +} diff --git a/edgecenter/test/resource_edgecenter_volume_test.go b/edgecenter/test/resource_edgecenter_volume_test.go new file mode 100644 index 00000000..bedcda72 --- /dev/null +++ b/edgecenter/test/resource_edgecenter_volume_test.go @@ -0,0 +1,114 @@ +//go:build cloud_resource + +package edgecenter_test + +import ( + "fmt" + "os" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestAccVolume(t *testing.T) { + t.Parallel() + type Params struct { + Name string + Size int + Type string + Source string + SnapshotID string + ImageID string + } + + create := Params{ + Name: "test", + Size: 1, + Type: "standard", + } + + update := Params{ + Name: "test2", + Size: 2, + Type: "ssd_hiiops", + } + + resourceName := "edgecenter_volume.acctest" + importStateIDPrefix := fmt.Sprintf("%s:%s:", os.Getenv("TEST_PROJECT_ID"), os.Getenv("TEST_REGION_ID")) + + VolumeTemplate := func(params *Params) string { + additional := fmt.Sprintf("%s\n %s", regionInfo(), projectInfo()) + if params.SnapshotID != "" { + additional += fmt.Sprintf(`%s snapshot_id = "%s"`, "\n", params.SnapshotID) + } + if params.ImageID != "" { + additional += fmt.Sprintf(`%s image_id = "%s"`, "\n", params.ImageID) + } + + template := fmt.Sprintf(` + resource "edgecenter_volume" "acctest" { + name = "%s" + size = %d + type_name = "%s" + %s + `, params.Name, params.Size, params.Type, additional) + + return template + "\n}" + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: VolumeTemplate(&create), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "size", strconv.Itoa(create.Size)), + resource.TestCheckResourceAttr(resourceName, "type_name", create.Type), + resource.TestCheckResourceAttr(resourceName, "name", create.Name), + ), + }, + { + Config: VolumeTemplate(&update), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "size", strconv.Itoa(update.Size)), + resource.TestCheckResourceAttr(resourceName, "type_name", update.Type), + resource.TestCheckResourceAttr(resourceName, "name", update.Name), + ), + }, + { + ImportStateIdPrefix: importStateIDPrefix, + ResourceName: resourceName, + ImportState: true, + }, + }, + }) +} + +func testAccVolumeDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*edgecenter.Config) + client, err := createTestClient(config.Provider, edgecenter.VolumesPoint, edgecenter.VersionPointV1) + if err != nil { + return err + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "edgecenter_volume" { + continue + } + + _, err := networks.Get(client, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("volume still exists") + } + } + + return nil +} diff --git a/edgecenter/test/utils_metadata_test.go b/edgecenter/test/utils_metadata_test.go new file mode 100644 index 00000000..f44f7193 --- /dev/null +++ b/edgecenter/test/utils_metadata_test.go @@ -0,0 +1,141 @@ +//go:build cloud_data_source || cloud_resource + +package edgecenter_test + +import ( + "fmt" + "reflect" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func normalizeMetadata(metadata interface{}, defaults ...bool) (map[string]interface{}, error) { + normalizedMetadata := map[string]interface{}{} + readOnly := false + + if len(defaults) > 0 { + readOnly = defaults[0] + } + + switch metadata := metadata.(type) { + default: + return nil, fmt.Errorf("unexpected type %T", metadata) + case []map[string]interface{}: + for _, v := range metadata { + normalizedMetadata[v["key"].(string)] = v + } + case map[string]interface{}: + for k, v := range metadata { + normalizedMetadata[k] = map[string]interface{}{ + "key": k, + "value": v, + "read_only": readOnly, + } + } + case map[string]string: + for k, v := range metadata { + normalizedMetadata[k] = map[string]interface{}{ + "key": k, + "value": v, + "read_only": readOnly, + } + } + } + + return normalizedMetadata, nil +} + +func modulePrimaryInstanceState(ms *terraform.ModuleState, name string) (*terraform.InstanceState, error) { + rs, ok := ms.Resources[name] + if !ok { + return nil, fmt.Errorf("not found: %s in %s", name, ms.Path) + } + + is := rs.Primary + if is == nil { + return nil, fmt.Errorf("no primary instance: %s in %s", name, ms.Path) + } + + return is, nil +} + +func getMetadataFromResourceAttributes(prefix string, attributes *map[string]string) ([]map[string]interface{}, error) { + metadataLength, err := strconv.Atoi((*attributes)[prefix+".#"]) + if err != nil { + return nil, err + } + metadata := make([]map[string]interface{}, metadataLength) + buildKey := func(idx int, name string) string { + return fmt.Sprintf("%v.%v.%v", prefix, idx, name) + } + + for i := 0; i < metadataLength; i++ { + readOnly, err := strconv.ParseBool((*attributes)[buildKey(i, "read_only")]) + if err != nil { + return nil, err + } + metadata[i] = map[string]interface{}{ + "key": (*attributes)[buildKey(i, "key")], + "value": (*attributes)[buildKey(i, "value")], + "read_only": readOnly, + } + } + + return metadata, nil +} + +func checkMapInMap(srcMap map[string]interface{}, dstMap map[string]interface{}) bool { + if len(srcMap) > len(dstMap) { + return false + } + if len(srcMap) == len(dstMap) { + return reflect.DeepEqual(srcMap, dstMap) + } + slicedMap := make(map[string]interface{}, len(srcMap)) + + for k := range srcMap { + if val, ok := dstMap[k]; ok { + slicedMap[k] = val + } else { + return false + } + } + + return reflect.DeepEqual(srcMap, slicedMap) +} + +func testAccCheckMetadata(t *testing.T, name string, isMetaExists bool, metadataForCheck interface{}) resource.TestCheckFunc { + t.Helper() + return func(s *terraform.State) error { + // retrieve the resource by name from state + ms := s.RootModule() + is, err := modulePrimaryInstanceState(ms, name) + if err != nil { + return err + } + + instanceMetadata, err := getMetadataFromResourceAttributes("metadata_read_only", &is.Attributes) + if err != nil { + return err + } + + mt1, err := normalizeMetadata(metadataForCheck) + if err != nil { + return err + } + + mt2, err := normalizeMetadata(instanceMetadata) + if err != nil { + return err + } + + if !(checkMapInMap(mt1, mt2) == isMetaExists) { + return fmt.Errorf("metadata not exist") + } + + return nil + } +} diff --git a/edgecenter/test/utils_test.go b/edgecenter/test/utils_test.go new file mode 100644 index 00000000..acb35537 --- /dev/null +++ b/edgecenter/test/utils_test.go @@ -0,0 +1,67 @@ +package edgecenter_test + +import ( + "testing" + + "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter" +) + +func TestExtractHostAndPath(t *testing.T) { + t.Parallel() + + type args struct { + uri string + } + tests := []struct { + name string + args args + wantHost string + wantPath string + wantErr bool + }{ + { + name: "long url success", + args: args{ + uri: "https://test.url/with/path", + }, + wantHost: "https://test.url", + wantPath: "/with/path", + wantErr: false, + }, + { + name: "short url success", + args: args{ + uri: "https://test.url", + }, + wantHost: "https://test.url", + wantPath: "", + wantErr: false, + }, + { + name: "error on empty", + args: args{ + uri: "", + }, + wantHost: "", + wantPath: "", + wantErr: true, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotHost, gotPath, err := edgecenter.ExtractHostAndPath(tt.args.uri) + if (err != nil) != tt.wantErr { + t.Errorf("ExtractHostAndPath() error = %v, wantErr %v", err, tt.wantErr) + return + } + if gotHost != tt.wantHost { + t.Errorf("ExtractHostAndPath() gotHost = %v, want %v", gotHost, tt.wantHost) + } + if gotPath != tt.wantPath { + t.Errorf("ExtractHostAndPath() gotPath = %v, want %v", gotPath, tt.wantPath) + } + }) + } +} diff --git a/edgecenter/utils.go b/edgecenter/utils.go new file mode 100644 index 00000000..0a667a25 --- /dev/null +++ b/edgecenter/utils.go @@ -0,0 +1,137 @@ +package edgecenter + +import ( + "fmt" + "log" + "net/url" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/mitchellh/mapstructure" + + dnsSDK "github.com/Edge-Center/edgecenter-dns-sdk-go" + storageSDK "github.com/Edge-Center/edgecenter-storage-sdk-go" + cdn "github.com/Edge-Center/edgecentercdn-go" + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter" +) + +const ( + VersionPointV1 = "v1" + VersionPointV2 = "v2" + + ProjectPoint = "projects" + RegionPoint = "regions" +) + +type Config struct { + Provider *edgecloud.ProviderClient + CDNClient cdn.ClientService + StorageClient *storageSDK.SDK + DNSClient *dnsSDK.Client +} + +// MapStructureDecoder decodes the given map into the provided structure using the specified decoder configuration. +func MapStructureDecoder(strct interface{}, v *map[string]interface{}, config *mapstructure.DecoderConfig) error { + config.Result = strct + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return fmt.Errorf("failed to create decoder: %w", err) + } + + return decoder.Decode(*v) +} + +// ImportStringParser parses a string containing project ID, region ID, and another field, +// and returns them as separate values along with any error encountered. +func ImportStringParser(infoStr string) (projectID int, regionID int, id3 string, err error) { //nolint: nonamedreturns + log.Printf("[DEBUG] Input id string: %s", infoStr) + infoStrings := strings.Split(infoStr, ":") + if len(infoStrings) != 3 { + err = fmt.Errorf("failed import: wrong input id: %s", infoStr) + return + } + + id1, id2, id3 := infoStrings[0], infoStrings[1], infoStrings[2] + + projectID, err = strconv.Atoi(id1) + if err != nil { + return + } + regionID, err = strconv.Atoi(id2) + if err != nil { + return + } + + return +} + +// CreateClient creates a new edgecloud.ServiceClient. +func CreateClient(provider *edgecloud.ProviderClient, d *schema.ResourceData, endpoint string, version string) (*edgecloud.ServiceClient, error) { + projectID, err := GetProject(provider, d.Get("project_id").(int), d.Get("project_name").(string)) + if err != nil { + return nil, err + } + + regionID := 0 + + rawRegionID := d.Get("region_id") + rawRegionName := d.Get("region_name") + if rawRegionID != nil && rawRegionName != nil { + regionID, err = GetRegion(provider, rawRegionID.(int), rawRegionName.(string)) + if err != nil { + return nil, fmt.Errorf("failed to get region: %w", err) + } + } + + client, err := edgecenter.ClientServiceFromProvider(provider, edgecloud.EndpointOpts{ + Name: endpoint, + Region: regionID, + Project: projectID, + Version: version, + }) + if err != nil { + return nil, fmt.Errorf("failed to create client: %w", err) + } + + return client, nil +} + +// revertState reverts the state of the specified fields in the given schema.ResourceData if "last_updated" is not empty. +// It takes a schema.ResourceData and a slice of strings containing the field names to be reverted as input arguments. +func revertState(d *schema.ResourceData, fields *[]string) { + if d.Get("last_updated").(string) != "" { + for _, field := range *fields { + if d.HasChange(field) { + oldValue, _ := d.GetChange(field) + switch v := oldValue.(type) { + case int: + d.Set(field, v) + case string: + d.Set(field, v) + case map[string]interface{}: + d.Set(field, v) + } + } + log.Printf("[DEBUG] Revert (%s) '%s' field", d.Id(), field) + } + } +} + +// ExtractHostAndPath splits a given URI into the host and path components. +func ExtractHostAndPath(uri string) (string, string, error) { + var host, path string + if uri == "" { + return host, path, fmt.Errorf("empty uri") + } + + pURL, err := url.Parse(uri) + if err != nil { + return host, path, fmt.Errorf("url parse: %w", err) + } + host = pURL.Scheme + "://" + pURL.Host + path = pURL.Path + + return host, path, nil +} diff --git a/edgecenter/utils_instance.go b/edgecenter/utils_instance.go new file mode 100644 index 00000000..9d9a6ca3 --- /dev/null +++ b/edgecenter/utils_instance.go @@ -0,0 +1,527 @@ +package edgecenter + +import ( + "crypto/md5" + "encoding/binary" + "errors" + "fmt" + "io" + "log" + "reflect" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/mitchellh/mapstructure" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/instances" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/instance/v1/types" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygroups" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/servergroup/v1/servergroups" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/task/v1/tasks" +) + +var instanceDecoderConfig = &mapstructure.DecoderConfig{ + TagName: "json", +} + +type instanceInterfaces []interface{} + +func (s instanceInterfaces) Len() int { + return len(s) +} + +func (s instanceInterfaces) Less(i, j int) bool { + ifLeft := s[i].(map[string]interface{}) + ifRight := s[j].(map[string]interface{}) + + // only bm instance has a parent interface, and it should be attached first + isTrunkLeft, okLeft := ifLeft["is_parent"] + isTrunkRight, okRight := ifRight["is_parent"] + if okLeft && okRight { + left, _ := isTrunkLeft.(bool) + right, _ := isTrunkRight.(bool) + switch { + case left && !right: + return true + case right && !left: + return false + } + } + + lOrder, _ := ifLeft["order"].(int) + rOrder, _ := ifRight["order"].(int) + + return lOrder < rOrder +} + +func (s instanceInterfaces) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +type OrderedInterfaceOpts struct { + instances.InterfaceOpts + Order int +} + +// decodeInstanceInterfaceOpts decodes the interface and returns InterfaceOpts with FloatingIP. +func decodeInstanceInterfaceOpts(iFaceMap map[string]interface{}) (instances.InterfaceOpts, error) { + var interfaceOpts instances.InterfaceOpts + err := MapStructureDecoder(&interfaceOpts, &iFaceMap, instanceDecoderConfig) + if err != nil { + return interfaceOpts, err + } + + if fipSource := iFaceMap["fip_source"].(string); fipSource != "" { + var fip instances.CreateNewInterfaceFloatingIPOpts + if existingFipID := iFaceMap["existing_fip_id"].(string); existingFipID != "" { + fip.Source = types.ExistingFloatingIP + fip.ExistingFloatingID = existingFipID + } else { + fip.Source = types.NewFloatingIP + } + interfaceOpts.FloatingIP = &fip + } + + return interfaceOpts, nil +} + +// extractInstanceInterfaceToListCreate creates a list of InterfaceInstanceCreateOpts objects from a list of interfaces. +func extractInstanceInterfaceToListCreate(interfaces []interface{}) ([]instances.InterfaceInstanceCreateOpts, error) { + interfaceInstanceCreateOptsList := make([]instances.InterfaceInstanceCreateOpts, 0) + for _, iFace := range interfaces { + iFaceMap := iFace.(map[string]interface{}) + + interfaceOpts, err := decodeInstanceInterfaceOpts(iFaceMap) + if err != nil { + return nil, err + } + + rawSgsID := iFaceMap["security_groups"].([]interface{}) + sgs := make([]edgecloud.ItemID, len(rawSgsID)) + for i, sgID := range rawSgsID { + sgs[i] = edgecloud.ItemID{ID: sgID.(string)} + } + + interfaceInstanceCreateOpts := instances.InterfaceInstanceCreateOpts{ + InterfaceOpts: interfaceOpts, + SecurityGroups: sgs, + } + interfaceInstanceCreateOptsList = append(interfaceInstanceCreateOptsList, interfaceInstanceCreateOpts) + } + + return interfaceInstanceCreateOptsList, nil +} + +// extractInstanceInterfaceToListRead creates a list of InterfaceOpts objects from a list of interfaces. +func extractInstanceInterfaceToListRead(interfaces []interface{}) ([]instances.InterfaceOpts, error) { + interfaceOptsList := make([]instances.InterfaceOpts, 0) + for _, iFace := range interfaces { + if iFace == nil { + continue + } + + iFaceMap := iFace.(map[string]interface{}) + interfaceOpts, err := decodeInstanceInterfaceOpts(iFaceMap) + if err != nil { + return nil, err + } + interfaceOptsList = append(interfaceOptsList, interfaceOpts) + } + + return interfaceOptsList, nil +} + +// extractMetadataMap converts a map of metadata into a metadata set options structure. +func extractMetadataMap(metadata map[string]interface{}) instances.MetadataSetOpts { + result := make([]instances.MetadataOpts, 0, len(metadata)) + for k, v := range metadata { + result = append(result, instances.MetadataOpts{Key: k, Value: v.(string)}) + } + return instances.MetadataSetOpts{Metadata: result} +} + +// extractInstanceVolumesMap converts a slice of instance volumes into a map of volume IDs to boolean values. +func extractInstanceVolumesMap(volumes []interface{}) map[string]bool { + result := make(map[string]bool) + for _, volume := range volumes { + v := volume.(map[string]interface{}) + result[v["volume_id"].(string)] = true + } + return result +} + +// extractVolumesIntoMap converts a slice of volumes into a map with volume_id as the key. +func extractVolumesIntoMap(volumes []interface{}) map[string]map[string]interface{} { + result := make(map[string]map[string]interface{}, len(volumes)) + for _, volume := range volumes { + vol := volume.(map[string]interface{}) + result[vol["volume_id"].(string)] = vol + } + return result +} + +// extractKeyValue takes a slice of metadata interfaces and converts it into an instances.MetadataSetOpts structure. +func extractKeyValue(metadata []interface{}) (instances.MetadataSetOpts, error) { + metaData := make([]instances.MetadataOpts, len(metadata)) + var metadataSetOpts instances.MetadataSetOpts + for i, meta := range metadata { + md := meta.(map[string]interface{}) + var MD instances.MetadataOpts + err := MapStructureDecoder(&MD, &md, instanceDecoderConfig) + if err != nil { + return metadataSetOpts, err + } + metaData[i] = MD + } + metadataSetOpts.Metadata = metaData + + return metadataSetOpts, nil +} + +// volumeUniqueID generates a unique ID for a volume based on its volume_id attribute. +func volumeUniqueID(i interface{}) int { + e := i.(map[string]interface{}) + h := md5.New() + io.WriteString(h, e["volume_id"].(string)) + return int(binary.BigEndian.Uint64(h.Sum(nil))) +} + +// isInterfaceAttached checks if an interface is attached to a list of instances.Interface objects based on the subnet ID or external interface type. +func isInterfaceAttached(ifs []instances.Interface, ifs2 map[string]interface{}) bool { + subnetID, _ := ifs2["subnet_id"].(string) + iType := types.InterfaceType(ifs2["type"].(string)) + for _, i := range ifs { + if iType == types.ExternalInterfaceType && i.NetworkDetails.External { + return true + } + for _, assignment := range i.IPAssignments { + if assignment.SubnetID == subnetID { + return true + } + } + for _, subPort := range i.SubPorts { + if iType == types.ExternalInterfaceType && subPort.NetworkDetails.External { + return true + } + for _, assignment := range subPort.IPAssignments { + if assignment.SubnetID == subnetID { + return true + } + } + } + } + + return false +} + +// extractVolumesMap takes a slice of volume interfaces and converts it into a slice of instances.CreateVolumeOpts. +func extractVolumesMap(volumes []interface{}) ([]instances.CreateVolumeOpts, error) { + vols := make([]instances.CreateVolumeOpts, len(volumes)) + for i, volume := range volumes { + vol := volume.(map[string]interface{}) + var V instances.CreateVolumeOpts + err := MapStructureDecoder(&V, &vol, instanceDecoderConfig) + if err != nil { + return nil, err + } + vols[i] = V + } + + return vols, nil +} + +// isInterfaceContains checks if a given verifiable interface is present in the provided set of interfaces (ifsSet). +func isInterfaceContains(verifiable map[string]interface{}, ifsSet []interface{}) bool { + verifiableType := verifiable["type"].(string) + verifiableSubnetID, _ := verifiable["subnet_id"].(string) + for _, e := range ifsSet { + i := e.(map[string]interface{}) + iType := i["type"].(string) + subnetID, _ := i["subnet_id"].(string) + if iType == types.ExternalInterfaceType.String() && verifiableType == types.ExternalInterfaceType.String() { + return true + } + + if iType == verifiableType && subnetID == verifiableSubnetID { + return true + } + } + + return false +} + +// ServerV2StateRefreshFunc returns a StateRefreshFunc to track the state of an instance using its instanceID. +func ServerV2StateRefreshFunc(client *edgecloud.ServiceClient, instanceID string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + s, err := instances.Get(client, instanceID).Extract() + if err != nil { + var errDefault404 edgecloud.Default404Error + if errors.As(err, &errDefault404) { + return s, "DELETED", nil + } + return nil, "", err + } + + return s, s.VMState, nil + } +} + +// findInstancePort searches for the instance port with the specified portID in the given list of instance ports. +func findInstancePort(portID string, ports []instances.InstancePorts) (instances.InstancePorts, error) { + for _, port := range ports { + if port.ID == portID { + return port, nil + } + } + + return instances.InstancePorts{}, fmt.Errorf("port not found") +} + +// contains check if slice contains the element. +func contains[K comparable](slice []K, elm K) bool { + for _, s := range slice { + if s == elm { + return true + } + } + return false +} + +// getMapDifference compares two maps and returns a map of only different values. +// uncheckedKeys - list of keys to skip when comparing. +func getMapDifference(iMapOld, iMapNew map[string]interface{}, uncheckedKeys []string) map[string]interface{} { + differentFields := make(map[string]interface{}) + + for oldMapK, oldMapV := range iMapOld { + if contains(uncheckedKeys, oldMapK) { + continue + } + + if newMapV, ok := iMapNew[oldMapK]; !ok || !reflect.DeepEqual(newMapV, oldMapV) { + differentFields[oldMapK] = oldMapV + } + } + + for newMapK, newMapV := range iMapNew { + if contains(uncheckedKeys, newMapK) { + continue + } + + if _, ok := iMapOld[newMapK]; !ok { + differentFields[newMapK] = newMapV + } + } + + return differentFields +} + +// detachInterfaceFromInstance detaches interface from an instance. +func detachInterfaceFromInstance(client *edgecloud.ServiceClient, instanceID string, iface map[string]interface{}) error { + var opts instances.InterfaceOpts + opts.PortID = iface["port_id"].(string) + opts.IPAddress = iface["ip_address"].(string) + + log.Printf("[DEBUG] detach interface: %+v", opts) + results, err := instances.DetachInterface(client, instanceID, opts).Extract() + if err != nil { + return err + } + + err = tasks.WaitTaskAndProcessResult(client, results.Tasks[0], true, InstanceCreatingTimeout, func(task tasks.TaskID) error { + if taskInfo, err := tasks.Get(client, string(task)).Extract(); err != nil { + return fmt.Errorf("cannot get task with ID: %s. Error: %w, task: %+v", task, err, taskInfo) + } + return nil + }) + if err != nil { + return err + } + + return nil +} + +// attachInterfaceToInstance attach interface to instance. +func attachInterfaceToInstance(instanceClient *edgecloud.ServiceClient, instanceID string, iface map[string]interface{}) error { + iType := types.InterfaceType(iface["type"].(string)) + opts := instances.InterfaceInstanceCreateOpts{ + InterfaceOpts: instances.InterfaceOpts{Type: iType}, + } + + switch iType { //nolint: exhaustive + case types.SubnetInterfaceType: + opts.SubnetID = iface["subnet_id"].(string) + case types.AnySubnetInterfaceType: + opts.NetworkID = iface["network_id"].(string) + case types.ReservedFixedIPType: + opts.PortID = iface["port_id"].(string) + } + opts.SecurityGroups = getSecurityGroupsIDs(iface["security_groups"].([]interface{})) + + log.Printf("[DEBUG] attach interface: %+v", opts) + results, err := instances.AttachInterface(instanceClient, instanceID, opts).Extract() + if err != nil { + return fmt.Errorf("cannot attach interface: %s. Error: %w", iType, err) + } + + err = tasks.WaitTaskAndProcessResult(instanceClient, results.Tasks[0], true, InstanceCreatingTimeout, func(task tasks.TaskID) error { + taskInfo, err := tasks.Get(instanceClient, string(task)).Extract() + if err != nil { + return fmt.Errorf("cannot get task with ID: %s. Error: %w, task: %+v", task, err, taskInfo) + } + + if _, err := instances.ExtractInstancePortIDFromTask(taskInfo); err != nil { + reservedFixedIPID, ok := (*taskInfo.Data)["reserved_fixed_ip_id"] + if !ok || reservedFixedIPID.(string) == "" { + return fmt.Errorf("cannot retrieve instance port ID from task info: %w", err) + } + } + + return nil + }) + if err != nil { + return err + } + + return nil +} + +// deleteServerGroup removes a server group from an instance. +func deleteServerGroup(sgClient, instanceClient *edgecloud.ServiceClient, instanceID, sgID string) error { + log.Printf("[DEBUG] remove server group from instance: %s", instanceID) + results, err := instances.RemoveServerGroup(instanceClient, instanceID).Extract() + if err != nil { + return fmt.Errorf("failed to remove server group %s from instance %s: %w", sgID, instanceID, err) + } + + err = tasks.WaitTaskAndProcessResult(sgClient, results.Tasks[0], true, InstanceCreatingTimeout, func(task tasks.TaskID) error { + sgInfo, err := servergroups.Get(sgClient, sgID).Extract() + if err != nil { + return fmt.Errorf("failed to get server group %s: %w", sgID, err) + } + for _, instanceInfo := range sgInfo.Instances { + if instanceInfo.InstanceID == instanceID { + return fmt.Errorf("server group %s was not removed from instance %s", sgID, instanceID) + } + } + return nil + }) + + if err != nil { + return err + } + + return nil +} + +// addServerGroup adds a server group to an instance. +func addServerGroup(sgClient, instanceClient *edgecloud.ServiceClient, instanceID, sgID string) error { + log.Printf("[DEBUG] add server group to instance: %s", instanceID) + results, err := instances.AddServerGroup(instanceClient, instanceID, instances.ServerGroupOpts{ServerGroupID: sgID}).Extract() + if err != nil { + return fmt.Errorf("failed to add server group %s to instance %s: %w", sgID, instanceID, err) + } + + err = tasks.WaitTaskAndProcessResult(sgClient, results.Tasks[0], true, InstanceCreatingTimeout, func(task tasks.TaskID) error { + sgInfo, err := servergroups.Get(sgClient, sgID).Extract() + if err != nil { + return fmt.Errorf("cannot get server group with ID: %s. Error: %w", sgID, err) + } + for _, instanceInfo := range sgInfo.Instances { + if instanceInfo.InstanceID == instanceID { + return nil + } + } + return fmt.Errorf("the server group: %s was not added to the instance: %s. Error: %w", sgID, instanceID, err) + }) + + if err != nil { + return err + } + + return nil +} + +// removeSecurityGroupFromInstance removes one or more security groups from a specific instance port. +func removeSecurityGroupFromInstance(sgClient, instanceClient *edgecloud.ServiceClient, instanceID, portID string, removeSGs []edgecloud.ItemID) error { + for _, sg := range removeSGs { + sgInfo, err := securitygroups.Get(sgClient, sg.ID).Extract() + if err != nil { + return err + } + + portSGNames := instances.PortSecurityGroupNames{PortID: &portID, SecurityGroupNames: []string{sgInfo.Name}} + sgOpts := instances.SecurityGroupOpts{PortsSecurityGroupNames: []instances.PortSecurityGroupNames{portSGNames}} + + log.Printf("[DEBUG] remove security group opts: %+v", sgOpts) + if err := instances.UnAssignSecurityGroup(instanceClient, instanceID, sgOpts).Err; err != nil { + return fmt.Errorf("cannot remove security group. Error: %w", err) + } + } + + return nil +} + +// attachSecurityGroupToInstance attaches one or more security groups to a specific instance port. +func attachSecurityGroupToInstance(sgClient, instanceClient *edgecloud.ServiceClient, instanceID, portID string, addSGs []edgecloud.ItemID) error { + for _, sg := range addSGs { + sgInfo, err := securitygroups.Get(sgClient, sg.ID).Extract() + if err != nil { + return err + } + + portSGNames := instances.PortSecurityGroupNames{PortID: &portID, SecurityGroupNames: []string{sgInfo.Name}} + sgOpts := instances.SecurityGroupOpts{PortsSecurityGroupNames: []instances.PortSecurityGroupNames{portSGNames}} + + log.Printf("[DEBUG] attach security group opts: %+v", sgOpts) + if err := instances.AssignSecurityGroup(instanceClient, instanceID, sgOpts).Err; err != nil { + return fmt.Errorf("cannot attach security group. Error: %w", err) + } + } + + return nil +} + +// prepareSecurityGroups prepares a list of unique security groups assigned to all instance ports. +func prepareSecurityGroups(ports []instances.InstancePorts) []interface{} { + securityGroups := make(map[string]bool) + for _, port := range ports { + for _, sg := range port.SecurityGroups { + securityGroups[sg.ID] = true + } + } + + result := make([]interface{}, 0, len(securityGroups)) + for sgID := range securityGroups { + result = append(result, map[string]interface{}{ + "id": sgID, + "name": "", + }) + } + + return result +} + +// getSecurityGroupsIDs converts a slice of raw security group IDs to a slice of edgecloud.ItemID. +func getSecurityGroupsIDs(sgsRaw []interface{}) []edgecloud.ItemID { + sgs := make([]edgecloud.ItemID, len(sgsRaw)) + for i, sgID := range sgsRaw { + sgs[i] = edgecloud.ItemID{ID: sgID.(string)} + } + return sgs +} + +// getSecurityGroupsDifference finds the difference between two slices of edgecloud.ItemID. +func getSecurityGroupsDifference(sl1, sl2 []edgecloud.ItemID) (diff []edgecloud.ItemID) { //nolint: nonamedreturns + set := make(map[string]bool) + for _, item := range sl1 { + set[item.ID] = true + } + + for _, item := range sl2 { + if !set[item.ID] { + diff = append(diff, item) + } + } + + return diff +} diff --git a/edgecenter/utils_k8s.go b/edgecenter/utils_k8s.go new file mode 100644 index 00000000..f53d77bd --- /dev/null +++ b/edgecenter/utils_k8s.go @@ -0,0 +1,61 @@ +package edgecenter + +import ( + "net" + + "gopkg.in/yaml.v3" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" +) + +func parseCIDRFromString(cidr string) (edgecloud.CIDR, error) { + var ecCIDR edgecloud.CIDR + _, netIPNet, err := net.ParseCIDR(cidr) + if err != nil { + return ecCIDR, err + } + ecCIDR.IP = netIPNet.IP + ecCIDR.Mask = netIPNet.Mask + + return ecCIDR, nil +} + +type K8sConfig struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + CurrentContext string `yaml:"current-context"` // nolint: tagliatelle + Preferences struct{} `yaml:"preferences"` + + Clusters []struct { + Name string `yaml:"name"` + Cluster struct { + CertificateAuthorityData string `yaml:"certificate-authority-data"` // nolint: tagliatelle + Server string `yaml:"server"` + } `yaml:"cluster"` + } `yaml:"clusters"` + + Contexts []struct { + Name string `yaml:"name"` + Context struct { + Cluster string `yaml:"cluster"` + User string `yaml:"user"` + } `yaml:"context"` + } `yaml:"contexts"` + + Users []struct { + Name string `yaml:"name"` + User struct { + ClientCertificateData string `yaml:"client-certificate-data"` // nolint: tagliatelle + ClientKeyData string `yaml:"client-key-data"` // nolint: tagliatelle + } `yaml:"user"` + } `yaml:"users"` +} + +func parseK8sConfig(data string) (*K8sConfig, error) { + var config K8sConfig + err := yaml.Unmarshal([]byte(data), &config) + if err != nil { + return nil, err + } + return &config, nil +} diff --git a/edgecenter/utils_loadbalancer.go b/edgecenter/utils_loadbalancer.go new file mode 100644 index 00000000..ce46da07 --- /dev/null +++ b/edgecenter/utils_loadbalancer.go @@ -0,0 +1,121 @@ +package edgecenter + +import ( + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/lbpools" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/listeners" + typesLb "github.com/Edge-Center/edgecentercloud-go/edgecenter/loadbalancer/v1/types" +) + +// ImportStringParserExtended parses a string containing project ID, region ID, and two other fields, +// and returns them as separate values along with any error encountered. +func ImportStringParserExtended(infoStr string) (projectID int, regionID int, id3 string, id4 string, err error) { //nolint: nonamedreturns + log.Printf("[DEBUG] Input id string: %s", infoStr) + infoStrings := strings.Split(infoStr, ":") + if len(infoStrings) != 4 { + err = fmt.Errorf("failed import: wrong input id: %s", infoStr) + return + } + + id1, id2, id3, id4 := infoStrings[0], infoStrings[1], infoStrings[2], infoStrings[3] + + projectID, err = strconv.Atoi(id1) + if err != nil { + return + } + regionID, err = strconv.Atoi(id2) + if err != nil { + return + } + + return +} + +// extractSessionPersistenceMap creates a session persistence options struct from the data in the given ResourceData. +func extractSessionPersistenceMap(d *schema.ResourceData) *lbpools.CreateSessionPersistenceOpts { + var sessionOpts *lbpools.CreateSessionPersistenceOpts + sessionPersistence := d.Get("session_persistence").([]interface{}) + if len(sessionPersistence) > 0 { + sm := sessionPersistence[0].(map[string]interface{}) + sessionOpts = &lbpools.CreateSessionPersistenceOpts{ + Type: typesLb.PersistenceType(sm["type"].(string)), + } + + granularity, ok := sm["persistence_granularity"].(string) + if ok { + sessionOpts.PersistenceGranularity = granularity + } + + timeout, ok := sm["persistence_timeout"].(int) + if ok { + sessionOpts.PersistenceTimeout = timeout + } + + cookieName, ok := sm["cookie_name"].(string) + if ok { + sessionOpts.CookieName = cookieName + } + } + + return sessionOpts +} + +// extractHealthMonitorMap creates a health monitor options struct from the data in the given ResourceData. +func extractHealthMonitorMap(d *schema.ResourceData) *lbpools.CreateHealthMonitorOpts { + var healthOpts *lbpools.CreateHealthMonitorOpts + monitors := d.Get("health_monitor").([]interface{}) + if len(monitors) > 0 { + hm := monitors[0].(map[string]interface{}) + healthOpts = &lbpools.CreateHealthMonitorOpts{ + Type: typesLb.HealthMonitorType(hm["type"].(string)), + Delay: hm["delay"].(int), + MaxRetries: hm["max_retries"].(int), + Timeout: hm["timeout"].(int), + } + + maxRetriesDown := hm["max_retries_down"].(int) + if maxRetriesDown != 0 { + healthOpts.MaxRetriesDown = maxRetriesDown + } + + httpMethod := hm["http_method"].(string) + if httpMethod != "" { + healthOpts.HTTPMethod = typesLb.HTTPMethodPointer(typesLb.HTTPMethod(httpMethod)) + } + + urlPath := hm["url_path"].(string) + if urlPath != "" { + healthOpts.URLPath = urlPath + } + + expectedCodes := hm["expected_codes"].(string) + if expectedCodes != "" { + healthOpts.ExpectedCodes = expectedCodes + } + + id := hm["id"].(string) + if id != "" { + healthOpts.ID = id + } + } + + return healthOpts +} + +// extractListenerIntoMap converts a listener object into a map. +func extractListenerIntoMap(listener *listeners.Listener) map[string]interface{} { + l := make(map[string]interface{}) + l["id"] = listener.ID + l["name"] = listener.Name + l["protocol"] = listener.Protocol.String() + l["protocol_port"] = listener.ProtocolPort + l["secret_id"] = listener.SecretID + l["sni_secret_id"] = listener.SNISecretID + return l +} diff --git a/edgecenter/utils_network.go b/edgecenter/utils_network.go new file mode 100644 index 00000000..e89c2219 --- /dev/null +++ b/edgecenter/utils_network.go @@ -0,0 +1,90 @@ +package edgecenter + +import ( + "encoding/json" + "net" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/availablenetworks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/network/v1/networks" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" +) + +// findNetworkByName searches for a network with the given name among the given networks. +// Returns the found network and a flag indicating the success of the search. +func findNetworkByName(name string, nets []networks.Network) (networks.Network, bool) { + for _, n := range nets { + if n.Name == name { + return n, true + } + } + return networks.Network{}, false +} + +// findSharedNetworkByName searches for a shared network with the given name among the given networks. +// Returns the found network and a flag indicating the success of the search. +func findSharedNetworkByName(name string, nets []availablenetworks.Network) (availablenetworks.Network, bool) { + for _, n := range nets { + if n.Name == name { + return n, true + } + } + return availablenetworks.Network{}, false +} + +// StructToMap converts the struct to map[string]interface{}. +// Returns an error if the conversion fails. +func StructToMap(obj interface{}) (map[string]interface{}, error) { + data, err := json.Marshal(obj) + if err != nil { + return nil, err + } + + var newMap map[string]interface{} + err = json.Unmarshal(data, &newMap) + if err != nil { + return nil, err + } + + return newMap, nil +} + +func dnsNameserversToStringList(dnsNameservers []net.IP) []string { + dns := make([]string, len(dnsNameservers)) + for i, ns := range dnsNameservers { + dns[i] = ns.String() + } + + return dns +} + +func hostRoutesToListOfMaps(hostRoutes []subnets.HostRoute) []map[string]string { + hrs := make([]map[string]string, len(hostRoutes)) + for i, hr := range hostRoutes { + hR := map[string]string{"destination": "", "nexthop": ""} + hR["destination"] = hr.Destination.String() + hR["nexthop"] = hr.NextHop.String() + hrs[i] = hR + } + + return hrs +} + +func prepareSubnets(subs []subnets.Subnet) []map[string]interface{} { + subnetList := make([]map[string]interface{}, 0, len(subs)) + for _, s := range subs { + subnetList = append(subnetList, map[string]interface{}{ + "id": s.ID, + "name": s.Name, + "enable_dhcp": s.EnableDHCP, + "cidr": s.CIDR.String(), + "available_ips": s.AvailableIps, + "total_ips": s.TotalIps, + "has_router": s.HasRouter, + "dns_nameservers": dnsNameserversToStringList(s.DNSNameservers), + "host_routes": hostRoutesToListOfMaps(s.HostRoutes), + "gateway_ip": s.GatewayIP.String(), + }) + } + + return subnetList +} diff --git a/edgecenter/utils_project.go b/edgecenter/utils_project.go new file mode 100644 index 00000000..ec243c83 --- /dev/null +++ b/edgecenter/utils_project.go @@ -0,0 +1,53 @@ +package edgecenter + +import ( + "fmt" + "log" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/project/v1/projects" +) + +// findProjectByName searches for a project with the specified name in the provided project slice. +// Returns the project ID if found, otherwise returns an error. +func findProjectByName(arr []projects.Project, name string) (int, error) { + for _, el := range arr { + if el.Name == name { + return el.ID, nil + } + } + return 0, fmt.Errorf("project with name %s not found", name) +} + +// GetProject returns a valid project ID for a resource. +// If the projectID is provided, it will be returned directly. +// If projectName is provided instead, the function will search for the project by name and return its ID. +// Returns an error if the project is not found or there is an issue with the client. +func GetProject(provider *edgecloud.ProviderClient, projectID int, projectName string) (int, error) { + log.Println("[DEBUG] Try to get project ID") + if projectID != 0 { + return projectID, nil + } + client, err := edgecenter.ClientServiceFromProvider(provider, edgecloud.EndpointOpts{ + Name: ProjectPoint, + Region: 0, + Project: 0, + Version: VersionPointV1, + }) + if err != nil { + return 0, err + } + projectsList, err := projects.ListAll(client) + if err != nil { + return 0, err + } + log.Printf("[DEBUG] Projects: %v", projectsList) + projectID, err = findProjectByName(projectsList, projectName) + if err != nil { + return 0, err + } + log.Printf("[DEBUG] The attempt to get the project is successful: projectID=%d", projectID) + + return projectID, nil +} diff --git a/edgecenter/utils_region.go b/edgecenter/utils_region.go new file mode 100644 index 00000000..c7af5aca --- /dev/null +++ b/edgecenter/utils_region.go @@ -0,0 +1,53 @@ +package edgecenter + +import ( + "fmt" + "log" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/region/v1/regions" +) + +// findRegionByName searches for a region with the specified name in the provided region slice. +// Returns the region ID if found, otherwise returns an error. +func findRegionByName(arr []regions.Region, name string) (int, error) { + for _, el := range arr { + if el.DisplayName == name { + return el.ID, nil + } + } + return 0, fmt.Errorf("region with name %s not found", name) +} + +// GetRegion returns a valid region ID for a resource. +// If the regionID is provided, it will be returned directly. +// If regionName is provided instead, the function will search for the region by name and return its ID. +// Returns an error if the region is not found or there is an issue with the client. +func GetRegion(provider *edgecloud.ProviderClient, regionID int, regionName string) (int, error) { + if regionID != 0 { + return regionID, nil + } + client, err := edgecenter.ClientServiceFromProvider(provider, edgecloud.EndpointOpts{ + Name: RegionPoint, + Region: 0, + Project: 0, + Version: VersionPointV1, + }) + if err != nil { + return 0, err + } + + rs, err := regions.ListAll(client) + if err != nil { + return 0, err + } + log.Printf("[DEBUG] Regions: %v", rs) + regionID, err = findRegionByName(rs, regionName) + if err != nil { + return 0, err + } + log.Printf("[DEBUG] The attempt to get the region is successful: regionID=%d", regionID) + + return regionID, nil +} diff --git a/edgecenter/utils_router.go b/edgecenter/utils_router.go new file mode 100644 index 00000000..eb951082 --- /dev/null +++ b/edgecenter/utils_router.go @@ -0,0 +1,127 @@ +package edgecenter + +import ( + "crypto/md5" + "encoding/binary" + "fmt" + "io" + "net" + "reflect" + + "github.com/mitchellh/mapstructure" + + edgecloud "github.com/Edge-Center/edgecentercloud-go" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/router/v1/routers" + "github.com/Edge-Center/edgecentercloud-go/edgecenter/subnet/v1/subnets" +) + +var routerDecoderConfig = &mapstructure.DecoderConfig{ + TagName: "json", +} + +// StringToNetHookFunc returns a DecodeHookFunc for the mapstructure package to handle the custom +// conversion of string values to net.IP and edgecloud.CIDR types. +func StringToNetHookFunc() mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) { + // Only process strings as source type. + if f.Kind() != reflect.String { + return data, nil + } + + // Process the target types. + switch t { + case reflect.TypeOf(edgecloud.CIDR{}): + var ecCIDR edgecloud.CIDR + _, ipNet, err := net.ParseCIDR(data.(string)) + if err != nil { + return nil, err + } + ecCIDR.IP = ipNet.IP + ecCIDR.Mask = ipNet.Mask + return ecCIDR, nil + case reflect.TypeOf(net.IP{}): + ip := net.ParseIP(data.(string)) + if ip == nil { + return nil, fmt.Errorf("failed parsing ip %v", data) + } + return ip, nil + default: + // If the target type is not supported, return the data as is. + return data, nil + } + } +} + +// extractHostRoutesMap converts a slice of interface{} representing host routes into a slice of subnets.HostRoute. +func extractHostRoutesMap(v []interface{}) ([]subnets.HostRoute, error) { + decoderConfig := &mapstructure.DecoderConfig{ + DecodeHook: StringToNetHookFunc(), + } + + hostRoutes := make([]subnets.HostRoute, len(v)) + for i, hostRoute := range v { + hs, ok := hostRoute.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to assert host route as map[string]interface{}") + } + var H subnets.HostRoute + err := MapStructureDecoder(&H, &hs, decoderConfig) + if err != nil { + return nil, err + } + hostRoutes[i] = H + } + + return hostRoutes, nil +} + +// routerInterfaceUniqueID generates a unique ID for a router interface using its subnet ID. +func routerInterfaceUniqueID(i interface{}) int { + e := i.(map[string]interface{}) + + subnetID := e["subnet_id"].(string) + + h := md5.New() + io.WriteString(h, subnetID) + + return int(binary.BigEndian.Uint64(h.Sum(nil))) +} + +// extractExternalGatewayInfoMap converts the first element of a gateway slice +// into a routers.GatewayInfo struct using the provided mapstructure decoder configuration. +func extractExternalGatewayInfoMap(gw []interface{}) (routers.GatewayInfo, error) { + gateway, ok := gw[0].(map[string]interface{}) + if !ok { + return routers.GatewayInfo{}, fmt.Errorf("failed to assert gateway as map[string]interface{}") + } + + var gwInfo routers.GatewayInfo + err := MapStructureDecoder(&gwInfo, &gateway, routerDecoderConfig) + if err != nil { + return routers.GatewayInfo{}, err + } + + return gwInfo, nil +} + +// extractInterfacesMap converts a slice of interface{} representing router interfaces +// into a slice of routers.Interface using the provided mapstructure decoder configuration. +func extractInterfacesMap(interfaces []interface{}) ([]routers.Interface, error) { + ifaceList := make([]routers.Interface, len(interfaces)) + for i, iface := range interfaces { + inter, ok := iface.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to assert interface as map[string]interface{}") + } + + var ifaceInfo routers.Interface + err := MapStructureDecoder(&ifaceInfo, &inter, routerDecoderConfig) + if err != nil { + return nil, err + } + + ifaceList[i] = ifaceInfo + } + + return ifaceList, nil +} diff --git a/edgecenter/utils_securitygroup.go b/edgecenter/utils_securitygroup.go new file mode 100644 index 00000000..9530acd3 --- /dev/null +++ b/edgecenter/utils_securitygroup.go @@ -0,0 +1,56 @@ +package edgecenter + +import ( + "crypto/md5" + "encoding/binary" + "io" + "strconv" + + "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/securitygroups" + typesSG "github.com/Edge-Center/edgecentercloud-go/edgecenter/securitygroup/v1/types" +) + +// secGroupUniqueID generates a unique ID for a security group rule using its properties. +func secGroupUniqueID(i interface{}) int { + e := i.(map[string]interface{}) + + h := md5.New() + proto, _ := e["protocol"].(string) + io.WriteString(h, e["direction"].(string)) + io.WriteString(h, e["ethertype"].(string)) + io.WriteString(h, proto) + io.WriteString(h, strconv.Itoa(e["port_range_min"].(int))) + io.WriteString(h, strconv.Itoa(e["port_range_max"].(int))) + io.WriteString(h, e["description"].(string)) + io.WriteString(h, e["remote_ip_prefix"].(string)) + + return int(binary.BigEndian.Uint64(h.Sum(nil))) +} + +// extractSecurityGroupRuleMap creates a security group rule from the provided map and security group ID. +func extractSecurityGroupRuleMap(r interface{}, gid string) securitygroups.CreateSecurityGroupRuleOpts { + rule := r.(map[string]interface{}) + + opts := securitygroups.CreateSecurityGroupRuleOpts{ + Direction: typesSG.RuleDirection(rule["direction"].(string)), + EtherType: typesSG.EtherType(rule["ethertype"].(string)), + Protocol: typesSG.Protocol(rule["protocol"].(string)), + SecurityGroupID: &gid, + } + + minP, maxP := rule["port_range_min"].(int), rule["port_range_max"].(int) + if minP != 0 && maxP != 0 { + opts.PortRangeMin = &minP + opts.PortRangeMax = &maxP + } + + description, _ := rule["description"].(string) + opts.Description = &description + + remoteIPPrefix := rule["remote_ip_prefix"].(string) + if remoteIPPrefix != "" { + opts.RemoteIPPrefix = &remoteIPPrefix + } + + return opts +} diff --git a/edgecenter/volume/datasource_volume.go b/edgecenter/volume/datasource_volume.go deleted file mode 100644 index 82665f19..00000000 --- a/edgecenter/volume/datasource_volume.go +++ /dev/null @@ -1,196 +0,0 @@ -package volume - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" -) - -func DataSourceEdgeCenterVolume() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceEdgeCenterVolumeRead, - Description: `A volume is a detachable block storage device akin to a USB hard drive or SSD, but located remotely in the cloud. -Volumes can be attached to a virtual machine and manipulated like a physical hard drive.`, - - Schema: map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - Description: "uuid of the region", - }, - "id": { - Type: schema.TypeString, - Optional: true, - Description: "volume uuid", - ValidateFunc: validation.IsUUID, - ExactlyOneOf: []string{"id", "name"}, - }, - "name": { - Type: schema.TypeString, - Optional: true, - Description: `volume name. this parameter is not unique, if there is more than one volume with the same name, -then the first one will be used. it is recommended to use "id"`, - ExactlyOneOf: []string{"id", "name"}, - }, - // computed attributes - "status": { - Type: schema.TypeString, - Computed: true, - Description: "current status of the volume resource", - }, - "size": { - Type: schema.TypeInt, - Computed: true, - Description: "size of the volume, specified in gigabytes (GB)", - }, - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "volume_type": { - Type: schema.TypeString, - Computed: true, - Description: "volume type", - }, - "metadata": { - Type: schema.TypeList, - Computed: true, - Description: "metadata in detailed format", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "key": { - Type: schema.TypeString, - Computed: true, - }, - "value": { - Type: schema.TypeString, - Computed: true, - }, - "read_only": { - Type: schema.TypeBool, - Computed: true, - }, - }, - }, - }, - "attachments": { - Type: schema.TypeList, - Computed: true, - Description: "the attachment list", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "volume_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the volume", - }, - "attachment_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the attachment object", - }, - "server_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the instance", - }, - }, - }, - }, - "bootable": { - Type: schema.TypeBool, - Computed: true, - Description: "the bootable boolean flag", - }, - "limiter_stats": { - Type: schema.TypeMap, - Computed: true, - Description: "the QoS parameters of this volume", - Elem: &schema.Schema{ - Type: schema.TypeInt, - }, - }, - }, - } -} - -func dataSourceEdgeCenterVolumeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - var foundVolume *edgecloud.Volume - - if id, ok := d.GetOk("id"); ok { - volume, _, err := client.Volumes.Get(ctx, id.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundVolume = volume - } else if volumeName, ok := d.GetOk("name"); ok { - volumeList, err := util.VolumesListByName(ctx, client, volumeName.(string)) - if err != nil { - return diag.FromErr(err) - } - - foundVolume = &volumeList[0] - } else { - return diag.Errorf("Error: specify either id or a name to lookup the volume") - } - - d.SetId(foundVolume.ID) - d.Set("name", foundVolume.Name) - - d.Set("status", foundVolume.Status) - d.Set("size", foundVolume.Size) - d.Set("region", foundVolume.Region) - d.Set("volume_type", foundVolume.VolumeType) - d.Set("bootable", foundVolume.Bootable) - - if len(foundVolume.MetadataDetailed) > 0 { - metadata := make([]map[string]interface{}, 0, len(foundVolume.MetadataDetailed)) - for _, metadataItem := range foundVolume.MetadataDetailed { - metadata = append(metadata, map[string]interface{}{ - "key": metadataItem.Key, - "value": metadataItem.Value, - "read_only": metadataItem.ReadOnly, - }) - } - d.Set("metadata", metadata) - } - - if len(foundVolume.Attachments) > 0 { - attachments := make([]map[string]interface{}, 0, len(foundVolume.Attachments)) - for _, attachment := range foundVolume.Attachments { - attachments = append(attachments, map[string]interface{}{ - "volume_id": attachment.VolumeID, - "attachment_id": attachment.AttachmentID, - "server_id": attachment.ServerID, - }) - } - d.Set("attachments", attachments) - } - - d.Set("limiter_stats", - map[string]int{ - "iops_base_limit": foundVolume.LimiterStats.IopsBaseLimit, - "iops_burst_limit": foundVolume.LimiterStats.IopsBurstLimit, - "MBps_base_limit": foundVolume.LimiterStats.MBpsBaseLimit, - "MBps_burst_limit": foundVolume.LimiterStats.MBpsBurstLimit, - }) - - return nil -} diff --git a/edgecenter/volume/resource_volume.go b/edgecenter/volume/resource_volume.go deleted file mode 100644 index 328754e2..00000000 --- a/edgecenter/volume/resource_volume.go +++ /dev/null @@ -1,228 +0,0 @@ -package volume - -import ( - "context" - "fmt" - "log" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" - "github.com/Edge-Center/edgecentercloud-go/util" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/config" - "github.com/Edge-Center/terraform-provider-edgecenter/edgecenter/converter" -) - -func ResourceEdgeCenterVolume() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceEdgeCenterVolumeCreate, - ReadContext: resourceEdgeCenterVolumeRead, - UpdateContext: resourceEdgeCenterVolumeUpdate, - DeleteContext: resourceEdgeCenterVolumeDelete, - Description: `A volume is a detachable block storage device akin to a USB hard drive or SSD, but located remotely in the cloud. -Volumes can be attached to a virtual machine and manipulated like a physical hard drive.`, - Schema: volumeSchema(), - - CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, v interface{}) error { - // if the new size of the volume is smaller than the old one return an error since - // only expanding the volume is allowed - oldSize, newSize := diff.GetChange("size") - if newSize.(int) < oldSize.(int) { - return fmt.Errorf("volumes `size` can only be expanded and not shrunk") - } - - return nil - }, - } -} - -func resourceEdgeCenterVolumeCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - opts := &edgecloud.VolumeCreateRequest{ - Name: d.Get("name").(string), - Size: d.Get("size").(int), - TypeName: edgecloud.VolumeType(d.Get("volume_type").(string)), - } - - if v, ok := d.GetOk("metadata"); ok { - opts.Metadata = converter.MapInterfaceToMapString(v.(map[string]interface{})) - } - - source := d.Get("source").(string) - opts.Source = edgecloud.VolumeSource(source) - switch source { - case "snapshot": - if v, ok := d.GetOk("snapshot_id"); ok { - opts.SnapshotID = v.(string) - } else { - return diag.Errorf("'snapshot_id' is mandatory if creating a volume from an image") - } - case "image": - if v, ok := d.GetOk("image_id"); ok { - opts.ImageID = v.(string) - } else { - return diag.Errorf("'image_id' is mandatory if creating a volume from an image") - } - } - - if v, ok := d.GetOk("instance_id_to_attach_to"); ok { - opts.InstanceIDToAttachTo = v.(string) - if attachmentTag, okTag := d.GetOk("attachment_tag"); okTag { - opts.AttachmentTag = attachmentTag.(string) - } - } - - log.Printf("[DEBUG] Volume create configuration: %#v", opts) - - taskResult, err := util.ExecuteAndExtractTaskResult(ctx, client.Volumes.Create, opts, client) - if err != nil { - return diag.Errorf("error creating volume: %s", err) - } - - d.SetId(taskResult.Volumes[0]) - - log.Printf("[INFO] Volume: %s", d.Id()) - - return resourceEdgeCenterVolumeRead(ctx, d, meta) -} - -func resourceEdgeCenterVolumeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - // Retrieve the volume properties for updating the state - foundVolume, resp, err := client.Volumes.Get(ctx, d.Id()) - if err != nil { - // check if volume no longer exists. - if resp != nil && resp.StatusCode == 404 { - log.Printf("[WARN] EdgeCenter Volume (%s) not found", d.Id()) - d.SetId("") - return nil - } - - return diag.Errorf("Error retrieving volume: %s", err) - } - - d.Set("volume_type", foundVolume.VolumeType) - d.Set("region", foundVolume.Region) - d.Set("status", foundVolume.Status) - d.Set("bootable", foundVolume.Bootable) - d.Set("limiter_stats", foundVolume.LimiterStats) - d.Set("snapshot_ids", foundVolume.SnapshotIDs) - d.Set("limiter_stats", - map[string]int{ - "iops_base_limit": foundVolume.LimiterStats.IopsBaseLimit, - "iops_burst_limit": foundVolume.LimiterStats.IopsBurstLimit, - "MBps_base_limit": foundVolume.LimiterStats.MBpsBaseLimit, - "MBps_burst_limit": foundVolume.LimiterStats.MBpsBurstLimit, - }) - if len(foundVolume.Attachments) > 0 { - attachments := make([]map[string]interface{}, 0, len(foundVolume.Attachments)) - for _, attachment := range foundVolume.Attachments { - attachments = append(attachments, map[string]interface{}{ - "volume_id": attachment.VolumeID, - "attachment_id": attachment.AttachmentID, - "server_id": attachment.ServerID, - }) - } - d.Set("attachments", attachments) - } - - return nil -} - -func resourceEdgeCenterVolumeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - if d.HasChange("name") { - newName := d.Get("name").(string) - if _, _, err := client.Volumes.Rename(ctx, d.Id(), &edgecloud.Name{Name: newName}); err != nil { - return diag.FromErr(err) - } - } - - if d.HasChange("instance_id_to_attach_to") { - oldInstance, newInstance := d.GetChange("instance_id_to_attach_to") - - if oldInstance != "" { - volumeDetachRequest := &edgecloud.VolumeDetachRequest{InstanceID: oldInstance.(string)} - if _, _, err := client.Volumes.Detach(ctx, d.Id(), volumeDetachRequest); err != nil { - return diag.Errorf("Error detaching volume from instance: %s", err) - } - } - - if newInstance != "" { - volumeAttachRequest := &edgecloud.VolumeAttachRequest{ - InstanceID: newInstance.(string), - AttachmentTag: d.Get("attachment_tag").(string), - } - if _, _, err := client.Volumes.Attach(ctx, d.Id(), volumeAttachRequest); err != nil { - return diag.Errorf("Error attaching volume to instance: %s", err) - } - } - } - - if d.HasChange("size") { - newSize := d.Get("size").(int) - task, _, err := client.Volumes.Extend(ctx, d.Id(), &edgecloud.VolumeExtendSizeRequest{Size: newSize}) - if err != nil { - return diag.FromErr(err) - } - if err = util.WaitForTaskComplete(ctx, client, task.Tasks[0]); err != nil { - return diag.FromErr(err) - } - } - - if d.HasChange("volume_type") { - newVolumeType := d.Get("volume_type").(string) - _, _, err := client.Volumes.ChangeType(ctx, d.Id(), &edgecloud.VolumeChangeTypeRequest{ - VolumeType: edgecloud.VolumeType(newVolumeType), - }) - if err != nil { - return diag.FromErr(err) - } - } - - if d.HasChange("metadata") { - metadata := edgecloud.Metadata(converter.MapInterfaceToMapString(d.Get("metadata").(map[string]interface{}))) - - if _, err := client.Volumes.MetadataUpdate(ctx, d.Id(), &metadata); err != nil { - return diag.Errorf("cannot update metadata. Error: %s", err) - } - } - - return resourceEdgeCenterVolumeRead(ctx, d, meta) -} - -func resourceEdgeCenterVolumeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*config.CombinedConfig).EdgeCloudClient() - client.Region = d.Get("region_id").(int) - client.Project = d.Get("project_id").(int) - - volume, _, err := client.Volumes.Get(ctx, d.Id()) - if err != nil { - return diag.Errorf("Error getting volume: %s", err) - } - - if len(volume.Attachments) > 0 { - volumeDetachRequest := &edgecloud.VolumeDetachRequest{InstanceID: volume.Attachments[0].ServerID} - if _, _, err = client.Volumes.Detach(ctx, d.Id(), volumeDetachRequest); err != nil { - return diag.Errorf("Error detaching volume from instance: %s", err) - } - } - - log.Printf("[INFO] Deleting volume: %s", d.Id()) - if err = util.DeleteResourceIfExist(ctx, client, client.Volumes, d.Id()); err != nil { - return diag.Errorf("Error deleting volume: %s", err) - } - d.SetId("") - - return nil -} diff --git a/edgecenter/volume/volumes.go b/edgecenter/volume/volumes.go deleted file mode 100644 index 52a6ef7b..00000000 --- a/edgecenter/volume/volumes.go +++ /dev/null @@ -1,140 +0,0 @@ -package volume - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - - edgecloud "github.com/Edge-Center/edgecentercloud-go" -) - -func volumeSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "project_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the project", - }, - "region_id": { - Type: schema.TypeInt, - Required: true, - ForceNew: true, - Description: "uuid of the region", - }, - "name": { - Type: schema.TypeString, - Required: true, - Description: "name of the volume", - }, - "size": { - Type: schema.TypeInt, - Required: true, - Description: "size of the volume, specified in gigabytes (GB)", - ValidateFunc: validation.IntAtLeast(1), - }, - "source": { - Type: schema.TypeString, - Required: true, - Description: "volume source. valid values are 'new-volume', 'snapshot' or 'image'", - ValidateFunc: validation.StringInSlice([]string{"new-volume", "snapshot", "image"}, false), - }, - "metadata": { - Type: schema.TypeMap, - Optional: true, - Description: "map containing metadata, for example tags.", - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "volume_type": { - Type: schema.TypeString, - Optional: true, - Description: "volume type with valid values. defaults to 'standard'", - ValidateFunc: validation.StringInSlice([]string{ - string(edgecloud.VolumeTypeSsdHiIops), - string(edgecloud.VolumeTypeSsdLocal), - string(edgecloud.VolumeTypeUltra), - string(edgecloud.VolumeTypeCold), - string(edgecloud.VolumeTypeStandard), - }, false), - Default: string(edgecloud.VolumeTypeStandard), - }, - "instance_id_to_attach_to": { - Type: schema.TypeString, - Optional: true, - Description: "VM’s instance_id to attach a newly created volume to", - }, - "attachment_tag": { - Type: schema.TypeString, - Optional: true, - Description: "the block device attachment tag (exposed in the metadata)", - RequiredWith: []string{"instance_id_to_attach_to"}, - }, - "image_id": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "ID of the image. this field is mandatory if creating a volume from an image", - }, - "snapshot_id": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Description: "ID of the snapshot. this field is mandatory if creating a volume from a snapshot", - }, - // computed attributes - "region": { - Type: schema.TypeString, - Computed: true, - Description: "name of the region", - }, - "status": { - Type: schema.TypeString, - Computed: true, - Description: "status of the volume", - }, - "bootable": { - Type: schema.TypeBool, - Computed: true, - Description: "the bootable boolean flag", - }, - "limiter_stats": { - Type: schema.TypeMap, - Computed: true, - Description: "the QoS parameters of this volume", - Elem: &schema.Schema{ - Type: schema.TypeInt, - }, - }, - "attachments": { - Type: schema.TypeList, - Computed: true, - Description: "the attachment list", - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "volume_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the volume", - }, - "attachment_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the attachment object", - }, - "server_id": { - Type: schema.TypeString, - Computed: true, - Description: "ID of the instance", - }, - }, - }, - }, - "snapshot_ids": { - Type: schema.TypeList, - Computed: true, - Description: "snapshots of the volume", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - } -} diff --git a/examples/data-sources/edgecenter_floatingip/data-source.tf b/examples/data-sources/edgecenter_floatingip/data-source.tf index 1dd5157b..3edaeeef 100644 --- a/examples/data-sources/edgecenter_floatingip/data-source.tf +++ b/examples/data-sources/edgecenter_floatingip/data-source.tf @@ -1,21 +1,22 @@ -# Example 1 -data "edgecenter_floatingip" "fip1" { - region_id = var.region_id - project_id = var.project_id - floating_ip_address = "10.10.0.1" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "fip1" { - value = data.edgecenter_floatingip.fip1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_floatingip" "fip2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "fip2" { - value = data.edgecenter_floatingip.fip2 +data "edgecenter_floatingip" "ip" { + floating_ip_address = "10.100.179.172" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } + +output "view" { + value = data.edgecenter_floatingip.ip +} + diff --git a/examples/data-sources/edgecenter_floatingip/variables.tf b/examples/data-sources/edgecenter_floatingip/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/data-sources/edgecenter_floatingip/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/data-sources/edgecenter_image/data-source.tf b/examples/data-sources/edgecenter_image/data-source.tf new file mode 100644 index 00000000..9aa22b43 --- /dev/null +++ b/examples/data-sources/edgecenter_image/data-source.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_image" "ubuntu" { + name = "ubuntu-20.04" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_image.ubuntu +} + diff --git a/examples/data-sources/edgecenter_instance/data-source.tf b/examples/data-sources/edgecenter_instance/data-source.tf index c4e94f6c..47e7d675 100644 --- a/examples/data-sources/edgecenter_instance/data-source.tf +++ b/examples/data-sources/edgecenter_instance/data-source.tf @@ -1,21 +1,22 @@ -# Example 1 -data "edgecenter_instance" "instance1" { - region_id = var.region_id - project_id = var.project_id - name = "test-instance" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "instance1" { - value = data.edgecenter_instance.instance1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_instance" "instance2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "instance2" { - value = data.edgecenter_instance.instance2 +data "edgecenter_instance" "vm" { + name = "test-vm" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } + +output "view" { + value = data.edgecenter_instance.vm +} + diff --git a/examples/data-sources/edgecenter_instance/variables.tf b/examples/data-sources/edgecenter_instance/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/data-sources/edgecenter_instance/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/data-sources/edgecenter_k8s/data-source.tf b/examples/data-sources/edgecenter_k8s/data-source.tf new file mode 100644 index 00000000..a8f3721f --- /dev/null +++ b/examples/data-sources/edgecenter_k8s/data-source.tf @@ -0,0 +1,10 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_k8s" "cluster" { + project_id = 1 + region_id = 1 + cluster_id = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" +} + diff --git a/examples/data-sources/edgecenter_k8s_client_config/data-source.tf b/examples/data-sources/edgecenter_k8s_client_config/data-source.tf new file mode 100644 index 00000000..061ee4ed --- /dev/null +++ b/examples/data-sources/edgecenter_k8s_client_config/data-source.tf @@ -0,0 +1,10 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_k8s_client_config" "cfg" { + project_id = 1 + region_id = 1 + cluster_id = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" +} + diff --git a/examples/data-sources/edgecenter_k8s_pool/data-source.tf b/examples/data-sources/edgecenter_k8s_pool/data-source.tf new file mode 100644 index 00000000..b3751d59 --- /dev/null +++ b/examples/data-sources/edgecenter_k8s_pool/data-source.tf @@ -0,0 +1,11 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_k8s_pool" "pool" { + project_id = 1 + region_id = 1 + cluster_id = "6bf878c1-1ce4-47c3-a39b-6b5f1d79bf25" + pool_id = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" +} + diff --git a/examples/data-sources/edgecenter_laas_hosts/data-source.tf b/examples/data-sources/edgecenter_laas_hosts/data-source.tf new file mode 100644 index 00000000..4a8e056b --- /dev/null +++ b/examples/data-sources/edgecenter_laas_hosts/data-source.tf @@ -0,0 +1,20 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_laas_hosts" "hosts" { + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_laas_hosts.hosts +} diff --git a/examples/data-sources/edgecenter_laas_status/data-source.tf b/examples/data-sources/edgecenter_laas_status/data-source.tf new file mode 100644 index 00000000..dd671dc4 --- /dev/null +++ b/examples/data-sources/edgecenter_laas_status/data-source.tf @@ -0,0 +1,20 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_laas_status" "status" { + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_laas_status.status +} diff --git a/examples/data-sources/edgecenter_lblistener/data-source.tf b/examples/data-sources/edgecenter_lblistener/data-source.tf index 865a3c9b..1a8a6c09 100644 --- a/examples/data-sources/edgecenter_lblistener/data-source.tf +++ b/examples/data-sources/edgecenter_lblistener/data-source.tf @@ -1,29 +1,23 @@ -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 1 -data "edgecenter_lbpool" "pool1" { - region_id = var.region_id - project_id = var.project_id - name = "test-lbpool" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_project" "pr" { + name = "test" } -output "pool1" { - value = data.edgecenter_lbpool.pool1 +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -# Example 2 -data "edgecenter_lbpool" "pool2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_lblistener" "l" { + name = "test-listener" + loadbalancer_id = "59b2eabc-c0a8-4545-8081-979bd963c6ab" //optional + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } -output "pool2" { - value = data.edgecenter_lbpool.pool2 +output "view" { + value = data.edgecenter_lblistener.l } + diff --git a/examples/data-sources/edgecenter_lblistener/variables.tf b/examples/data-sources/edgecenter_lblistener/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/data-sources/edgecenter_lblistener/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/data-sources/edgecenter_lbpool/data-source.tf b/examples/data-sources/edgecenter_lbpool/data-source.tf index c9bac1e9..088f84de 100644 --- a/examples/data-sources/edgecenter_lbpool/data-source.tf +++ b/examples/data-sources/edgecenter_lbpool/data-source.tf @@ -1,29 +1,22 @@ -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 1 -data "edgecenter_lblistener" "listener1" { - region_id = var.region_id - project_id = var.project_id - name = "test-lblistener" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_project" "pr" { + name = "test" } -output "listener1" { - value = data.edgecenter_lblistener.listener1 +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -# Example 2 -data "edgecenter_lblistener" "listener2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" - loadbalancer_id = edgecenter_loadbalancer.lb.id +data "edgecenter_lbpool" "pool" { + name = "test-pool" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } -output "listener2" { - value = data.edgecenter_lblistener.listener2 +output "view" { + value = data.edgecenter_lbpool.pool } + diff --git a/examples/data-sources/edgecenter_lbpool/variables.tf b/examples/data-sources/edgecenter_lbpool/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/data-sources/edgecenter_lbpool/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/data-sources/edgecenter_loadbalancer/data-source.tf b/examples/data-sources/edgecenter_loadbalancer/data-source.tf index fb1f2e2c..e9239765 100644 --- a/examples/data-sources/edgecenter_loadbalancer/data-source.tf +++ b/examples/data-sources/edgecenter_loadbalancer/data-source.tf @@ -1,21 +1,22 @@ -# Example 1 -data "edgecenter_loadbalancer" "lb1" { - region_id = var.region_id - project_id = var.project_id - name = "test-loadbalancer" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "lb1" { - value = data.edgecenter_loadbalancer.lb1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_loadbalancer" "lb2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "lb2" { - value = data.edgecenter_loadbalancer.lb2 +data "edgecenter_loadbalancer" "lb" { + name = "test-lb" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } + +output "view" { + value = data.edgecenter_loadbalancer.lb +} + diff --git a/examples/data-sources/edgecenter_loadbalancer/variables.tf b/examples/data-sources/edgecenter_loadbalancer/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/data-sources/edgecenter_loadbalancer/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/data-sources/edgecenter_loadbalancerv2/data-source.tf b/examples/data-sources/edgecenter_loadbalancerv2/data-source.tf new file mode 100644 index 00000000..24411132 --- /dev/null +++ b/examples/data-sources/edgecenter_loadbalancerv2/data-source.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_loadbalancerv2" "lb" { + name = "test-lb" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_loadbalancerv2.lb +} + diff --git a/examples/data-sources/edgecenter_network/data-source.tf b/examples/data-sources/edgecenter_network/data-source.tf new file mode 100644 index 00000000..fe361fa3 --- /dev/null +++ b/examples/data-sources/edgecenter_network/data-source.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_network" "tnw" { + name = "example" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_network.tnw +} + diff --git a/examples/data-sources/edgecenter_project/data-source.tf b/examples/data-sources/edgecenter_project/data-source.tf new file mode 100644 index 00000000..a5abad3d --- /dev/null +++ b/examples/data-sources/edgecenter_project/data-source.tf @@ -0,0 +1,7 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} diff --git a/examples/data-sources/edgecenter_region/data-source.tf b/examples/data-sources/edgecenter_region/data-source.tf new file mode 100644 index 00000000..97e1e1c6 --- /dev/null +++ b/examples/data-sources/edgecenter_region/data-source.tf @@ -0,0 +1,7 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} diff --git a/examples/data-sources/edgecenter_reservedfixedip/data-source.tf b/examples/data-sources/edgecenter_reservedfixedip/data-source.tf new file mode 100644 index 00000000..8c4f0f65 --- /dev/null +++ b/examples/data-sources/edgecenter_reservedfixedip/data-source.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_reservedfixedip" "ip" { + fixed_ip_address = "192.168.0.66" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_reservedfixedip.ip +} + diff --git a/examples/data-sources/edgecenter_router/data-source.tf b/examples/data-sources/edgecenter_router/data-source.tf new file mode 100644 index 00000000..92eaa953 --- /dev/null +++ b/examples/data-sources/edgecenter_router/data-source.tf @@ -0,0 +1,23 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_router" "tr" { + name = "test_router" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_router.tr +} + + diff --git a/examples/data-sources/edgecenter_secret/data-source.tf b/examples/data-sources/edgecenter_secret/data-source.tf new file mode 100644 index 00000000..19025670 --- /dev/null +++ b/examples/data-sources/edgecenter_secret/data-source.tf @@ -0,0 +1,21 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_secret" "lb_https" { + name = "lb_https" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_secret.lb_https +} diff --git a/examples/data-sources/edgecenter_securitygroup/data-source.tf b/examples/data-sources/edgecenter_securitygroup/data-source.tf new file mode 100644 index 00000000..b1f5d47e --- /dev/null +++ b/examples/data-sources/edgecenter_securitygroup/data-source.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_securitygroup" "default" { + name = "default" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_securitygroup.default +} + diff --git a/examples/data-sources/edgecenter_servergroup/data-source.tf b/examples/data-sources/edgecenter_servergroup/data-source.tf new file mode 100644 index 00000000..92a34ad8 --- /dev/null +++ b/examples/data-sources/edgecenter_servergroup/data-source.tf @@ -0,0 +1,21 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_servergroup" "default" { + name = "default" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_servergroup.default +} diff --git a/examples/data-sources/edgecenter_storage_s3/data-source.tf b/examples/data-sources/edgecenter_storage_s3/data-source.tf new file mode 100644 index 00000000..8fe76b99 --- /dev/null +++ b/examples/data-sources/edgecenter_storage_s3/data-source.tf @@ -0,0 +1,7 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_storage_s3" "example_s3" { + name = "example" +} diff --git a/examples/data-sources/edgecenter_storage_s3_bucket/data-source.tf b/examples/data-sources/edgecenter_storage_s3_bucket/data-source.tf new file mode 100644 index 00000000..3f09e3c5 --- /dev/null +++ b/examples/data-sources/edgecenter_storage_s3_bucket/data-source.tf @@ -0,0 +1,8 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_storage_s3_bucket" "example_s3_bucket" { + storage_id = 1 + name = "example1bucket2name" +} diff --git a/examples/data-sources/edgecenter_subnet/data-source.tf b/examples/data-sources/edgecenter_subnet/data-source.tf new file mode 100644 index 00000000..ad5b739d --- /dev/null +++ b/examples/data-sources/edgecenter_subnet/data-source.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +data "edgecenter_project" "pr" { + name = "test" +} + +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" +} + +data "edgecenter_subnet" "tsn" { + name = "subtest" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id +} + +output "view" { + value = data.edgecenter_subnet.tsn +} + diff --git a/examples/data-sources/edgecenter_volume/data-source.tf b/examples/data-sources/edgecenter_volume/data-source.tf index 72265c92..00b23dc6 100644 --- a/examples/data-sources/edgecenter_volume/data-source.tf +++ b/examples/data-sources/edgecenter_volume/data-source.tf @@ -1,21 +1,22 @@ -# Example 1 -data "edgecenter_volume" "volume1" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -output "volume1" { - value = data.edgecenter_volume.volume1 +data "edgecenter_project" "pr" { + name = "test" } -# Example 2 -data "edgecenter_volume" "volume2" { - region_id = var.region_id - project_id = var.project_id - id = "00000000-0000-0000-0000-000000000000" +data "edgecenter_region" "rg" { + name = "ED-10 Preprod" } -output "volume2" { - value = data.edgecenter_volume.volume2 +data "edgecenter_volume" "tv" { + name = "test-hd" + region_id = data.edgecenter_region.rg.id + project_id = data.edgecenter_project.pr.id } + +output "view" { + value = data.edgecenter_volume.tv +} + diff --git a/examples/data-sources/edgecenter_volume/variables.tf b/examples/data-sources/edgecenter_volume/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/data-sources/edgecenter_volume/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index bd2b2f61..95ccf259 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -2,12 +2,219 @@ terraform { required_providers { edgecenter = { source = "Edge-Center/edgecenter" - version = ">=1.0.0" + version = ">= 0.1.12" } } } provider "edgecenter" { - api_key = "251$d3361.............1b35f26d8" - edgecenter_cloud_api = "https://api.edgecenter.ru/cloud" + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_keypair" "kp" { + project_id = 1 + public_key = "your oub key" + sshkey_name = "testkey" +} + +resource "edgecenter_network" "network" { + name = "network_example" + mtu = 1450 + type = "vxlan" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet" { + name = "subnet_example" + cidr = "192.168.10.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = ["8.8.4.4", "1.1.1.1"] + + host_routes { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" + } + + gateway_ip = "192.168.10.1" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet2" { + name = "subnet2_example" + cidr = "192.168.20.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = ["8.8.4.4", "1.1.1.1"] + + host_routes { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" + } + + gateway_ip = "192.168.20.1" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "first_volume" { + name = "boot volume" + type_name = "ssd_hiiops" + size = 6 + image_id = "f4ce3d30-e29c-4cfd-811f-46f383b6081f" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "second_volume" { + name = "second volume" + type_name = "ssd_hiiops" + image_id = "f4ce3d30-e29c-4cfd-811f-46f383b6081f" + size = 6 + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "third_volume" { + name = "third volume" + type_name = "ssd_hiiops" + size = 6 + region_id = 1 + project_id = 1 +} + +resource "edgecenter_instance" "instance" { + flavor_id = "g1-standard-2-4" + name = "test" + keypair_name = edgecenter_keypair.kp.sshkey_name + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.first_volume.id + boot_index = 0 + } + + interface { + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet.id + } + + interface { + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet2.id + } + + security_group { + id = "66988147-f1b9-43b2-aaef-dee6d009b5b7" + name = "default" + } + + metadata { + key = "some_key" + value = "some_data" + } + + configuration { + key = "some_key" + value = "some_data" + } + + region_id = 1 + project_id = 1 +} + +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test1" + flavor = "lb1-1-2" + listener { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } +} + +resource "edgecenter_lbpool" "pl" { + project_id = 1 + region_id = 1 + name = "test_pool1" + protocol = "HTTP" + lb_algorithm = "LEAST_CONNECTIONS" + loadbalancer_id = edgecenter_loadbalancer.lb.id + listener_id = edgecenter_loadbalancer.lb.listener.0.id + health_monitor { + type = "PING" + delay = 60 + max_retries = 5 + timeout = 10 + } + session_persistence { + type = "APP_COOKIE" + cookie_name = "test_new_cookie" + } +} + +resource "edgecenter_lbmember" "lbm" { + project_id = 1 + region_id = 1 + pool_id = edgecenter_lbpool.pl.id + instance_id = edgecenter_instance.instance.id + address = tolist(edgecenter_instance.instance.interface).0.ip_address + protocol_port = 8081 + weight = 5 +} + +resource "edgecenter_instance" "instance2" { + flavor_id = "g1-standard-2-4" + name = "test2" + keypair_name = edgecenter_keypair.kp.sshkey_name + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.second_volume.id + boot_index = 0 + } + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.third_volume.id + boot_index = 1 + } + + interface { + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet.id + } + + security_group { + id = "66988147-f1b9-43b2-aaef-dee6d009b5b7" + name = "default" + } + + metadata { + key = "some_key" + value = "some_data" + } + + configuration { + key = "some_key" + value = "some_data" + } + + region_id = 1 + project_id = 1 +} + +resource "edgecenter_lbmember" "lbm2" { + project_id = 1 + region_id = 1 + pool_id = edgecenter_lbpool.pl.id + instance_id = edgecenter_instance.instance2.id + address = tolist(edgecenter_instance.instance2.interface).0.ip_address + protocol_port = 8081 + weight = 5 } diff --git a/examples/resources/edgecenter_baremetal/import.sh b/examples/resources/edgecenter_baremetal/import.sh new file mode 100644 index 00000000..e7e6cfb5 --- /dev/null +++ b/examples/resources/edgecenter_baremetal/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_baremetal.instance1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_baremetal/resource.tf b/examples/resources/edgecenter_baremetal/resource.tf new file mode 100644 index 00000000..049e62b2 --- /dev/null +++ b/examples/resources/edgecenter_baremetal/resource.tf @@ -0,0 +1,25 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_baremetal" "bm" { + name = "test bm instance" + region_id = 1 + project_id = 1 + flavor_id = "bm1-infrastructure-small" + image_id = "1ee7ccee-5003-48c9-8ae0-d96063af75b2" // your image id + + //additional interface, available type is 'subnet' or 'external' + // interface { + // type = "subnet" + // network_id = "9c7867fb-f404-4a2d-8bb5-24acf2fccaf1" //your network_id + // subnet_id = "b68ea6e2-c2b6-4a8d-95eb-7194d12a2156" // your subnet_id + // } + + // interface { + // type = "external" + // is_parent = "true" // if is_parent = true interface cant be detached, and always connected first + // } + + keypair_name = "test" // your keypair name +} \ No newline at end of file diff --git a/examples/resources/edgecenter_cdn_origingroup/resource.tf b/examples/resources/edgecenter_cdn_origingroup/resource.tf new file mode 100644 index 00000000..a111ff16 --- /dev/null +++ b/examples/resources/edgecenter_cdn_origingroup/resource.tf @@ -0,0 +1,17 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_cdn_origingroup" "origin_group_1" { + name = "origin_group_1" + use_next = true + origin { + source = "example.com" + enabled = true + } + origin { + source = "mirror.example.com" + enabled = true + backup = true + } +} diff --git a/examples/resources/edgecenter_cdn_resource/resource.tf b/examples/resources/edgecenter_cdn_resource/resource.tf new file mode 100644 index 00000000..c320503a --- /dev/null +++ b/examples/resources/edgecenter_cdn_resource/resource.tf @@ -0,0 +1,45 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + + +resource "edgecenter_cdn_resource" "cdn_example_com" { + cname = "cdn.example.com" + origin_group = edgecenter_cdn_origingroup.origin_group_1.id + origin_protocol = "MATCH" + secondary_hostnames = ["cdn2.example.com"] + + options { + edge_cache_settings { + default = "8d" + } + browser_cache_settings { + value = "1d" + } + redirect_http_to_https { + value = true + } + gzip_on { + value = true + } + cors { + value = [ + "*" + ] + } + rewrite { + body = "/(.*) /$1" + } + webp { + jpg_quality = 55 + png_quality = 66 + } + + tls_versions { + enabled = true + value = [ + "TLSv1.2", + ] + } + } +} diff --git a/examples/resources/edgecenter_cdn_rule/resource.tf b/examples/resources/edgecenter_cdn_rule/resource.tf new file mode 100644 index 00000000..735493bd --- /dev/null +++ b/examples/resources/edgecenter_cdn_rule/resource.tf @@ -0,0 +1,78 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_cdn_rule" "cdn_example_com_rule_1" { + resource_id = edgecenter_cdn_resource.cdn_example_com.id + name = "All PNG images" + rule = "/folder/images/*.png" + + options { + edge_cache_settings { + default = "14d" + } + browser_cache_settings { + value = "14d" + } + redirect_http_to_https { + value = true + } + gzip_on { + value = true + } + cors { + value = [ + "*" + ] + } + rewrite { + body = "/(.*) /$1" + } + webp { + jpg_quality = 55 + png_quality = 66 + } + ignore_query_string { + value = true + } + } +} + +resource "edgecenter_cdn_rule" "cdn_example_com_rule_2" { + resource_id = edgecenter_cdn_resource.cdn_example_com.id + name = "All JS scripts" + rule = "/folder/images/*.js" + origin_protocol = "HTTP" + + options { + redirect_http_to_https { + enabled = false + value = true + } + gzip_on { + enabled = false + value = true + } + query_params_whitelist { + value = [ + "abc", + ] + } + } +} + +resource "edgecenter_cdn_origingroup" "origin_group_1" { + name = "origin_group_1" + use_next = true + origin { + source = "example.com" + enabled = true + } +} + +resource "edgecenter_cdn_resource" "cdn_example_com" { + cname = "cdn.example.com" + origin_group = edgecenter_cdn_origingroup.origin_group_1.id + origin_protocol = "MATCH" + secondary_hostnames = ["cdn2.example.com"] +} diff --git a/examples/resources/edgecenter_cdn_sslcert/resource.tf b/examples/resources/edgecenter_cdn_sslcert/resource.tf new file mode 100644 index 00000000..7c4a9180 --- /dev/null +++ b/examples/resources/edgecenter_cdn_sslcert/resource.tf @@ -0,0 +1,20 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +variable "cert" { + type = string + sensitive = true +} + +variable "private_key" { + type = string + sensitive = true +} + +resource "edgecenter_cdn_sslcert" "cdnopt_cert" { + name = "Test cert for cdnopt_bookatest_by" + cert = var.cert + private_key = var.private_key +} + diff --git a/examples/resources/edgecenter_dns_zone/import.sh b/examples/resources/edgecenter_dns_zone/import.sh new file mode 100644 index 00000000..42bdb88a --- /dev/null +++ b/examples/resources/edgecenter_dns_zone/import.sh @@ -0,0 +1,2 @@ +# import using zone name format +terraform import edgecenter_dns_zone.example_zone example_zone.com \ No newline at end of file diff --git a/examples/resources/edgecenter_dns_zone/resource.tf b/examples/resources/edgecenter_dns_zone/resource.tf new file mode 100644 index 00000000..6f14fba0 --- /dev/null +++ b/examples/resources/edgecenter_dns_zone/resource.tf @@ -0,0 +1,7 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_dns_zone" "example_zone" { + name = "example_zone.com" +} diff --git a/examples/resources/edgecenter_dns_zone_record/import.sh b/examples/resources/edgecenter_dns_zone_record/import.sh new file mode 100644 index 00000000..4fbe3c98 --- /dev/null +++ b/examples/resources/edgecenter_dns_zone_record/import.sh @@ -0,0 +1,2 @@ +# import using zone:domain:type format +terraform import edgecenter_dns_zone_record.example_rrset0 example.com:domain.example.com:A \ No newline at end of file diff --git a/examples/resources/edgecenter_dns_zone_record/resource.tf b/examples/resources/edgecenter_dns_zone_record/resource.tf new file mode 100644 index 00000000..4a641283 --- /dev/null +++ b/examples/resources/edgecenter_dns_zone_record/resource.tf @@ -0,0 +1,85 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +// +// example0: managing zone and records by TF using variables +// +variable "example_domain0" { + type = string + default = "examplezone.com" +} + +resource "edgecenter_dns_zone" "examplezone0" { + name = var.example_domain0 +} + +resource "edgecenter_dns_zone_record" "example_rrset0" { + zone = edgecenter_dns_zone.examplezone0.name + domain = edgecenter_dns_zone.examplezone0.name + type = "A" + ttl = 100 + + resource_record { + content = "127.0.0.100" + } + resource_record { + content = "127.0.0.200" + // enabled = false + } +} + +// +// example1: managing zone outside of TF +// +resource "edgecenter_dns_zone_record" "subdomain_examplezone" { + zone = "examplezone.com" + domain = "subdomain.examplezone.com" + type = "TXT" + ttl = 10 + + filter { + type = "geodistance" + limit = 1 + strict = true + } + + resource_record { + content = "1234" + enabled = true + + meta { + latlong = [52.367, 4.9041] + asn = [12345] + ip = ["1.1.1.1"] + notes = ["notes"] + continents = ["asia"] + countries = ["russia"] + default = true + } + } +} + +resource "edgecenter_dns_zone_record" "subdomain_examplezone_mx" { + zone = "examplezone.com" + domain = "subdomain.examplezone.com" + type = "MX" + ttl = 10 + + resource_record { + content = "10 mail.my.com." + enabled = true + } +} + +resource "edgecenter_dns_zone_record" "subdomain_examplezone_caa" { + zone = "examplezone.com" + domain = "subdomain.examplezone.com" + type = "CAA" + ttl = 10 + + resource_record { + content = "0 issue \"company.org; account=12345\"" + enabled = true + } +} diff --git a/examples/resources/edgecenter_faas_function/import.sh b/examples/resources/edgecenter_faas_function/import.sh new file mode 100644 index 00000000..d76f19f7 --- /dev/null +++ b/examples/resources/edgecenter_faas_function/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_faas_function.test 1:6:ns:test_func \ No newline at end of file diff --git a/examples/resources/edgecenter_faas_function/resource.tf b/examples/resources/edgecenter_faas_function/resource.tf new file mode 100644 index 00000000..85fc827a --- /dev/null +++ b/examples/resources/edgecenter_faas_function/resource.tf @@ -0,0 +1,31 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_faas_function" "func" { + project_id = 1 + region_id = 1 + name = "testf" + namespace = "ns4test" + description = "function description" + envs = { + BIG = "EXAMPLE2" + } + runtime = "go1.16.6" + code_text = <:: format +terraform import edgecenter_faas_namespace.test 1:6:ns \ No newline at end of file diff --git a/examples/resources/edgecenter_faas_namespace/resource.tf b/examples/resources/edgecenter_faas_namespace/resource.tf new file mode 100644 index 00000000..332e5624 --- /dev/null +++ b/examples/resources/edgecenter_faas_namespace/resource.tf @@ -0,0 +1,14 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_faas_namespace" "ns" { + project_id = 1 + region_id = 1 + name = "testns" + description = "test description" + envs = { + BIG_ENV = "EXAMPLE" + } +} + diff --git a/examples/resources/edgecenter_floatingip/import.sh b/examples/resources/edgecenter_floatingip/import.sh new file mode 100644 index 00000000..fa8bac21 --- /dev/null +++ b/examples/resources/edgecenter_floatingip/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_floatingip.fip1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_floatingip/resource.tf b/examples/resources/edgecenter_floatingip/resource.tf index 3f103920..21e311cd 100644 --- a/examples/resources/edgecenter_floatingip/resource.tf +++ b/examples/resources/edgecenter_floatingip/resource.tf @@ -1,8 +1,15 @@ -resource "edgecenter_floatingip" "fip" { - region_id = var.region_id - project_id = var.project_id - metadata = { - "key1" : "value1", - "key2" : "value2", +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_floatingip" "floating_ip" { + project_id = 1 + region_id = 1 + metadata_map = { + tag1 = "tag1_value" } + // fixed_ip_address = "192.168.10.39" // instance`s interface ip + // port_id = "5c992875-f653-4b7b-af5b-1dc3019e5ffa" //instance`s interface port_id } + + diff --git a/examples/resources/edgecenter_floatingip/variables.tf b/examples/resources/edgecenter_floatingip/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_floatingip/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/resources/edgecenter_instance/import.sh b/examples/resources/edgecenter_instance/import.sh new file mode 100644 index 00000000..3730cef3 --- /dev/null +++ b/examples/resources/edgecenter_instance/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_instance.instance1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_instance/resource.tf b/examples/resources/edgecenter_instance/resource.tf index 2bf98d32..651632a3 100644 --- a/examples/resources/edgecenter_instance/resource.tf +++ b/examples/resources/edgecenter_instance/resource.tf @@ -1,51 +1,135 @@ -# Example 1 -resource "edgecenter_instance" "instance1" { - region_id = var.region_id - project_id = var.project_id - name = "test-instance" - flavor = "g1-standard-2-4" - keypair_name = "test-keypair" - server_group_id = "00000000-0000-0000-0000-000000000000" - security_groups = ["00000000-0000-0000-0000-000000000000"] - user_data = "#cloud-config\npassword: ваш пароль\nchpasswd: { expire: False }\nssh_pwauth: True" - - metadata = { - "key1" : "value1", - "key2" : "value2", +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_network" "network" { + name = "network_example" + mtu = 1450 + type = "vxlan" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet" { + name = "subnet_example" + cidr = "192.168.10.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = ["8.8.4.4", "1.1.1.1"] + + host_routes { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" } + gateway_ip = "192.168.10.1" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "first_volume" { + name = "boot volume" + type_name = "ssd_hiiops" + size = 5 + image_id = "f4ce3d30-e29c-4cfd-811f-46f383b6081f" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_volume" "second_volume" { + name = "second volume" + type_name = "ssd_hiiops" + size = 5 + region_id = 1 + project_id = 1 +} + +resource "edgecenter_instance" "instance" { + flavor_id = "g1-standard-2-4" + name = "test" + volume { - name = "system" - type_name = "ssd_hiiops" - size = 30 - source = "image" - image_id = "00000000-0000-0000-0000-000000000000" - attachment_tag = "tag" - boot_index = 0 - metadata = { - "tag" : "system" - } + source = "existing-volume" + volume_id = edgecenter_volume.first_volume.id + boot_index = 0 } - interface { - type = "any_subnet" - network_id = "00000000-0000-0000-0000-000000000000" - floating_ip_source = "new" + volume { + source = "existing-volume" + volume_id = edgecenter_volume.second_volume.id + boot_index = 1 } interface { - type = "any_subnet" - network_id = "00000000-0000-0000-0000-000000000000" - floating_ip_source = "existing" - floating_ip = "00000000-0000-0000-0000-000000000000" + type = "subnet" + network_id = edgecenter_network.network.id + subnet_id = edgecenter_subnet.subnet.id + security_groups = ["d75db0b2-58f1-4a11-88c6-a932bb897310"] + } + + metadata_map = { + some_key = "some_value" + stage = "dev" + } + + configuration { + key = "some_key" + value = "some_data" + } + + region_id = 1 + project_id = 1 +} + +//*** +// another one example with one interface to private network and fip to internet +//*** + +resource "edgecenter_reservedfixedip" "fixed_ip" { + project_id = 1 + region_id = 1 + type = "ip_address" + network_id = "faf6507b-1ff1-4ebf-b540-befd5c09fe06" + fixed_ip_address = "192.168.13.6" + is_vip = false +} + +resource "edgecenter_volume" "first_volume" { + name = "boot volume" + type_name = "ssd_hiiops" + size = 10 + image_id = "6dc4e061-6fab-41f3-91a3-0ba848fb32d9" + project_id = 1 + region_id = 1 +} + +resource "edgecenter_floatingip" "fip" { + project_id = 1 + region_id = 1 + fixed_ip_address = edgecenter_reservedfixedip.fixed_ip.fixed_ip_address + port_id = edgecenter_reservedfixedip.fixed_ip.port_id +} + + +resource "edgecenter_instance" "v" { + project_id = 1 + region_id = 1 + name = "hello" + flavor_id = "g1-standard-1-2" + + volume { + source = "existing-volume" + volume_id = edgecenter_volume.first_volume.id + boot_index = 0 } interface { - type = "subnet" - network_id = "00000000-0000-0000-0000-000000000000" - subnet_id = "00000000-0000-0000-0000-000000000000" + type = "reserved_fixed_ip" + port_id = edgecenter_reservedfixedip.fixed_ip.port_id + fip_source = "existing" + existing_fip_id = edgecenter_floatingip.fip.id + security_groups = ["ada84751-fcca-4491-9249-2dfceb321616"] } } -# Example 2 -# TBD with separate resource + + diff --git a/examples/resources/edgecenter_instance/variables.tf b/examples/resources/edgecenter_instance/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_instance/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/resources/edgecenter_k8s/import.sh b/examples/resources/edgecenter_k8s/import.sh new file mode 100644 index 00000000..e0cac597 --- /dev/null +++ b/examples/resources/edgecenter_k8s/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_k8s.cluster1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_k8s/resource.tf b/examples/resources/edgecenter_k8s/resource.tf new file mode 100644 index 00000000..550426a3 --- /dev/null +++ b/examples/resources/edgecenter_k8s/resource.tf @@ -0,0 +1,22 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_k8s" "v" { + project_id = 1 + region_id = 1 + version = "1.25.11" + name = "tf-k8s" + fixed_network = "6bf878c1-1ce4-47c3-a39b-6b5f1d79bf25" + fixed_subnet = "dc3a3ea9-86ae-47ad-a8e8-79df0ce04839" + keypair = "tf-keypair" + pool { + name = "tf-pool" + flavor_id = "g1-standard-1-2" + min_node_count = 1 + max_node_count = 2 + node_count = 1 + docker_volume_size = 2 + } +} + diff --git a/examples/resources/edgecenter_k8s_pool/import.sh b/examples/resources/edgecenter_k8s_pool/import.sh new file mode 100644 index 00000000..79754270 --- /dev/null +++ b/examples/resources/edgecenter_k8s_pool/import.sh @@ -0,0 +1,2 @@ +# import using ::: format +terraform import edgecenter_k8s_pool.k8s_pool1 1:6:a775dd94-4e9c-4da7-9f0e-ffc9ae34446b:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_k8s_pool/resource.tf b/examples/resources/edgecenter_k8s_pool/resource.tf new file mode 100644 index 00000000..7f26096c --- /dev/null +++ b/examples/resources/edgecenter_k8s_pool/resource.tf @@ -0,0 +1,16 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_k8s_pool" "v" { + project_id = 1 + region_id = 1 + cluster_id = "6bf878c1-1ce4-47c3-a39b-6b5f1d79bf25" + name = "tf-pool" + flavor_id = "g1-standard-1-2" + min_node_count = 1 + max_node_count = 2 + node_count = 1 + docker_volume_size = 2 +} + diff --git a/examples/resources/edgecenter_keypair/resource.tf b/examples/resources/edgecenter_keypair/resource.tf new file mode 100644 index 00000000..56b2eda2 --- /dev/null +++ b/examples/resources/edgecenter_keypair/resource.tf @@ -0,0 +1,13 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_keypair" "kp" { + project_id = 1 + public_key = "your public key here" + sshkey_name = "test" +} + +output "kp" { + value = edgecenter_keypair.kp +} diff --git a/examples/resources/edgecenter_laas_topic/import.sh b/examples/resources/edgecenter_laas_topic/import.sh new file mode 100644 index 00000000..6d2ca5ca --- /dev/null +++ b/examples/resources/edgecenter_laas_topic/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_laas_topic.test 1:6:test_topic \ No newline at end of file diff --git a/examples/resources/edgecenter_laas_topic/resource.tf b/examples/resources/edgecenter_laas_topic/resource.tf new file mode 100644 index 00000000..59889161 --- /dev/null +++ b/examples/resources/edgecenter_laas_topic/resource.tf @@ -0,0 +1,10 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_laas_topic" "test" { + region_id = 1 + project_id = 1 + + name = "test" +} diff --git a/examples/resources/edgecenter_lblistener/import.sh b/examples/resources/edgecenter_lblistener/import.sh new file mode 100644 index 00000000..0a375438 --- /dev/null +++ b/examples/resources/edgecenter_lblistener/import.sh @@ -0,0 +1,2 @@ +# import using ::: format +terraform import edgecenter_lblistener.lblistener1 1:6:a775dd94-4e9c-4da7-9f0e-ffc9ae34446b:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_lblistener/resource.tf b/examples/resources/edgecenter_lblistener/resource.tf index 5bab67ec..ef63f0a8 100644 --- a/examples/resources/edgecenter_lblistener/resource.tf +++ b/examples/resources/edgecenter_lblistener/resource.tf @@ -1,16 +1,20 @@ -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other_fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -resource "edgecenter_lblistener" "listener" { - region_id = var.region_id - project_id = var.project_id - name = "test-lblistener" - loadbalancer_id = edgecenter_loadbalancer.lb.id - protocol_port = 80 - protocol = "HTTP" - insert_x_forwarded = true - allowed_cidrs = ["10.10.0.0/24"] +resource "edgecenter_loadbalancerv2" "lb" { + project_id = 1 + region_id = 1 + name = "test" + flavor = "lb1-1-2" } + +resource "edgecenter_lblistener" "listener" { + project_id = 1 + region_id = 1 + name = "test" + protocol = "TCP" + protocol_port = 36621 + allowed_cidrs = ["127.0.0.0/24", "192.168.0.0/24"] + loadbalancer_id = edgecenter_loadbalancerv2.lb.id +} \ No newline at end of file diff --git a/examples/resources/edgecenter_lblistener/variables.tf b/examples/resources/edgecenter_lblistener/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_lblistener/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/resources/edgecenter_lbmember/import.sh b/examples/resources/edgecenter_lbmember/import.sh new file mode 100644 index 00000000..496322e4 --- /dev/null +++ b/examples/resources/edgecenter_lbmember/import.sh @@ -0,0 +1,2 @@ +# import using ::: format +terraform import edgecenter_lbmember.lbmember1 1:6:a775dd94-4e9c-4da7-9f0e-ffc9ae34446b:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_lbmember/resource.tf b/examples/resources/edgecenter_lbmember/resource.tf index 22d7cba7..f99eaff7 100644 --- a/examples/resources/edgecenter_lbmember/resource.tf +++ b/examples/resources/edgecenter_lbmember/resource.tf @@ -1,14 +1,46 @@ -resource "edgecenter_lbpool" "pool" { - region_id = var.region_id - project_id = var.project_id - // other_fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -resource "edgecenter_lbmember" "member" { - region_id = var.region_id - project_id = var.project_id - pool_id = edgecenter_lbpool.pool.id - address = "10.10.0.7" - protocol_port = 9099 - weight = 20 +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test1" + flavor = "lb1-1-2" + listeners { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } } + +resource "edgecenter_lbpool" "pl" { + project_id = 1 + region_id = 1 + name = "test_pool1" + protocol = "HTTP" + lb_algorithm = "LEAST_CONNECTIONS" + loadbalancer_id = edgecenter_loadbalancer.lb.id + listener_id = edgecenter_loadbalancer.lb.listeners.0.id + health_monitor { + type = "PING" + delay = 60 + max_retries = 5 + timeout = 10 + } + session_persistence { + type = "APP_COOKIE" + cookie_name = "test_new_cookie" + } +} + +resource "edgecenter_lbmember" "lbm" { + project_id = 1 + region_id = 1 + pool_id = edgecenter_lbpool.pl.id + address = "10.10.2.15" + protocol_port = 8081 + weight = 5 +} + + diff --git a/examples/resources/edgecenter_lbmember/variables.tf b/examples/resources/edgecenter_lbmember/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_lbmember/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/resources/edgecenter_lbpool/import.sh b/examples/resources/edgecenter_lbpool/import.sh new file mode 100644 index 00000000..25214f16 --- /dev/null +++ b/examples/resources/edgecenter_lbpool/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_lbpool.lbpool1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_lbpool/resource.tf b/examples/resources/edgecenter_lbpool/resource.tf index ebc2aa37..a9d4c4c0 100644 --- a/examples/resources/edgecenter_lbpool/resource.tf +++ b/examples/resources/edgecenter_lbpool/resource.tf @@ -1,25 +1,35 @@ -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - // other_fields +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -resource "edgecenter_lblistener" "lis" { - region_id = var.region_id - project_id = var.project_id - // other_fields +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test1" + flavor = "lb1-1-2" + listener { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } } -resource "edgecenter_lbpool" "pool" { - region_id = var.region_id - project_id = var.project_id - name = "test-lbpool" - lb_algorithm = "LEAST_CONNECTIONS" +resource "edgecenter_lbpool" "pl" { + project_id = 1 + region_id = 1 + name = "test_pool1" protocol = "HTTP" + lb_algorithm = "LEAST_CONNECTIONS" loadbalancer_id = edgecenter_loadbalancer.lb.id - listener_id = edgecenter_lblistener.lis.id - healthmonitor { - type = "TCP" - delay = 70 + listener_id = edgecenter_loadbalancer.lb.listener.0.id + health_monitor { + type = "PING" + delay = 60 + max_retries = 5 + timeout = 10 + } + session_persistence { + type = "APP_COOKIE" + cookie_name = "test_new_cookie" } } diff --git a/examples/resources/edgecenter_lbpool/variables.tf b/examples/resources/edgecenter_lbpool/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_lbpool/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/resources/edgecenter_lifecyclepolicy/import.sh b/examples/resources/edgecenter_lifecyclepolicy/import.sh new file mode 100644 index 00000000..33f6aaf0 --- /dev/null +++ b/examples/resources/edgecenter_lifecyclepolicy/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_lifecyclepolicy.lifecyclepolicy1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_lifecyclepolicy/resource.tf b/examples/resources/edgecenter_lifecyclepolicy/resource.tf new file mode 100644 index 00000000..488b684f --- /dev/null +++ b/examples/resources/edgecenter_lifecyclepolicy/resource.tf @@ -0,0 +1,30 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_lifecyclepolicy" "lp" { + project_id = 1 + region_id = 1 + name = "test" + status = "active" + action = "volume_snapshot" + volume { + id = "fe93bfdd-4ce3-4041-b89b-4f10d0d49498" + } + schedule { + max_quantity = 4 + interval { + weeks = 1 + days = 2 + hours = 3 + minutes = 4 + } + resource_name_template = "reserve snap of the volume {volume_id}" + retention_time { + weeks = 4 + days = 3 + hours = 2 + minutes = 1 + } + } +} \ No newline at end of file diff --git a/examples/resources/edgecenter_loadbalancer/import.sh b/examples/resources/edgecenter_loadbalancer/import.sh new file mode 100644 index 00000000..f23cc6d0 --- /dev/null +++ b/examples/resources/edgecenter_loadbalancer/import.sh @@ -0,0 +1,2 @@ +# import using ::: format, listener_id - nested listener id +terraform import edgecenter_loadbalancer.loadbalancer1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7:a336f28c-fbb0-4256-9545-e905bed9f48f \ No newline at end of file diff --git a/examples/resources/edgecenter_loadbalancer/resource.tf b/examples/resources/edgecenter_loadbalancer/resource.tf index 39f2a1af..e594889c 100644 --- a/examples/resources/edgecenter_loadbalancer/resource.tf +++ b/examples/resources/edgecenter_loadbalancer/resource.tf @@ -1,15 +1,19 @@ -# Example 1 -resource "edgecenter_loadbalancer" "lb" { - region_id = var.region_id - project_id = var.project_id - name = "test-lb" - flavor_name = "lb1-1-2" - vip_network_id = "00000000-0000-0000-0000-000000000000" - floating_ip_source = "new" - metadata = { - "tag" : "system" - } +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 2 -# TBD with separate resource +resource "edgecenter_loadbalancer" "lb" { + project_id = 1 + region_id = 1 + name = "test" + flavor = "lb1-1-2" + //when upgrading to version 0.2.28 nested listener max length reduced to 1 + //that mean, if you had more than one nested listener and removed them from + //schema they not delete in the cloud. User has to delete it manually and + //recreate as edgecenter_lblistener resource + listener { + name = "test" + protocol = "HTTP" + protocol_port = 80 + } +} \ No newline at end of file diff --git a/examples/resources/edgecenter_loadbalancer/variables.tf b/examples/resources/edgecenter_loadbalancer/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_loadbalancer/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/examples/resources/edgecenter_loadbalancerv2/import.sh b/examples/resources/edgecenter_loadbalancerv2/import.sh new file mode 100644 index 00000000..312eb5e5 --- /dev/null +++ b/examples/resources/edgecenter_loadbalancerv2/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_loadbalancer.loadbalancer1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_loadbalancerv2/resource.tf b/examples/resources/edgecenter_loadbalancerv2/resource.tf new file mode 100644 index 00000000..5d165eff --- /dev/null +++ b/examples/resources/edgecenter_loadbalancerv2/resource.tf @@ -0,0 +1,13 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_loadbalancerv2" "lb" { + project_id = 1 + region_id = 1 + name = "test" + flavor = "lb1-1-2" + metadata_map = { + tag1 = "tag1_value" + } +} diff --git a/examples/resources/edgecenter_network/import.sh b/examples/resources/edgecenter_network/import.sh new file mode 100644 index 00000000..14253f00 --- /dev/null +++ b/examples/resources/edgecenter_network/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_network.metwork1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_network/resource.tf b/examples/resources/edgecenter_network/resource.tf new file mode 100644 index 00000000..8f1dc511 --- /dev/null +++ b/examples/resources/edgecenter_network/resource.tf @@ -0,0 +1,10 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_network" "network" { + name = "network_example" + type = "vxlan" + region_id = 1 + project_id = 1 +} diff --git a/examples/resources/edgecenter_reservedfixedip/import.sh b/examples/resources/edgecenter_reservedfixedip/import.sh new file mode 100644 index 00000000..ba5463ec --- /dev/null +++ b/examples/resources/edgecenter_reservedfixedip/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_reservedfixedip.reservedfixedip1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_reservedfixedip/resource.tf b/examples/resources/edgecenter_reservedfixedip/resource.tf new file mode 100644 index 00000000..dcbd90e4 --- /dev/null +++ b/examples/resources/edgecenter_reservedfixedip/resource.tf @@ -0,0 +1,10 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_reservedfixedip" "fixed_ip" { + project_id = 1 + region_id = 1 + type = "external" + is_vip = false +} \ No newline at end of file diff --git a/examples/resources/edgecenter_router/import.sh b/examples/resources/edgecenter_router/import.sh new file mode 100644 index 00000000..855098ee --- /dev/null +++ b/examples/resources/edgecenter_router/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_router.router1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_router/resource.tf b/examples/resources/edgecenter_router/resource.tf new file mode 100644 index 00000000..a4ed1ed0 --- /dev/null +++ b/examples/resources/edgecenter_router/resource.tf @@ -0,0 +1,38 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_router" "router" { + name = "router_example" + + dynamic "external_gateway_info" { + iterator = egi + for_each = var.external_gateway_info + content { + type = egi.value.type + enable_snat = egi.value.enable_snat + network_id = egi.value.network_id + } + } + + dynamic "interfaces" { + iterator = ifaces + for_each = var.interfaces + content { + type = ifaces.value.type + subnet_id = ifaces.value.subnet_id + } + } + + dynamic "routes" { + iterator = rs + for_each = var.routes + content { + destination = rs.value.destination + nexthop = rs.value.nexthop + } + } + + region_id = 1 + project_id = 1 +} \ No newline at end of file diff --git a/examples/resources/edgecenter_router/vars.tf b/examples/resources/edgecenter_router/vars.tf new file mode 100755 index 00000000..d54db8ce --- /dev/null +++ b/examples/resources/edgecenter_router/vars.tf @@ -0,0 +1,48 @@ +variable "external_gateway_info" { + type = list(object({ + type = string + enable_snat = bool + network_id = string + })) + default = [ + { + type = "manual" + enable_snat = false + network_id = "" //set external network id + }, + ] +} + +variable "interfaces" { + type = list(object({ + type = string + subnet_id = string + })) + default = [ + { + type = "subnet" + subnet_id = "9bc36cf6-407c-4a74-bc83-ce3aa3854c3d" + }, + { + type = "subnet" + subnet_id = "f3f6a294-a319-4db4-84b6-6016a3481924" + }, + ] +} + +variable "routes" { + type = list(object({ + destination = string + nexthop = string + })) + default = [ + { + destination = "192.168.101.0/24" + nexthop = "192.168.100.2" + }, + { + destination = "192.168.102.0/24" + nexthop = "192.168.100.3" + }, + ] +} \ No newline at end of file diff --git a/examples/resources/edgecenter_secret/import.sh b/examples/resources/edgecenter_secret/import.sh new file mode 100644 index 00000000..aa50112e --- /dev/null +++ b/examples/resources/edgecenter_secret/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_secret.secret_id 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_secret/resource.tf b/examples/resources/edgecenter_secret/resource.tf new file mode 100644 index 00000000..4af57a18 --- /dev/null +++ b/examples/resources/edgecenter_secret/resource.tf @@ -0,0 +1,14 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_secret" "lb_https" { + region_id = 1 + project_id = 1 + + name = "test" + private_key = "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDQ4E6U0vql4EST\n8o41TlHRz6MKmMhddVUjM2juTKjxv4WuB4T3z/wokznEjQg4H7gfYEKeCJqelrfq\ntdOtbPsznSceMOXB5uA2Sc9WVKwk7owoRJxPd4LQeOcarVOFdIzudzkgSK/oV7Za\nL8Y2hylsB4SX2cfbULtmW/WDePp3YZAL6zYV1fXJSnK+hL2iUSqikiViEGRta+47\nnaTKZnnmSgojdshzsw0wlF/PgRJ/Anf9j9J8ratdJP81yAG5daU3L2NdJ3qx9UbV\ntKnSq2z2u4yx6xdb4t4WFQBKNjC6+YZN/gI5lp96p3FNTNS4PKYxAAUrnCwf0EE3\n7dOR4eWlAgMBAAECggEBALPm3ge0h4li1e4PVYh4AmSRT74KxVgpfMCqwM+uWzyM\nVpkDhPTjwC06UOEHD3M3bqAninkOtA2vhoyzOrP+T4Wu70hDmUAemDJp9BhJKVNN\n2o28Olz/dD4WRAZoDq29Kr0hFqTFtiyJj1eyGihQ1c5j00HuowI0UJPi1Fz+T8uN\nPwukUtTPYwEds6SApii3v9VKjmvbRDmsbHU3KkUoaeqpRnRagyp1vtoLXigezUcK\nrQcoh6wlKtvj0YLR2lxq9Wmj1nn6m3F5Bom54X8o18tcOmFSRudRb+Fxjb0jnqSK\nAsyVlZg4alTBQUmx9gIKv0oSJAIh2nXdclECkGjs8WkCgYEA9xvdDWephsbv+X3k\nndnDG9JTxfrR6HMHPrUrTaZ8/VD+Qw4zuReoNGkcQbV3Cb26egprWQWfYc9+l6mU\nAWgOjFgeGie1uwOwkhv6CfhE/iVvotJ3hOOsC5pLEhz4vRpO75C9wSehjfTYkP1m\nXEAhRTRbgMnvzChWyh5CEjosX5sCgYEA2GRHrG0JVxsYSCugLPKf9fSK4CQDm0bK\nywBwZtAWX0xhiHO/BW6PeK1Mqx2nbiWl1hXNpZKJNS9bnrZWym/yUqOvg2XJKjb6\nhHBvwAD1MOQ8Ysby4JHGCrMBEwlcDpI2wpMpXkKhU3X0XWjkqrhqCH/TETFKkqLt\nfJX/c9PTQ78CgYAEPek0grQJST7zVHLpNsS/pIOloWGbEOZt8CQ3KAV7P7mtov/G\nTJ6pj6hZhGjvtN8Pm0Aufgc3YZ11swaEY6nkRNr3bfkTpcORLoPDSgy9JB1feSdu\nE45vgI2LWQ34CQyT1jM7rpd6XVqeWos4SC2KB5UOh+ji40piG9TchT0fwwKBgA/M\nmpMTTvhGKSqzzLkbaeR6W11sI7tFmu7hdFN9Y/THTeO5l7vcy6ri9FMWEjBvnUEZ\nTG+HWG9CquzWoVWcgNPZ0anFV7+2Teo3j2E0cLKGJ4aKwhb1bcFAOpbaOxdxQ4BH\nYGDaeo7ucM4VJ4TzfAJs2stJjwlPzgknpoQddjJfAoGBAIFfnU8x/SrNhAqZrG9d\n3kpJ5LmbVswOYtj01KHM+KpEwOQVF+s2NOeHqyC7QUIWrue00+1MT88F9cNHDeWk\n0dEOJNWCfzcV85l8A+0p6/4qAW7h7RNiFqeA8GyVKCT8f7fu/7WpYw8D0aq8w5X/\nKZl+AjB+MzYFs71+SC4ohTlI\n-----END PRIVATE KEY-----" + certificate = "-----BEGIN CERTIFICATE-----\nMIIDpDCCAoygAwIBAgIJAIUvym0uaBHbMA0GCSqGSIb3DQEBCwUAMD0xCzAJBgNV\nBAYTAlJVMQ8wDQYDVQQIDAZNT1NDT1cxCzAJBgNVBAoMAkNBMRAwDgYDVQQDDAdS\nT09UIENBMB4XDTIxMDczMDE1MTU0NVoXDTMxMDcyODE1MTU0NVowTDELMAkGA1UE\nBhMCQ0ExDTALBgNVBAgMBE5vbmUxCzAJBgNVBAcMAk5CMQ0wCwYDVQQKDAROb25l\nMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK\nAoIBAQDQ4E6U0vql4EST8o41TlHRz6MKmMhddVUjM2juTKjxv4WuB4T3z/wokznE\njQg4H7gfYEKeCJqelrfqtdOtbPsznSceMOXB5uA2Sc9WVKwk7owoRJxPd4LQeOca\nrVOFdIzudzkgSK/oV7ZaL8Y2hylsB4SX2cfbULtmW/WDePp3YZAL6zYV1fXJSnK+\nhL2iUSqikiViEGRta+47naTKZnnmSgojdshzsw0wlF/PgRJ/Anf9j9J8ratdJP81\nyAG5daU3L2NdJ3qx9UbVtKnSq2z2u4yx6xdb4t4WFQBKNjC6+YZN/gI5lp96p3FN\nTNS4PKYxAAUrnCwf0EE37dOR4eWlAgMBAAGjgZcwgZQwVwYDVR0jBFAwTqFBpD8w\nPTELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1PU0NPVzELMAkGA1UECgwCQ0ExEDAO\nBgNVBAMMB1JPT1QgQ0GCCQCectJTETy4lTAJBgNVHRMEAjAAMAsGA1UdDwQEAwIE\n8DAhBgNVHREEGjAYgglsb2NhbGhvc3SCCyoubG9jYWxob3N0MA0GCSqGSIb3DQEB\nCwUAA4IBAQBqzJcwygLsVCTPlReUpcKVn84aFqzfZA0m7hYvH+7PDH/FM8SbX3zg\nteBL/PgQAZw1amO8xjeMc2Pe2kvi9VrpfTeGqNia/9axhGu3q/NEP0tyDFXAE2bR\njBdGhd5gCmg+X4WdHigCgn51cz5r2k3fSOIWP+TQWHqc8Yt+vZXnkwnQkRA1Ki7N\nWOiJjj/ae5RWwma/kJNmShTZn754gbQn06bAjNbPjclsHRLkawmLqikd1rYUhIdk\nOr1Nrl+CWMx3CXg0TVVdJ6rH3dO31uyvb+3qEY7WnL+HhZyr08ay8gJsEKPuPFA2\nxvveXqt9ceU5qh+8T7mHwGALEUw96QcP\n-----END CERTIFICATE-----" + certificate_chain = "-----BEGIN CERTIFICATE-----\nMIIC9jCCAd4CCQCectJTETy4lTANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJS\nVTEPMA0GA1UECAwGTU9TQ09XMQswCQYDVQQKDAJDQTEQMA4GA1UEAwwHUk9PVCBD\nQTAeFw0yMTA3MzAxNTExMzVaFw0yNDA1MTkxNTExMzVaMD0xCzAJBgNVBAYTAlJV\nMQ8wDQYDVQQIDAZNT1NDT1cxCzAJBgNVBAoMAkNBMRAwDgYDVQQDDAdST09UIENB\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo6tZ0NV6QIR/mvsqtAII\nzTTuBMrZR5OTwKvcGnhe4GVDwzJ/OgEWkghLAzOojcJvkfzJOtWwOXqwgphksc+7\n+vwIPTPt3iWjbQUzXK8pFLkjxrO8px/QxPuUrp+U6DTVvvgQesjMZ9jQRUFKOiCc\nu0st1N5Q/CJR4VOJxtYoLy1ZUlsABhwJ+6trkoOFTLRPlMUX1EIG57jYAotHvQFo\nc8UNx3KzvJsJJ56SniXCIkeu61IOt8aOXHU+3TLYhZnPiP311cMbXA0J3vGPRZwz\n25BZjF3IF/ShXlfzz76FjWUTAThc0+HA8lzx53xD4/n8HN+sGubGx9TvLyZimG/U\nGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAnK8Wzw33fR6R6pqV05XI9Yu8J+BwC\nCn2bKxxYwwQWZyX1as+UIlGuvyBRJba9W2UGMj95FQfWVdDyFC98spUur+O/5yL+\nNHH+dxGnkxIRc6RMIy+GXJwPrLiB/t70hSvwgVa249zNJVcwYN/5SGX5wLaJKnim\neY99xm75nr03O/RJK/DR8HvWysH7zxvrMWs0ppfwxkxrwOcg0Cb9xODVkg/wyClw\nLiHWlmH/eyC8nkiLYJKmV7566VWCV+gy+hC/DRstVVjIMG6LsqaPq6ycm7N8EV8s\nBb5uXIVHW6w5a20c40+W9G4EDYiQjdgEaf0FoMAWGDnOEaPsvjQk2/z5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDPDCCAiQCCQDxA75ydLHVoTANBgkqhkiG9w0BAQsFADBgMQswCQYDVQQGEwJS\nVTEPMA0GA1UECAwGTU9TQ09XMQ8wDQYDVQQHDAZNT1NDT1cxFTATBgNVBAoMDElO\nVEVSTUVESUFURTEYMBYGA1UEAwwPSU5URVJNRURJQVRFIENBMB4XDTIxMDczMDE1\nMTIyMloXDTI0MDUxOTE1MTIyMlowYDELMAkGA1UEBhMCUlUxDzANBgNVBAgMBk1P\nU0NPVzEPMA0GA1UEBwwGTU9TQ09XMRUwEwYDVQQKDAxJTlRFUk1FRElBVEUxGDAW\nBgNVBAMMD0lOVEVSTUVESUFURSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC\nAQoCggEBAKOrWdDVekCEf5r7KrQCCM007gTK2UeTk8Cr3Bp4XuBlQ8MyfzoBFpII\nSwMzqI3Cb5H8yTrVsDl6sIKYZLHPu/r8CD0z7d4lo20FM1yvKRS5I8azvKcf0MT7\nlK6flOg01b74EHrIzGfY0EVBSjognLtLLdTeUPwiUeFTicbWKC8tWVJbAAYcCfur\na5KDhUy0T5TFF9RCBue42AKLR70BaHPFDcdys7ybCSeekp4lwiJHrutSDrfGjlx1\nPt0y2IWZz4j99dXDG1wNCd7xj0WcM9uQWYxdyBf0oV5X88++hY1lEwE4XNPhwPJc\n8ed8Q+P5/BzfrBrmxsfU7y8mYphv1BsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA\ngOHvrh66+bQoG3Lo8bfp7D1Xvm/Md3gJq2nMotl2BH1TvNzMV93fCXygRX8J8rTL\n7xjUC2SbOrFDWFq2hNJQagdecAeuG+U55BY6Wi8SsHw+fhgxQyl9wtXWwotQPmsD\nuRhR1rL3vEphgPLbxNBzA7Lvj+P89Ar988Qy+o5AiUzHMUuqZbGOqs8UcKCQP7e/\nIX+zqqFwqyI8f90SVySGgs574jo8jQFy3l5fnp6yK0MPWg2cBCjpa5H1A+5DADF+\nnryV6Ie/m/wfxmitZZN+YCJu+8Bmmdl/FCwbmiH+HCLhrO8gonH3K21cQujMyFF5\nc7OFj86hvhqbr4kzz1J8lg==\n-----END CERTIFICATE-----" + expiration = "2025-12-28T19:14:44.213" +} diff --git a/examples/resources/edgecenter_securitygroup/import.sh b/examples/resources/edgecenter_securitygroup/import.sh new file mode 100644 index 00000000..9889ec13 --- /dev/null +++ b/examples/resources/edgecenter_securitygroup/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_securitygroup.securitygroup1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_securitygroup/resource.tf b/examples/resources/edgecenter_securitygroup/resource.tf new file mode 100644 index 00000000..66b85a8e --- /dev/null +++ b/examples/resources/edgecenter_securitygroup/resource.tf @@ -0,0 +1,31 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_securitygroup" "sg" { + name = "test sg" + region_id = 1 + project_id = 1 + + security_group_rules { + direction = "egress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 19990 + port_range_max = 19990 + } + + security_group_rules { + direction = "ingress" + ethertype = "IPv4" + protocol = "tcp" + port_range_min = 19990 + port_range_max = 19990 + } + + security_group_rules { + direction = "egress" + ethertype = "IPv4" + protocol = "vrrp" + } +} diff --git a/examples/resources/edgecenter_servergroup/import.sh b/examples/resources/edgecenter_servergroup/import.sh new file mode 100644 index 00000000..4f4c4446 --- /dev/null +++ b/examples/resources/edgecenter_servergroup/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_servergroup.servergroup1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_servergroup/resource.tf b/examples/resources/edgecenter_servergroup/resource.tf new file mode 100644 index 00000000..d2ba2251 --- /dev/null +++ b/examples/resources/edgecenter_servergroup/resource.tf @@ -0,0 +1,10 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_servergroup" "default" { + name = "default" + policy = "affinity" + region_id = 1 + project_id = 1 +} diff --git a/examples/resources/edgecenter_snapshot/import.sh b/examples/resources/edgecenter_snapshot/import.sh new file mode 100644 index 00000000..49b39669 --- /dev/null +++ b/examples/resources/edgecenter_snapshot/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_snapshot.snapshot1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_snapshot/resource.tf b/examples/resources/edgecenter_snapshot/resource.tf new file mode 100644 index 00000000..177a127d --- /dev/null +++ b/examples/resources/edgecenter_snapshot/resource.tf @@ -0,0 +1,16 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_snapshot" "snapshot" { + project_id = 1 + region_id = 1 + name = "snapshot example" + volume_id = "28e9edcb-1593-41fe-971b-da729c6ec301" + description = "snapshot example description" + metadata = { + env = "test" + } +} + + diff --git a/examples/resources/edgecenter_storage_s3/resource.tf b/examples/resources/edgecenter_storage_s3/resource.tf new file mode 100644 index 00000000..1f60a513 --- /dev/null +++ b/examples/resources/edgecenter_storage_s3/resource.tf @@ -0,0 +1,8 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_storage_s3" "example_s3" { + name = "example" + location = "s-ed1" +} diff --git a/examples/resources/edgecenter_storage_s3_bucket/resource.tf b/examples/resources/edgecenter_storage_s3_bucket/resource.tf new file mode 100644 index 00000000..1b02e463 --- /dev/null +++ b/examples/resources/edgecenter_storage_s3_bucket/resource.tf @@ -0,0 +1,8 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_storage_s3_bucket" "example_s3_bucket" { + name = "example1bucket2name" + storage_id = 1 +} diff --git a/examples/resources/edgecenter_storage_sftp/resource.tf b/examples/resources/edgecenter_storage_sftp/resource.tf new file mode 100644 index 00000000..1921c167 --- /dev/null +++ b/examples/resources/edgecenter_storage_sftp/resource.tf @@ -0,0 +1,9 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_storage_sftp" "example_sftp" { + name = "example" + location = "mia" + ssh_key_id = [199] +} diff --git a/examples/resources/edgecenter_storage_sftp_key/resource.tf b/examples/resources/edgecenter_storage_sftp_key/resource.tf new file mode 100644 index 00000000..2ef1afbe --- /dev/null +++ b/examples/resources/edgecenter_storage_sftp_key/resource.tf @@ -0,0 +1,8 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_storage_sftp_key" "terraform_test_key" { + name = "terraform_test_key" + key = "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAklOUpkDHrfHY17SbrmTIpNLTGK9Tjom/BWDSUGPl+nafzlHDTYW7hdI4yZ5ew18JH4JW9jbhUFrviQzM7xlELEVf4h9lFX5QVkbPppSwg0cda3Pbv7kOdJ/MTyBlWXFCR+HAo3FXRitBqxiX1nKhXpHAZsMciLq8V6RjsNAQwdsdMFvSlVK/7XAt3FaoJoAsncM1Q9x5+3V0Ww68/eIFmb1zuUFljQJKprrX88XypNDvjYNby6vw/Pb0rwert/EnmZ+AW4OZPnTPI89ZPmVMLuayrD2cE86Z/il8b+gw3r3+1nKatmIkjn2so1d01QraTlMqVSsbxNrRFi9wrf+M7Q== schacon@mylaptop.local" +} diff --git a/examples/resources/edgecenter_subnet/import.sh b/examples/resources/edgecenter_subnet/import.sh new file mode 100644 index 00000000..040bc793 --- /dev/null +++ b/examples/resources/edgecenter_subnet/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_subnet.subnet1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_subnet/resource.tf b/examples/resources/edgecenter_subnet/resource.tf new file mode 100644 index 00000000..fd88e396 --- /dev/null +++ b/examples/resources/edgecenter_subnet/resource.tf @@ -0,0 +1,31 @@ +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" +} + +resource "edgecenter_network" "network" { + name = "network_example" + mtu = 1450 + type = "vxlan" + region_id = 1 + project_id = 1 +} + +resource "edgecenter_subnet" "subnet" { + name = "subnet_example" + cidr = "192.168.10.0/24" + network_id = edgecenter_network.network.id + dns_nameservers = var.dns_nameservers + + dynamic "host_routes" { + iterator = hr + for_each = var.host_routes + content { + destination = hr.value.destination + nexthop = hr.value.nexthop + } + } + + gateway_ip = "192.168.10.1" + region_id = 1 + project_id = 1 +} \ No newline at end of file diff --git a/examples/resources/edgecenter_subnet/vars.tf b/examples/resources/edgecenter_subnet/vars.tf new file mode 100755 index 00000000..93e173e9 --- /dev/null +++ b/examples/resources/edgecenter_subnet/vars.tf @@ -0,0 +1,21 @@ +variable "dns_nameservers" { + type = list(any) + default = ["8.8.4.4", "1.1.1.1"] +} + +variable "host_routes" { + type = list(object({ + destination = string + nexthop = string + })) + default = [ + { + destination = "10.0.3.0/24" + nexthop = "10.0.0.13" + }, + { + destination = "10.0.4.0/24" + nexthop = "10.0.0.14" + }, + ] +} \ No newline at end of file diff --git a/examples/resources/edgecenter_volume/import.sh b/examples/resources/edgecenter_volume/import.sh new file mode 100644 index 00000000..0ce183fc --- /dev/null +++ b/examples/resources/edgecenter_volume/import.sh @@ -0,0 +1,2 @@ +# import using :: format +terraform import edgecenter_volume.volume1 1:6:447d2959-8ae0-4ca0-8d47-9f050a3637d7 \ No newline at end of file diff --git a/examples/resources/edgecenter_volume/resource.tf b/examples/resources/edgecenter_volume/resource.tf index 748ea94c..1a88bbfa 100644 --- a/examples/resources/edgecenter_volume/resource.tf +++ b/examples/resources/edgecenter_volume/resource.tf @@ -1,35 +1,14 @@ -# Example 1 -resource "edgecenter_volume" "volume1" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume" - size = 20 - source = "new-volume" - volume_type = "ssd_hiiops" - instance_id_to_attach_to = "00000000-0000-0000-0000-000000000000" - attachment_tag = "test-tag" - metadata = { - "key1" : "value1", - "key2" : "value2", - } -} - -# Example 2 -resource "edgecenter_volume" "volume_image" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume-image" - size = 20 - source = "image" - image_id = "00000000-0000-0000-0000-000000000000" +provider "edgecenter" { + permanent_api_token = "251$d3361.............1b35f26d8" } -# Example 3 -resource "edgecenter_volume" "volume_snapshot" { - region_id = var.region_id - project_id = var.project_id - name = "test-volume-snapshot" - size = 20 - source = "snapshot" - snapshot_id = "00000000-0000-0000-0000-000000000000" +resource "edgecenter_volume" "volume" { + name = "volume_example" + type_name = "standard" + size = 1 + region_id = 1 + project_id = 1 + metadata_map = { + tag1 = "tag1_value" + } } diff --git a/examples/resources/edgecenter_volume/variables.tf b/examples/resources/edgecenter_volume/variables.tf deleted file mode 100644 index 0b06dece..00000000 --- a/examples/resources/edgecenter_volume/variables.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "region_id" { - type = string -} - -variable "project_id" { - type = string -} diff --git a/go.mod b/go.mod index 5f86b5e0..456331a3 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,65 @@ module github.com/Edge-Center/terraform-provider-edgecenter -go 1.21 +go 1.20 require ( - github.com/Edge-Center/edgecentercloud-go v1.0.1 - github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0 + github.com/AlekSi/pointer v1.2.0 + github.com/Edge-Center/edgecenter-dns-sdk-go v0.1.0 + github.com/Edge-Center/edgecenter-storage-sdk-go v0.2.0 + github.com/Edge-Center/edgecentercdn-go v0.1.4 + github.com/Edge-Center/edgecentercloud-go v0.1.11 + github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0 github.com/mitchellh/mapstructure v1.5.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect - github.com/avast/retry-go/v4 v4.5.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/fatih/color v1.13.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/errors v0.20.4 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.26.0 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/strfmt v0.21.7 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/validate v0.22.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.12.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.3.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 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-retryablehttp v0.7.4 // 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.19.1 // indirect + github.com/hashicorp/hc-install v0.6.0 // indirect + github.com/hashicorp/hcl/v2 v2.18.0 // indirect github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-exec v0.19.0 // indirect + github.com/hashicorp/terraform-json v0.17.1 // indirect github.com/hashicorp/terraform-plugin-go v0.19.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 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/kr/pretty v0.3.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/ladydascalie/currency v1.6.0 // indirect + github.com/leodido/go-urn v1.2.2 // indirect + github.com/mailru/easyjson v0.7.7 // 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 @@ -39,16 +67,27 @@ require ( github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/sirupsen/logrus v1.8.2 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.14.1 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/text v0.14.0 // indirect + github.com/zclconf/go-cty v1.14.0 // indirect + go.mongodb.org/mongo-driver v1.11.3 // indirect + go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.14.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.15.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect - google.golang.org/grpc v1.57.1 // indirect + google.golang.org/grpc v1.57.0 // indirect google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 9a6b1ff1..6041bcde 100644 --- a/go.sum +++ b/go.sum @@ -1,79 +1,207 @@ -github.com/Edge-Center/edgecentercloud-go v1.0.1 h1:yPV+T6Z1dG3UjujjiGZFKJn1PC3slJeD+iwS+n2se5E= -github.com/Edge-Center/edgecentercloud-go v1.0.1/go.mod h1:zfzX+BWQ1yHMMsDerql6dSUD5bjPp4POg6B7ptr8YHQ= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= +github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Edge-Center/edgecenter-dns-sdk-go v0.1.0 h1:MDQr/60IhD1x7f5Bs20ljTQXGXFp0Uwx2+VaxtFRaDk= +github.com/Edge-Center/edgecenter-dns-sdk-go v0.1.0/go.mod h1:xWN2LYVokamADMRz1cPhOrYX/NlxiDJp9tjBumHU5G8= +github.com/Edge-Center/edgecenter-storage-sdk-go v0.2.0 h1:1aPDpywWbaF7VEjP/GjVoSgcipxWTTzEPPZv5kOWE8A= +github.com/Edge-Center/edgecenter-storage-sdk-go v0.2.0/go.mod h1:TcWO0BPvDsE6AGlPBqpKCZhoQ70rRlqmm85J32qcL8I= +github.com/Edge-Center/edgecentercdn-go v0.1.4 h1:Jt8f+CSriwVQ/KAb+a+v1dDNChtHjlpilgJOX8mOSx0= +github.com/Edge-Center/edgecentercdn-go v0.1.4/go.mod h1:RwEyxwPAmxor1mZKUTa2bIU2p5qM6kcAofUkaE4O1V4= +github.com/Edge-Center/edgecentercloud-go v0.1.11 h1:00h5o/71lEoSdU1B4AWmviuOfO28P6nsRP+afjIsW80= +github.com/Edge-Center/edgecentercloud-go v0.1.11/go.mod h1:kmXGtx0lL1ib+SPfJe/uIAyDHamquAvqiftoLSyhxF8= +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/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +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/avast/retry-go/v4 v4.5.1 h1:AxIx0HGi4VZ3I02jr78j5lZ3M6x1E0Ivxa6b0pUUh7o= -github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc= +github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +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-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= +github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= +github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg= +github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k= +github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg= +github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI= +github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= -github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= +github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= +github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= +github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs= +github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI= +github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28= +github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo= +github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk= +github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw= +github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg= +github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= +github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +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/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +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 v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 h1:Ud/6/AdmJ1R7ibdS0Wo5MWPj0T1R0fkpaD087bBaW8I= +github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637/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.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-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA= -github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +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/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= -github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= +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-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.30.0 h1:X7vB6vn5tON2b49ILa4W7mFAsndeqJ7bZFOGbVO+0Cc= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.30.0/go.mod h1:ydFcxbdj6klCqYEPkPvdvFKiNGKZLUs+896ODUXCyao= +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/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +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/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/ladydascalie/currency v1.6.0 h1:r5s/TMCYcpn6jPRHLV3F8nI7YjpY8trvstfuixxiHns= +github.com/ladydascalie/currency v1.6.0/go.mod h1:C9eil8e6tthhBb5yhwoH1U0LT5hm1BP/g+v/V82KYjY= +github.com/leodido/go-urn v1.2.2 h1:7z68G0FCGvDk646jz1AelTYNYWrTNm0bEcFAo147wt4= +github.com/leodido/go-urn v1.2.2/go.mod h1:kUaIbLZWttglzwNuG0pgsh5vuV6u2YcGBYz1hIPjtOQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= +github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= 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= @@ -88,22 +216,60 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ 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.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rwtodd/Go.Sed v0.0.0-20210816025313-55464686f9ef/go.mod h1:8AEUvGVi2uQ5b24BIhcr0GCcpd/RNAFWaN2CJFrWIIQ= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +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/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.8.2 h1:Na+MAUL+cI0P3CtS35fqYIYVL6uKkDYY7sptpCtHHlI= +github.com/sirupsen/logrus v1.8.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 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= @@ -111,45 +277,141 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9 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/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= -github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= +github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= +github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +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= +go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg= +go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng= +go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8= +go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= +go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190412183630-56d357773e84/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/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-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20220715151400-c0bba94af5f8/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.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/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-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +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.1 h1:upNTNqv0ES+2ZOOqACwVtS3Il8M12/+Hz41RCPzAjQg= -google.golang.org/grpc v1.57.1/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +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.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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/main.go b/main.go index fb3176e5..81b01699 100644 --- a/main.go +++ b/main.go @@ -16,9 +16,11 @@ func main() { flag.StringVar(&address, "address", "provider", "this value is used in the TF_REATTACH_PROVIDERS environment variable during debugging") flag.Parse() - plugin.Serve(&plugin.ServeOpts{ + opts := &plugin.ServeOpts{ Debug: debug, ProviderAddr: address, ProviderFunc: edgecenter.Provider, - }) + } + + plugin.Serve(opts) } diff --git a/scripts/errcheck.sh b/scripts/errcheck.sh new file mode 100755 index 00000000..e80c04d2 --- /dev/null +++ b/scripts/errcheck.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Check gofmt +echo "==> Checking for unchecked errors..." + +if ! which errcheck > /dev/null; then + echo "==> Installing errcheck..." + go get -u github.com/kisielk/errcheck +fi + +err_files=$(errcheck -ignoretests \ + -ignore 'github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema:Set' \ + -ignore 'bytes:.*' \ + -ignore 'io:Close|Write' \ + $(go list -f '{{.Dir}}' ./...| grep -v /vendor/)) + +if [[ -n ${err_files} ]]; then + echo 'Unchecked errors found in the following places:' + echo "${err_files}" + echo "Please handle returned errors. You can check directly with \`make errcheck\`" + exit 1 +fi + +exit 0 diff --git a/scripts/gofmtcheck.sh b/scripts/gofmtcheck.sh deleted file mode 100755 index 1c055815..00000000 --- a/scripts/gofmtcheck.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -# Check gofmt -echo "==> Checking that code complies with gofmt requirements..." -gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`) -if [[ -n ${gofmt_files} ]]; then - echo 'gofmt needs running on the following files:' - echo "${gofmt_files}" - echo "You can use the command: \`make fmt\` to reformat code." - exit 1 -fi - -exit 0 diff --git a/terraform-registry-manifest.json b/terraform-registry-manifest.json new file mode 100644 index 00000000..625ab562 --- /dev/null +++ b/terraform-registry-manifest.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "metadata": { + "protocol_versions": ["5.0"] + } +} \ No newline at end of file