forked from mirrors/terraform-provider-binarylane
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 093c2e022a | |||
| a9c133e73c | |||
| f659cfcd25 | |||
| 3ed14bdae3 | |||
| 2e9f10dc46 | |||
| 0876e38dea | |||
| 00b6fae313 | |||
| f42c218d98 | |||
| d2451eb650 |
@@ -38,7 +38,7 @@ jobs:
|
|||||||
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
passphrase: ${{ secrets.PASSPHRASE }}
|
passphrase: ${{ secrets.PASSPHRASE }}
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
|
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
|
||||||
with:
|
with:
|
||||||
args: release --clean
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ tmp
|
|||||||
# Normally, you wouldn't want to exclude lock files, but our examples are tested
|
# Normally, you wouldn't want to exclude lock files, but our examples are tested
|
||||||
# with a locally built provider and won't be useful to others.
|
# with a locally built provider and won't be useful to others.
|
||||||
.terraform.lock.hcl
|
.terraform.lock.hcl
|
||||||
|
*.zip
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
exclude: ^docs/
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
|
||||||
|
- repo: https://github.com/dnephin/pre-commit-golang
|
||||||
|
rev: v0.5.1
|
||||||
|
hooks:
|
||||||
|
- id: go-fmt
|
||||||
|
- id: go-vet
|
||||||
|
exclude: ^tools/
|
||||||
|
- id: go-mod-tidy
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||||
|
environment:
|
||||||
|
GOCACHE_S3_BUCKET: gocache
|
||||||
|
GOCACHE_S3_REGION: us-east-1
|
||||||
|
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||||
|
GOCACHE_S3_PATH_STYLE: "true"
|
||||||
|
GOCACHE_KEY_PREFIX: ci-terraform-provider-binarylane
|
||||||
|
GOCACHE_METRICS: "true"
|
||||||
|
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||||
|
commands:
|
||||||
|
- make build
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 1
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 2
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: pre-commit
|
||||||
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||||
|
environment:
|
||||||
|
GOCACHE_S3_BUCKET: gocache
|
||||||
|
GOCACHE_S3_REGION: us-east-1
|
||||||
|
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||||
|
GOCACHE_S3_PATH_STYLE: "true"
|
||||||
|
GOCACHE_KEY_PREFIX: ci-terraform-provider-binarylane
|
||||||
|
GOCACHE_METRICS: "true"
|
||||||
|
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||||
|
commands:
|
||||||
|
- uvx pre-commit run --all-files
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 1
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 2
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: package
|
||||||
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||||
|
environment:
|
||||||
|
GOCACHE_S3_BUCKET: gocache
|
||||||
|
GOCACHE_S3_REGION: us-east-1
|
||||||
|
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||||
|
GOCACHE_S3_PATH_STYLE: "true"
|
||||||
|
GOCACHE_KEY_PREFIX: ci-terraform-provider-binarylane
|
||||||
|
GOCACHE_METRICS: "true"
|
||||||
|
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||||
|
commands:
|
||||||
|
- make package VERSION=${CI_COMMIT_TAG}
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 1
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 2
|
||||||
|
|
||||||
|
- name: upload
|
||||||
|
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
VERSION=$$(echo ${CI_COMMIT_TAG} | sed 's/^v//')
|
||||||
|
FILE="terraform-provider-binarylane_$${VERSION}_linux_amd64.zip"
|
||||||
|
curl -f -X PUT \
|
||||||
|
"https://artifactapi.k8s.syd1.au.unkin.net/api/v2/remotes/terraform-unkin/files/unkin/binarylane/$${FILE}" \
|
||||||
|
-H "Content-Type: application/zip" \
|
||||||
|
--data-binary @"$${FILE}"
|
||||||
|
depends_on: [package]
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 128Mi
|
||||||
|
cpu: 100m
|
||||||
|
limits:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 500m
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: lint
|
||||||
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||||
|
environment:
|
||||||
|
GOCACHE_S3_BUCKET: gocache
|
||||||
|
GOCACHE_S3_REGION: us-east-1
|
||||||
|
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||||
|
GOCACHE_S3_PATH_STYLE: "true"
|
||||||
|
GOCACHE_KEY_PREFIX: ci-terraform-provider-binarylane
|
||||||
|
GOCACHE_METRICS: "true"
|
||||||
|
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||||
|
commands:
|
||||||
|
- make lint
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 1
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 2
|
||||||
|
|
||||||
|
- name: test
|
||||||
|
image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/gobuilder:0.1.2-alma9
|
||||||
|
environment:
|
||||||
|
GOCACHE_S3_BUCKET: gocache
|
||||||
|
GOCACHE_S3_REGION: us-east-1
|
||||||
|
GOCACHE_S3_ENDPOINT_URL: https://s3.ceph.unkin.net
|
||||||
|
GOCACHE_S3_PATH_STYLE: "true"
|
||||||
|
GOCACHE_KEY_PREFIX: ci-terraform-provider-binarylane
|
||||||
|
GOCACHE_METRICS: "true"
|
||||||
|
GOCACHEPROG: go-cache-plugin --cache-dir=/tmp/gocache
|
||||||
|
AWS_ACCESS_KEY_ID:
|
||||||
|
from_secret: GOCACHE_AWS_ACCESS_KEY_ID
|
||||||
|
AWS_SECRET_ACCESS_KEY:
|
||||||
|
from_secret: GOCACHE_AWS_SECRET_ACCESS_KEY
|
||||||
|
commands:
|
||||||
|
- make test
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
serviceAccountName: default
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 512Mi
|
||||||
|
cpu: 1
|
||||||
|
limits:
|
||||||
|
memory: 2Gi
|
||||||
|
cpu: 2
|
||||||
+1
-1
@@ -14,7 +14,7 @@ Based on [this example from the terraform docs](https://developer.hashicorp.com/
|
|||||||
provider_installation {
|
provider_installation {
|
||||||
dev_overrides {
|
dev_overrides {
|
||||||
# Example GOBIN path, will need to be replaced with your own GOBIN path. Default is $GOPATH/bin
|
# Example GOBIN path, will need to be replaced with your own GOBIN path. Default is $GOPATH/bin
|
||||||
"oscarhermoso/binarylane" = "/home/oscarhermoso/Git/terraform-provider-binarylane/bin"
|
"git.unkin.net/unkin/binarylane" = "/path/to/terraform-provider-binarylane/bin"
|
||||||
}
|
}
|
||||||
|
|
||||||
direct {}
|
direct {}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
.PHONY: build install test lint fmt clean tidy package pre-commit patch minor major check-go
|
||||||
|
|
||||||
|
BINARY := terraform-provider-binarylane
|
||||||
|
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev")
|
||||||
|
OS_ARCH := linux_amd64
|
||||||
|
INSTALL_VERSION := $(shell echo $(VERSION) | sed 's/^v//')
|
||||||
|
INSTALL_DIR := ~/.terraform.d/plugins/git.unkin.net/unkin/binarylane/$(INSTALL_VERSION)/$(OS_ARCH)
|
||||||
|
ZIP := $(BINARY)_$(INSTALL_VERSION)_$(OS_ARCH).zip
|
||||||
|
|
||||||
|
GO_VERSION_REQUIRED := 1.25
|
||||||
|
GO_VERSION_ACTUAL := $(shell go version | sed 's/go version go\([0-9]*\.[0-9]*\).*/\1/')
|
||||||
|
|
||||||
|
check-go:
|
||||||
|
@if [ "$$(printf '%s\n%s' "$(GO_VERSION_REQUIRED)" "$(GO_VERSION_ACTUAL)" | sort -V | head -1)" != "$(GO_VERSION_REQUIRED)" ]; then \
|
||||||
|
echo "ERROR: Go >= $(GO_VERSION_REQUIRED) required, found $(GO_VERSION_ACTUAL)"; exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
build: check-go tidy
|
||||||
|
go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BINARY)
|
||||||
|
|
||||||
|
install: build
|
||||||
|
mkdir -p $(INSTALL_DIR)
|
||||||
|
cp $(BINARY) $(INSTALL_DIR)/
|
||||||
|
|
||||||
|
test: check-go
|
||||||
|
go test -race -count=1 ./...
|
||||||
|
|
||||||
|
lint: check-go
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
fmt: check-go
|
||||||
|
gofmt -w .
|
||||||
|
|
||||||
|
package: build
|
||||||
|
cp $(BINARY) $(BINARY)_v$(INSTALL_VERSION)
|
||||||
|
python3 -c "import zipfile,sys; z=zipfile.ZipFile(sys.argv[1],'w',zipfile.ZIP_DEFLATED); z.write(sys.argv[2]); z.close()" $(ZIP) $(BINARY)_v$(INSTALL_VERSION)
|
||||||
|
rm $(BINARY)_v$(INSTALL_VERSION)
|
||||||
|
|
||||||
|
pre-commit:
|
||||||
|
uvx pre-commit run --all-files
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY) *.zip
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
|
||||||
|
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
|
||||||
|
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
|
||||||
|
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
|
||||||
|
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
|
||||||
|
|
||||||
|
patch:
|
||||||
|
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
|
||||||
|
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||||
|
|
||||||
|
minor:
|
||||||
|
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
|
||||||
|
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||||
|
|
||||||
|
major:
|
||||||
|
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
|
||||||
|
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||||
+1
-1
@@ -15,7 +15,7 @@ description: |-
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||||
|
page_title: "binarylane_server_reverse_name Resource - terraform-provider-binarylane"
|
||||||
|
subcategory: ""
|
||||||
|
description: |-
|
||||||
|
Manage the reverse name (PTR record) for a public IPv4 address of a BinaryLane server. Destroying this resource clears the custom reverse name, restoring the BinaryLane default.
|
||||||
|
---
|
||||||
|
|
||||||
|
# binarylane_server_reverse_name (Resource)
|
||||||
|
|
||||||
|
Manage the reverse name (PTR record) for a public IPv4 address of a BinaryLane server. Destroying this resource clears the custom reverse name, restoring the BinaryLane default.
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```terraform
|
||||||
|
resource "binarylane_server" "example" {
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "binarylane_server_reverse_name" "example" {
|
||||||
|
server_id = binarylane_server.example.id
|
||||||
|
ipv4_address = binarylane_server.example.public_ipv4_addresses.0
|
||||||
|
reverse_name = "mx1.example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- schema generated by tfplugindocs -->
|
||||||
|
## Schema
|
||||||
|
|
||||||
|
### Required
|
||||||
|
|
||||||
|
- `ipv4_address` (String) The public IPv4 address to set the reverse name for.
|
||||||
|
- `reverse_name` (String) The reverse name (PTR record) for the IPv4 address.
|
||||||
|
- `server_id` (Number) The ID of the server the IPv4 address is assigned to.
|
||||||
|
|
||||||
|
### Read-Only
|
||||||
|
|
||||||
|
- `id` (String) The ID of this resource, in the format `<server_id>:<ipv4_address>`.
|
||||||
|
|
||||||
|
## Import
|
||||||
|
|
||||||
|
Import is supported using the following syntax:
|
||||||
|
|
||||||
|
The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
terraform import binarylane_server_reverse_name.example "<server id>:<ipv4 address>"
|
||||||
|
```
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ If you want to deploy multiple parts of a cloud-init configuration, you can use
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
terraform import binarylane_server_reverse_name.example "<server id>:<ipv4 address>"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
resource "binarylane_server" "example" {
|
||||||
|
# ...
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "binarylane_server_reverse_name" "example" {
|
||||||
|
server_id = binarylane_server.example.id
|
||||||
|
ipv4_address = binarylane_server.example.public_ipv4_addresses.0
|
||||||
|
reverse_name = "mx1.example.com"
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
terraform {
|
terraform {
|
||||||
required_providers {
|
required_providers {
|
||||||
binarylane = {
|
binarylane = {
|
||||||
source = "oscarhermoso/binarylane"
|
source = "git.unkin.net/unkin/binarylane"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ func (p *binarylaneProvider) Resources(_ context.Context) []func() resource.Reso
|
|||||||
return []func() resource.Resource{
|
return []func() resource.Resource{
|
||||||
NewServerResource,
|
NewServerResource,
|
||||||
NewServerFirewallRulesResource,
|
NewServerFirewallRulesResource,
|
||||||
|
NewServerReverseNameResource,
|
||||||
NewSshKeyResource,
|
NewSshKeyResource,
|
||||||
NewVpcResource,
|
NewVpcResource,
|
||||||
NewVpcRouteEntriesResource,
|
NewVpcRouteEntriesResource,
|
||||||
|
|||||||
@@ -121,6 +121,11 @@ func (r *serverFirewallRulesResource) Create(ctx context.Context, req resource.C
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resolveUnknownDestinationPorts(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Save data into Terraform state
|
// Save data into Terraform state
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
}
|
}
|
||||||
@@ -207,6 +212,11 @@ func (r *serverFirewallRulesResource) Update(ctx context.Context, req resource.U
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resolveUnknownDestinationPorts(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Save updated data into Terraform state
|
// Save updated data into Terraform state
|
||||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
}
|
}
|
||||||
@@ -245,6 +255,22 @@ func (r *serverFirewallRulesResource) Delete(ctx context.Context, req resource.D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Omitted destination_ports (all ports) are planned as unknown because the attribute is computed.
|
||||||
|
func resolveUnknownDestinationPorts(ctx context.Context, data *serverFirewallRulesResourceModel) diag.Diagnostics {
|
||||||
|
var rules []resources.FirewallRulesValue
|
||||||
|
diags := data.FirewallRules.ElementsAs(ctx, &rules, false)
|
||||||
|
if diags.HasError() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
for i := range rules {
|
||||||
|
if rules[i].DestinationPorts.IsUnknown() {
|
||||||
|
rules[i].DestinationPorts = types.ListNull(types.StringType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.FirewallRules, diags = types.ListValueFrom(ctx, resources.FirewallRulesValue{}.Type(ctx), rules)
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
func GetFirewallRulesState(ctx context.Context, firewallRules *[]binarylane.AdvancedFirewallRule) (basetypes.ListValue, diag.Diagnostics) {
|
func GetFirewallRulesState(ctx context.Context, firewallRules *[]binarylane.AdvancedFirewallRule) (basetypes.ListValue, diag.Diagnostics) {
|
||||||
var diags diag.Diagnostics
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
@@ -258,7 +284,7 @@ func GetFirewallRulesState(ctx context.Context, firewallRules *[]binarylane.Adva
|
|||||||
destinationAddresses, diag := types.ListValueFrom(ctx, types.StringType, rule.DestinationAddresses)
|
destinationAddresses, diag := types.ListValueFrom(ctx, types.StringType, rule.DestinationAddresses)
|
||||||
diags.Append(diag...)
|
diags.Append(diag...)
|
||||||
|
|
||||||
var destinationPorts basetypes.ListValue
|
destinationPorts := types.ListNull(types.StringType)
|
||||||
if rule.DestinationPorts != nil {
|
if rule.DestinationPorts != nil {
|
||||||
destinationPorts, diag = types.ListValueFrom(ctx, types.StringType, rule.DestinationPorts)
|
destinationPorts, diag = types.ListValueFrom(ctx, types.StringType, rule.DestinationPorts)
|
||||||
diags.Append(diag...)
|
diags.Append(diag...)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"terraform-provider-binarylane/internal/binarylane"
|
||||||
|
"terraform-provider-binarylane/internal/resources"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerFirewallRulesResourceOmittedPorts(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
_, _ = w.Write([]byte(`{"firewall_rules":[{"action":"accept","protocol":"icmp","source_addresses":["10.0.0.0/8"],"destination_addresses":["10.0.0.1"],"destination_ports":null}]}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client, err := binarylane.NewClientWithConfig(srv.URL, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := &serverFirewallRulesResource{bc: &BinarylaneClient{client: client}}
|
||||||
|
|
||||||
|
schemaResp := resource.SchemaResponse{}
|
||||||
|
r.Schema(ctx, resource.SchemaRequest{}, &schemaResp)
|
||||||
|
s := schemaResp.Schema
|
||||||
|
emptyState := func() tfsdk.State {
|
||||||
|
return tfsdk.State{Schema: s, Raw: tftypes.NewValue(s.Type().TerraformType(ctx), nil)}
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleType := resources.FirewallRulesValue{}
|
||||||
|
rule := resources.NewFirewallRulesValueMust(ruleType.AttributeTypes(ctx), map[string]attr.Value{
|
||||||
|
"description": types.StringNull(),
|
||||||
|
"action": types.StringValue("accept"),
|
||||||
|
"protocol": types.StringValue("icmp"),
|
||||||
|
"destination_addresses": types.ListValueMust(types.StringType, []attr.Value{types.StringValue("10.0.0.1")}),
|
||||||
|
"destination_ports": types.ListUnknown(types.StringType),
|
||||||
|
"source_addresses": types.ListValueMust(types.StringType, []attr.Value{types.StringValue("10.0.0.0/8")}),
|
||||||
|
})
|
||||||
|
model := serverFirewallRulesResourceModel{}
|
||||||
|
model.ServerId = types.Int64Value(1)
|
||||||
|
model.FirewallRules = types.ListValueMust(ruleType.Type(ctx), []attr.Value{rule})
|
||||||
|
|
||||||
|
plan := tfsdk.Plan{Schema: s, Raw: tftypes.NewValue(s.Type().TerraformType(ctx), nil)}
|
||||||
|
if diags := plan.Set(ctx, &model); diags.HasError() {
|
||||||
|
t.Fatal(diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
createResp := resource.CreateResponse{State: emptyState()}
|
||||||
|
r.Create(ctx, resource.CreateRequest{Plan: plan}, &createResp)
|
||||||
|
if createResp.Diagnostics.HasError() {
|
||||||
|
t.Fatal(createResp.Diagnostics)
|
||||||
|
}
|
||||||
|
var ports types.List
|
||||||
|
createResp.State.GetAttribute(ctx, path.Root("firewall_rules").AtListIndex(0).AtName("destination_ports"), &ports)
|
||||||
|
if !ports.IsNull() {
|
||||||
|
t.Fatalf("create: expected null destination_ports, got %s", ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateResp := resource.UpdateResponse{State: emptyState()}
|
||||||
|
r.Update(ctx, resource.UpdateRequest{Plan: plan}, &updateResp)
|
||||||
|
if updateResp.Diagnostics.HasError() {
|
||||||
|
t.Fatal(updateResp.Diagnostics)
|
||||||
|
}
|
||||||
|
updateResp.State.GetAttribute(ctx, path.Root("firewall_rules").AtListIndex(0).AtName("destination_ports"), &ports)
|
||||||
|
if !ports.IsNull() {
|
||||||
|
t.Fatalf("update: expected null destination_ports, got %s", ports)
|
||||||
|
}
|
||||||
|
|
||||||
|
readResp := resource.ReadResponse{State: createResp.State}
|
||||||
|
r.Read(ctx, resource.ReadRequest{State: createResp.State}, &readResp)
|
||||||
|
if readResp.Diagnostics.HasError() {
|
||||||
|
t.Fatal(readResp.Diagnostics)
|
||||||
|
}
|
||||||
|
readResp.State.GetAttribute(ctx, path.Root("firewall_rules").AtListIndex(0).AtName("destination_ports"), &ports)
|
||||||
|
if !ports.IsNull() {
|
||||||
|
t.Fatalf("read: expected null destination_ports, got %s", ports)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1361,28 +1361,36 @@ func (r *serverResource) ImportState(
|
|||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
serverActionPollInterval = 5 * time.Second
|
||||||
|
serverActionTimeout = 20 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
func (r *serverResource) waitForServerAction(ctx context.Context, serverId int64, actionId int64) error {
|
func (r *serverResource) waitForServerAction(ctx context.Context, serverId int64, actionId int64) error {
|
||||||
|
if _, ok := ctx.Deadline(); !ok {
|
||||||
|
var cancel context.CancelFunc
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, serverActionTimeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
var lastReadyResp *binarylane.GetServersServerIdActionsActionIdResponse
|
var lastReadyResp *binarylane.GetServersServerIdActionsActionIdResponse
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
readyResp, err := r.bc.client.GetServersServerIdActionsActionIdWithResponse(ctx, serverId, actionId)
|
||||||
case <-ctx.Done():
|
if err != nil && ctx.Err() == nil {
|
||||||
if lastReadyResp == nil {
|
return fmt.Errorf("unexpected error waiting for server action: server_id=%d, action_id=%d, error: %w", serverId, actionId, err)
|
||||||
return fmt.Errorf("timed out waiting for server action: server_id=%d, action_id=%d", serverId, actionId)
|
}
|
||||||
} else {
|
if err == nil {
|
||||||
return fmt.Errorf("timed out waiting for server action: server_id=%d, action_id=%d, last response was status=%s, body: %s",
|
if readyResp.StatusCode() == http.StatusNotFound {
|
||||||
serverId, actionId, lastReadyResp.Status(), lastReadyResp.Body)
|
return fmt.Errorf("server action not found: server_id=%d, action_id=%d, body: %s", serverId, actionId, readyResp.Body)
|
||||||
}
|
}
|
||||||
default:
|
if readyResp.StatusCode() == http.StatusOK {
|
||||||
readyResp, err := r.bc.client.GetServersServerIdActionsActionIdWithResponse(ctx, serverId, actionId)
|
switch {
|
||||||
if err != nil {
|
case readyResp.JSON200.Action.Status == binarylane.Errored:
|
||||||
return fmt.Errorf("unexpected error waiting for server action: server_id=%d, action_id=%d, error: %w", serverId, actionId, err)
|
return fmt.Errorf("server action failed to with error: server_id=%d, action_id=%d, error: %s", serverId, actionId, readyResp.Body)
|
||||||
}
|
case readyResp.JSON200.Action.Status == binarylane.Completed, readyResp.JSON200.Action.CompletedAt != nil:
|
||||||
if readyResp.StatusCode() == http.StatusOK && readyResp.JSON200.Action.Status == binarylane.Errored {
|
return nil
|
||||||
return fmt.Errorf("server action failed to with error: server_id=%d, action_id=%d, error: %s", serverId, actionId, readyResp.Body)
|
}
|
||||||
}
|
|
||||||
if readyResp.StatusCode() == http.StatusOK && readyResp.JSON200.Action.CompletedAt != nil {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
lastReadyResp = readyResp
|
lastReadyResp = readyResp
|
||||||
tflog.Debug(ctx,
|
tflog.Debug(ctx,
|
||||||
@@ -1391,7 +1399,16 @@ func (r *serverResource) waitForServerAction(ctx context.Context, serverId int64
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second * 5)
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
if lastReadyResp == nil {
|
||||||
|
return fmt.Errorf("timed out waiting for server action: server_id=%d, action_id=%d", serverId, actionId)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("timed out waiting for server action: server_id=%d, action_id=%d, last response was status=%s, body: %s",
|
||||||
|
serverId, actionId, lastReadyResp.Status(), lastReadyResp.Body)
|
||||||
|
case <-time.After(serverActionPollInterval):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"terraform-provider-binarylane/internal/binarylane"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ resource.Resource = &serverReverseNameResource{}
|
||||||
|
_ resource.ResourceWithConfigure = &serverReverseNameResource{}
|
||||||
|
_ resource.ResourceWithImportState = &serverReverseNameResource{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewServerReverseNameResource() resource.Resource {
|
||||||
|
return &serverReverseNameResource{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverReverseNameResource struct {
|
||||||
|
bc *BinarylaneClient
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverReverseNameResourceModel struct {
|
||||||
|
Id types.String `tfsdk:"id"`
|
||||||
|
ServerId types.Int64 `tfsdk:"server_id"`
|
||||||
|
Ipv4Address types.String `tfsdk:"ipv4_address"`
|
||||||
|
ReverseName types.String `tfsdk:"reverse_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||||
|
if req.ProviderData == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bc, ok := req.ProviderData.(BinarylaneClient)
|
||||||
|
if !ok {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected Resource Configure Type",
|
||||||
|
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.bc = &bc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||||
|
resp.TypeName = req.ProviderTypeName + "_server_reverse_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||||
|
resp.Schema = schema.Schema{
|
||||||
|
Description: "Manage the reverse name (PTR record) for a public IPv4 address of a BinaryLane server. " +
|
||||||
|
"Destroying this resource clears the custom reverse name, restoring the BinaryLane default.",
|
||||||
|
Attributes: map[string]schema.Attribute{
|
||||||
|
"id": schema.StringAttribute{
|
||||||
|
Description: "The ID of this resource, in the format `<server_id>:<ipv4_address>`.",
|
||||||
|
Computed: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.UseStateForUnknown(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"server_id": schema.Int64Attribute{
|
||||||
|
Description: "The ID of the server the IPv4 address is assigned to.",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.Int64{
|
||||||
|
int64validator.AtLeast(1),
|
||||||
|
},
|
||||||
|
PlanModifiers: []planmodifier.Int64{
|
||||||
|
int64planmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"ipv4_address": schema.StringAttribute{
|
||||||
|
Description: "The public IPv4 address to set the reverse name for.",
|
||||||
|
Required: true,
|
||||||
|
PlanModifiers: []planmodifier.String{
|
||||||
|
stringplanmodifier.RequiresReplace(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"reverse_name": schema.StringAttribute{
|
||||||
|
Description: "The reverse name (PTR record) for the IPv4 address.",
|
||||||
|
Required: true,
|
||||||
|
Validators: []validator.String{
|
||||||
|
stringvalidator.LengthAtLeast(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||||
|
var data serverReverseNameResourceModel
|
||||||
|
|
||||||
|
// Read Terraform plan data into the model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create API call logic
|
||||||
|
tflog.Debug(ctx, fmt.Sprintf("Creating server reverse name: server_id=%d, ipv4_address=%s",
|
||||||
|
data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
err := r.changeReverseName(ctx, data.ServerId.ValueInt64(), data.Ipv4Address.ValueString(), data.ReverseName.ValueStringPointer())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
fmt.Sprintf("Error creating server reverse name: server_id=%d, ipv4_address=%s", data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()),
|
||||||
|
err.Error(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Id = types.StringValue(formatServerReverseNameId(data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
|
||||||
|
// Save data into Terraform state
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||||
|
var data serverReverseNameResourceModel
|
||||||
|
|
||||||
|
// Read Terraform prior state data into the model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read API call logic
|
||||||
|
tflog.Debug(ctx, fmt.Sprintf("Reading server reverse name: server_id=%d, ipv4_address=%s",
|
||||||
|
data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
serverResp, err := r.bc.client.GetServersServerIdWithResponse(ctx, data.ServerId.ValueInt64())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
fmt.Sprintf("Error reading server reverse name: server_id=%d", data.ServerId.ValueInt64()),
|
||||||
|
err.Error(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if serverResp.StatusCode() == http.StatusNotFound {
|
||||||
|
tflog.Warn(ctx, fmt.Sprintf("Server not found, removing reverse name from state: server_id=%d", data.ServerId.ValueInt64()))
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if serverResp.StatusCode() != http.StatusOK {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Unexpected HTTP status code reading server reverse name",
|
||||||
|
fmt.Sprintf("Received %s reading server reverse name: server_id=%d. Details: %s", serverResp.Status(), data.ServerId.ValueInt64(), serverResp.Body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var network *binarylane.Network
|
||||||
|
for i, n := range serverResp.JSON200.Server.Networks.V4 {
|
||||||
|
if n.IpAddress == data.Ipv4Address.ValueString() {
|
||||||
|
network = &serverResp.JSON200.Server.Networks.V4[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if network == nil {
|
||||||
|
tflog.Warn(ctx, fmt.Sprintf("IPv4 address not found on server, removing reverse name from state: server_id=%d, ipv4_address=%s",
|
||||||
|
data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
resp.State.RemoveResource(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Id = types.StringValue(formatServerReverseNameId(data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
data.ReverseName = types.StringPointerValue(network.ReverseName)
|
||||||
|
|
||||||
|
// Save into Terraform state
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||||
|
var data serverReverseNameResourceModel
|
||||||
|
|
||||||
|
// Read Terraform plan data into the model
|
||||||
|
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update API call logic
|
||||||
|
tflog.Debug(ctx, fmt.Sprintf("Updating server reverse name: server_id=%d, ipv4_address=%s",
|
||||||
|
data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
err := r.changeReverseName(ctx, data.ServerId.ValueInt64(), data.Ipv4Address.ValueString(), data.ReverseName.ValueStringPointer())
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
fmt.Sprintf("Error updating server reverse name: server_id=%d, ipv4_address=%s", data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()),
|
||||||
|
err.Error(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save updated data into Terraform state
|
||||||
|
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||||
|
var data serverReverseNameResourceModel
|
||||||
|
|
||||||
|
// Read Terraform prior state data into the model
|
||||||
|
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||||
|
if resp.Diagnostics.HasError() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete API call logic: a null reverse name clears the custom reverse name
|
||||||
|
tflog.Debug(ctx, fmt.Sprintf("Deleting server reverse name: server_id=%d, ipv4_address=%s",
|
||||||
|
data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()))
|
||||||
|
err := r.changeReverseName(ctx, data.ServerId.ValueInt64(), data.Ipv4Address.ValueString(), nil)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
fmt.Sprintf("Error deleting server reverse name: server_id=%d, ipv4_address=%s", data.ServerId.ValueInt64(), data.Ipv4Address.ValueString()),
|
||||||
|
err.Error(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) ImportState(
|
||||||
|
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
|
||||||
|
) {
|
||||||
|
serverId, ipv4Address, err := parseServerReverseNameId(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
resp.Diagnostics.AddError(
|
||||||
|
"Error importing server reverse name",
|
||||||
|
"Could not import server reverse name, unexpected error (ID should be <server_id>:<ipv4_address>): "+err.Error(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
|
||||||
|
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("ipv4_address"), ipv4Address)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) changeReverseName(ctx context.Context, serverId int64, ipv4Address string, reverseName *string) error {
|
||||||
|
actionResp, err := r.bc.client.PostServersServerIdActionsChangeReverseNameWithResponse(
|
||||||
|
ctx,
|
||||||
|
serverId,
|
||||||
|
binarylane.PostServersServerIdActionsChangeReverseNameJSONRequestBody{
|
||||||
|
Type: "change_reverse_name",
|
||||||
|
Ipv4Address: ipv4Address,
|
||||||
|
ReverseName: reverseName,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error changing reverse name for server: server_id=%d, error: %w", serverId, err)
|
||||||
|
}
|
||||||
|
if actionResp.StatusCode() != http.StatusOK && actionResp.StatusCode() != http.StatusAccepted {
|
||||||
|
return fmt.Errorf("unexpected HTTP status code changing reverse name for server: server_id=%d, details: %s", serverId, actionResp.Body)
|
||||||
|
}
|
||||||
|
// The change_reverse_name action can stay unresolvable, so wait for the result on the server instead
|
||||||
|
if reverseName == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.waitForReverseName(ctx, serverId, ipv4Address, *reverseName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *serverReverseNameResource) waitForReverseName(ctx context.Context, serverId int64, ipv4Address string, want string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, serverActionTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var last string
|
||||||
|
for {
|
||||||
|
serverResp, err := r.bc.client.GetServersServerIdWithResponse(ctx, serverId)
|
||||||
|
if err == nil && serverResp.StatusCode() == http.StatusOK {
|
||||||
|
for _, n := range serverResp.JSON200.Server.Networks.V4 {
|
||||||
|
if n.IpAddress != ipv4Address {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n.ReverseName != nil {
|
||||||
|
if *n.ReverseName == want {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
last = *n.ReverseName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("timed out waiting for reverse name: server_id=%d, ipv4_address=%s, want=%q, last=%q", serverId, ipv4Address, want, last)
|
||||||
|
case <-time.After(serverActionPollInterval):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatServerReverseNameId(serverId int64, ipv4Address string) string {
|
||||||
|
return fmt.Sprintf("%d:%s", serverId, ipv4Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseServerReverseNameId(id string) (int64, string, error) {
|
||||||
|
serverIdStr, ipv4Address, ok := strings.Cut(id, ":")
|
||||||
|
if !ok {
|
||||||
|
return 0, "", fmt.Errorf("missing ':' separator in %q", id)
|
||||||
|
}
|
||||||
|
serverId, err := strconv.ParseInt(serverIdStr, 10, 64)
|
||||||
|
if err != nil || serverId < 1 {
|
||||||
|
return 0, "", fmt.Errorf("invalid server_id %q", serverIdStr)
|
||||||
|
}
|
||||||
|
addr, err := netip.ParseAddr(ipv4Address)
|
||||||
|
if err != nil || !addr.Is4() {
|
||||||
|
return 0, "", fmt.Errorf("invalid ipv4_address %q", ipv4Address)
|
||||||
|
}
|
||||||
|
return serverId, ipv4Address, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"terraform-provider-binarylane/internal/binarylane"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-testing/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseServerReverseNameId(t *testing.T) {
|
||||||
|
serverId, ip, err := parseServerReverseNameId(formatServerReverseNameId(12345, "203.0.113.7"))
|
||||||
|
if err != nil || serverId != 12345 || ip != "203.0.113.7" {
|
||||||
|
t.Fatalf("round trip: got (%d, %q, %v)", serverId, ip, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range []string{
|
||||||
|
"",
|
||||||
|
"12345",
|
||||||
|
"12345:",
|
||||||
|
":203.0.113.7",
|
||||||
|
"abc:203.0.113.7",
|
||||||
|
"0:203.0.113.7",
|
||||||
|
"-1:203.0.113.7",
|
||||||
|
"12345:not-an-ip",
|
||||||
|
"12345:2001:db8::1",
|
||||||
|
"12345:203.0.113.7:80",
|
||||||
|
} {
|
||||||
|
if _, _, err := parseServerReverseNameId(id); err == nil {
|
||||||
|
t.Errorf("expected error parsing %q", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerReverseNameResource(t *testing.T) {
|
||||||
|
// Must assign a password to the server or Binary Lane will send emails
|
||||||
|
password := GenerateTestPassword(t)
|
||||||
|
|
||||||
|
serverConfig := `
|
||||||
|
|
||||||
|
resource "binarylane_server" "test" {
|
||||||
|
name = "tf-test-server-reverse-name"
|
||||||
|
region = "per"
|
||||||
|
image = "debian-12"
|
||||||
|
size = "std-min"
|
||||||
|
password = "` + password + `"
|
||||||
|
public_ipv4_count = 1
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var serverId int64
|
||||||
|
var ipv4Address string
|
||||||
|
|
||||||
|
resource.ParallelTest(t, resource.TestCase{
|
||||||
|
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||||
|
CheckDestroy: func(_ *terraform.State) error {
|
||||||
|
return testAccCheckServerReverseNameCleared(serverId, ipv4Address)
|
||||||
|
},
|
||||||
|
Steps: []resource.TestStep{
|
||||||
|
// Create and Read testing
|
||||||
|
{
|
||||||
|
Config: providerConfig + serverConfig + `
|
||||||
|
resource "binarylane_server_reverse_name" "test" {
|
||||||
|
server_id = binarylane_server.test.id
|
||||||
|
ipv4_address = binarylane_server.test.public_ipv4_addresses.0
|
||||||
|
reverse_name = "tf-test-ptr.example.com"
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Check: resource.ComposeAggregateTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttrPair("binarylane_server_reverse_name.test", "server_id", "binarylane_server.test", "id"),
|
||||||
|
resource.TestCheckResourceAttrPair("binarylane_server_reverse_name.test", "ipv4_address", "binarylane_server.test", "public_ipv4_addresses.0"),
|
||||||
|
resource.TestCheckResourceAttr("binarylane_server_reverse_name.test", "reverse_name", "tf-test-ptr.example.com"),
|
||||||
|
resource.TestCheckResourceAttrSet("binarylane_server_reverse_name.test", "id"),
|
||||||
|
func(s *terraform.State) error {
|
||||||
|
attrs := s.RootModule().Resources["binarylane_server_reverse_name.test"].Primary.Attributes
|
||||||
|
var err error
|
||||||
|
serverId, err = strconv.ParseInt(attrs["server_id"], 10, 64)
|
||||||
|
ipv4Address = attrs["ipv4_address"]
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// ImportState testing
|
||||||
|
{
|
||||||
|
ResourceName: "binarylane_server_reverse_name.test",
|
||||||
|
ImportState: true,
|
||||||
|
ImportStateVerify: true,
|
||||||
|
ImportStateIdFunc: func(s *terraform.State) (string, error) {
|
||||||
|
resourceState := s.RootModule().Resources["binarylane_server_reverse_name.test"]
|
||||||
|
return resourceState.Primary.Attributes["id"], nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Update and Read testing
|
||||||
|
{
|
||||||
|
Config: providerConfig + serverConfig + `
|
||||||
|
resource "binarylane_server_reverse_name" "test" {
|
||||||
|
server_id = binarylane_server.test.id
|
||||||
|
ipv4_address = binarylane_server.test.public_ipv4_addresses.0
|
||||||
|
reverse_name = "tf-test-ptr-updated.example.com" # Updated reverse name
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
Check: resource.ComposeAggregateTestCheckFunc(
|
||||||
|
resource.TestCheckResourceAttr("binarylane_server_reverse_name.test", "reverse_name", "tf-test-ptr-updated.example.com"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// Removing the resource clears the reverse name while the server still exists
|
||||||
|
{
|
||||||
|
Config: providerConfig + serverConfig,
|
||||||
|
Check: func(_ *terraform.State) error {
|
||||||
|
return testAccCheckServerReverseNameCleared(serverId, ipv4Address)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Delete testing automatically occurs in TestCase
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccCheckServerReverseNameCleared(serverId int64, ipv4Address string) error {
|
||||||
|
client, err := binarylane.NewClientWithConfig("", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := client.GetServersServerIdWithResponse(context.Background(), serverId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.StatusCode() == http.StatusNotFound {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status %s reading server %d: %s", resp.Status(), serverId, resp.Body)
|
||||||
|
}
|
||||||
|
for _, n := range resp.JSON200.Server.Networks.V4 {
|
||||||
|
if n.IpAddress != ipv4Address || n.ReverseName == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch *n.ReverseName {
|
||||||
|
case "tf-test-ptr.example.com", "tf-test-ptr-updated.example.com":
|
||||||
|
return fmt.Errorf("reverse name for %s on server %d not cleared: %s", ipv4Address, serverId, *n.ReverseName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"terraform-provider-binarylane/internal/binarylane"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
|
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServerReverseNameResourceUnit(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
var (
|
||||||
|
postStatus = http.StatusOK
|
||||||
|
serverFound = true
|
||||||
|
serverIp = "203.0.113.7"
|
||||||
|
serverName = "ptr.example.com"
|
||||||
|
lastBody map[string]any
|
||||||
|
)
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
switch {
|
||||||
|
case r.Method == http.MethodPost && r.URL.Path == "/servers/1/actions":
|
||||||
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
lastBody = nil
|
||||||
|
if err := json.Unmarshal(b, &lastBody); err != nil {
|
||||||
|
t.Errorf("decode POST body: %v", err)
|
||||||
|
}
|
||||||
|
w.WriteHeader(postStatus)
|
||||||
|
if postStatus == http.StatusOK {
|
||||||
|
_, _ = w.Write([]byte(`{"action":{"id":9,"status":"in-progress"}}`))
|
||||||
|
}
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/servers/1":
|
||||||
|
if !serverFound {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, `{"server":{"id":1,"networks":{"v4":[{"ip_address":%q,"type":"public","reverse_name":%q}]}}}`, serverIp, serverName)
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client, err := binarylane.NewClientWithConfig(srv.URL, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := &serverReverseNameResource{bc: &BinarylaneClient{client: client}}
|
||||||
|
|
||||||
|
schemaResp := resource.SchemaResponse{}
|
||||||
|
r.Schema(ctx, resource.SchemaRequest{}, &schemaResp)
|
||||||
|
s := schemaResp.Schema
|
||||||
|
emptyState := func() tfsdk.State {
|
||||||
|
return tfsdk.State{Schema: s, Raw: tftypes.NewValue(s.Type().TerraformType(ctx), nil)}
|
||||||
|
}
|
||||||
|
model := serverReverseNameResourceModel{
|
||||||
|
Id: types.StringUnknown(),
|
||||||
|
ServerId: types.Int64Value(1),
|
||||||
|
Ipv4Address: types.StringValue("203.0.113.7"),
|
||||||
|
ReverseName: types.StringValue("ptr.example.com"),
|
||||||
|
}
|
||||||
|
plan := tfsdk.Plan{Schema: s, Raw: tftypes.NewValue(s.Type().TerraformType(ctx), nil)}
|
||||||
|
if diags := plan.Set(ctx, &model); diags.HasError() {
|
||||||
|
t.Fatal(diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
createResp := resource.CreateResponse{State: emptyState()}
|
||||||
|
r.Create(ctx, resource.CreateRequest{Plan: plan}, &createResp)
|
||||||
|
if createResp.Diagnostics.HasError() {
|
||||||
|
t.Fatal(createResp.Diagnostics)
|
||||||
|
}
|
||||||
|
if lastBody["reverse_name"] != "ptr.example.com" || lastBody["ipv4_address"] != "203.0.113.7" {
|
||||||
|
t.Fatalf("create: unexpected request body %v", lastBody)
|
||||||
|
}
|
||||||
|
state := createResp.State
|
||||||
|
|
||||||
|
read := func() (serverReverseNameResourceModel, bool) {
|
||||||
|
t.Helper()
|
||||||
|
readResp := resource.ReadResponse{State: state}
|
||||||
|
r.Read(ctx, resource.ReadRequest{State: state}, &readResp)
|
||||||
|
if readResp.Diagnostics.HasError() {
|
||||||
|
t.Fatal(readResp.Diagnostics)
|
||||||
|
}
|
||||||
|
if readResp.State.Raw.IsNull() {
|
||||||
|
return serverReverseNameResourceModel{}, false
|
||||||
|
}
|
||||||
|
var got serverReverseNameResourceModel
|
||||||
|
if diags := readResp.State.Get(ctx, &got); diags.HasError() {
|
||||||
|
t.Fatal(diags)
|
||||||
|
}
|
||||||
|
return got, true
|
||||||
|
}
|
||||||
|
|
||||||
|
serverName = "drifted.example.com"
|
||||||
|
if got, ok := read(); !ok || got.ReverseName.ValueString() != "drifted.example.com" {
|
||||||
|
t.Fatalf("read: expected drift to drifted.example.com, got %v (present=%v)", got.ReverseName, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
serverIp = "198.51.100.1"
|
||||||
|
if _, ok := read(); ok {
|
||||||
|
t.Fatal("read: expected state removed when IP is not on the server")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverFound = false
|
||||||
|
if _, ok := read(); ok {
|
||||||
|
t.Fatal("read: expected state removed when server is 404")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverFound, serverIp, serverName = true, "203.0.113.7", "ptr.example.com"
|
||||||
|
postStatus = http.StatusAccepted
|
||||||
|
updateResp := resource.UpdateResponse{State: emptyState()}
|
||||||
|
r.Update(ctx, resource.UpdateRequest{Plan: plan, State: state}, &updateResp)
|
||||||
|
if updateResp.Diagnostics.HasError() {
|
||||||
|
t.Fatalf("update: 202 without action should succeed: %v", updateResp.Diagnostics)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteResp := resource.DeleteResponse{State: state}
|
||||||
|
r.Delete(ctx, resource.DeleteRequest{State: state}, &deleteResp)
|
||||||
|
if deleteResp.Diagnostics.HasError() {
|
||||||
|
t.Fatalf("delete: 202 without action should succeed: %v", deleteResp.Diagnostics)
|
||||||
|
}
|
||||||
|
if _, ok := lastBody["reverse_name"]; ok {
|
||||||
|
t.Fatalf("delete: request body must not contain reverse_name, got %v", lastBody)
|
||||||
|
}
|
||||||
|
if lastBody["ipv4_address"] != "203.0.113.7" {
|
||||||
|
t.Fatalf("delete: unexpected request body %v", lastBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The change_reverse_name action never resolves while the PTR is already applied on the server.
|
||||||
|
func TestServerReverseNameCreateStuckActionUnit(t *testing.T) {
|
||||||
|
serverActionPollInterval = time.Millisecond
|
||||||
|
t.Cleanup(func() { serverActionPollInterval = 5 * time.Second })
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
serverGets := 0
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
switch {
|
||||||
|
case r.Method == http.MethodPost && r.URL.Path == "/servers/1/actions":
|
||||||
|
_, _ = w.Write([]byte(`{"action":{"id":9,"status":"in-progress"}}`))
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/servers/1/actions/9":
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
case r.Method == http.MethodGet && r.URL.Path == "/servers/1":
|
||||||
|
serverGets++
|
||||||
|
name := "default.example.net"
|
||||||
|
if serverGets > 2 {
|
||||||
|
name = "ptr.example.com"
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintf(w, `{"server":{"id":1,"networks":{"v4":[{"ip_address":"203.0.113.7","type":"public","reverse_name":%q}]}}}`, name)
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
|
||||||
|
w.WriteHeader(http.StatusTeapot)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client, err := binarylane.NewClientWithConfig(srv.URL, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r := &serverReverseNameResource{bc: &BinarylaneClient{client: client}}
|
||||||
|
want := "ptr.example.com"
|
||||||
|
if err := r.changeReverseName(ctx, 1, "203.0.113.7", &want); err != nil {
|
||||||
|
t.Fatalf("create must finish once the server reports the reverse name: %v", err)
|
||||||
|
}
|
||||||
|
if serverGets != 3 {
|
||||||
|
t.Fatalf("expected to poll the server until the reverse name applied, got %d GETs", serverGets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWaitForServerActionUnit(t *testing.T) {
|
||||||
|
serverActionPollInterval = time.Millisecond
|
||||||
|
t.Cleanup(func() { serverActionPollInterval = 5 * time.Second })
|
||||||
|
|
||||||
|
for name, tc := range map[string]struct {
|
||||||
|
status int
|
||||||
|
body string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
"completed_at set": {http.StatusOK, `{"action":{"id":9,"status":"in-progress","completed_at":"2026-01-01T00:00:00Z"}}`, false},
|
||||||
|
"completed status only": {http.StatusOK, `{"action":{"id":9,"status":"completed"}}`, false},
|
||||||
|
"errored": {http.StatusOK, `{"action":{"id":9,"status":"errored"}}`, true},
|
||||||
|
"not found": {http.StatusNotFound, `{"message":"Action not found."}`, true},
|
||||||
|
"in progress until timeout": {http.StatusOK, `{"action":{"id":9,"status":"in-progress"}}`, true},
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(tc.status)
|
||||||
|
_, _ = w.Write([]byte(tc.body))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client, err := binarylane.NewClientWithConfig(srv.URL, "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sr := serverResource{bc: &BinarylaneClient{client: client}}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
if err := sr.waitForServerAction(ctx, 1, 9); (err != nil) != tc.wantErr {
|
||||||
|
t.Fatalf("wantErr=%v, got %v", tc.wantErr, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user