Compare commits

...

15 Commits

Author SHA1 Message Date
Oscar Hermoso e18d2424d0 📼 ensure that load balancer always sets model state for health checks (#32) 2024-10-25 10:39:11 +08:00
dependabot[bot] e1aac7ec1f 🐹 Bump github.com/hashicorp/terraform-plugin-framework-validators (#31)
Bumps the minor-patch group with 1 update: [github.com/hashicorp/terraform-plugin-framework-validators](https://github.com/hashicorp/terraform-plugin-framework-validators).


Updates `github.com/hashicorp/terraform-plugin-framework-validators` from 0.13.0 to 0.14.0
- [Release notes](https://github.com/hashicorp/terraform-plugin-framework-validators/releases)
- [Changelog](https://github.com/hashicorp/terraform-plugin-framework-validators/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/terraform-plugin-framework-validators/compare/v0.13.0...v0.14.0)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/terraform-plugin-framework-validators
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-18 09:50:08 +08:00
Oscar Hermoso 6c3520bbda ⭕ Retry if Servers, SSH Keys, or Load Balancer creation returns 500 response (#29)
* ⭕ retry on failed create of server, load balancer, and SSH key

* 🧹 sweepers for test resources

* 🧹 sweepers, but now they're working
2024-10-13 08:48:12 +08:00
dependabot[bot] 22ea042e33 Bump hashicorp/setup-terraform from 2 to 3 (#23)
Bumps [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) from 2 to 3.
- [Release notes](https://github.com/hashicorp/setup-terraform/releases)
- [Changelog](https://github.com/hashicorp/setup-terraform/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/setup-terraform/compare/v2...v3)

---
updated-dependencies:
- dependency-name: hashicorp/setup-terraform
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-13 08:47:41 +08:00
dependabot[bot] b63f052a9f Bump actions/checkout from 3 to 4 (#22)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-10 16:37:09 +08:00
Oscar Hermoso a5731e0a12 🧲 Fix README link to examples/README.md 2024-10-05 08:34:22 +08:00
Oscar Hermoso e5450d9f27 🐒 Fix SSH key logging, types (#28)
* 🐒 Fix SSH key logging, import type

* 💱 Ignore comments in go generate git diff
2024-10-04 13:10:14 +08:00
dependabot[bot] 53b1643f30 🐹 Bump github.com/oapi-codegen/oapi-codegen/v2 in the minor-patch group (#27)
Bumps the minor-patch group with 1 update: [github.com/oapi-codegen/oapi-codegen/v2](https://github.com/oapi-codegen/oapi-codegen).


Updates `github.com/oapi-codegen/oapi-codegen/v2` from 2.3.0 to 2.4.1
- [Release notes](https://github.com/oapi-codegen/oapi-codegen/releases)
- [Commits](https://github.com/oapi-codegen/oapi-codegen/compare/v2.3.0...v2.4.1)

---
updated-dependencies:
- dependency-name: github.com/oapi-codegen/oapi-codegen/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-04 12:15:54 +08:00
Oscar Hermoso 1084c27ab6 🥢 import ssh key by fingerprint, make tests faster (#26)
* 🥢 import ssh key by fingerprint, make tests faster

* ⌚ Wait for load balancer creation
2024-10-04 10:14:23 +08:00
dependabot[bot] ffcadb74b3 Bump actions/setup-go from 4 to 5 (#21)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-27 11:41:55 +08:00
Oscar Hermoso d1adea205b 🐛 Move cancel-in-progress to correct location 2024-09-27 08:53:27 +08:00
Oscar Hermoso 57ed8ff440 🐈 pass secrets to test.yml, add prefix to dependabot commit messages (#25)
* 🐈 Add commit message prefix to dependabot.yml

* 😶 Pass GitHub actions secrets from release.yml to test.yml
2024-09-27 08:50:17 +08:00
Oscar Hermoso 702aae5b2f 🐛 Fix release.yml depending on test.yml 2024-09-27 08:27:01 +08:00
Oscar Hermoso 9747587128 🧪 Bump terraform framework to v0.4.1 & test skipping go generate (#24)
* 🧪 Bump terraform framework to v0.4.1 & test skipping go generate

* ⏰ run go generate, update dependabot.yml
2024-09-27 08:22:01 +08:00
Oscar Hermoso 44772aa55d 🚛 load balancer resource (#17)
* 🚛 load balancer resource

* Fix whitespace in server resource
2024-09-16 15:55:01 +08:00
39 changed files with 2567 additions and 276 deletions
+8
View File
@@ -0,0 +1,8 @@
TF_ACC=1
BINARYLANE_API_TOKEN=
# Uncomment to enable debug logging (will dump request bodies)
# TF_LOG=DEBUG
# Uncomment to disable the Go cache when testing, useful for flaky tests
# GOCACHE=off
+20
View File
@@ -1,10 +1,30 @@
version: 2
updates:
- package-ecosystem: 'github-actions'
commit-message:
prefix: '🐈 '
directory: '/'
schedule:
interval: 'weekly'
day: 'friday'
time: '06:00'
timezone: 'Australia/Perth'
groups:
minor-patch:
update-types:
- 'minor'
- 'patch'
- package-ecosystem: 'gomod'
commit-message:
prefix: '🐹 '
directory: '/'
schedule:
interval: 'weekly'
day: 'friday'
time: '06:00'
timezone: 'Australia/Perth'
groups:
minor-patch:
update-types:
- 'minor'
- 'patch'
+8 -2
View File
@@ -6,7 +6,7 @@ name: Release
on:
push:
tags:
- "v*"
- 'v*'
# Releases need permissions to read and write the repository contents.
# GitHub considers creating releases and uploading assets as writing contents.
@@ -14,7 +14,13 @@ permissions:
contents: write
jobs:
test:
uses: ./.github/workflows/test.yml
secrets:
BINARYLANE_API_TOKEN: ${{ secrets.BINARYLANE_API_TOKEN }}
goreleaser:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
@@ -23,7 +29,7 @@ jobs:
fetch-depth: 0
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version-file: "go.mod"
go-version-file: 'go.mod'
cache: true
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
+61
View File
@@ -0,0 +1,61 @@
name: Terraform Provider Tests
on:
pull_request:
paths:
- '.github/workflows/test.yml'
- '**.go'
workflow_call:
secrets:
BINARYLANE_API_TOKEN:
required: true
permissions:
# Permission for checking out code
contents: read
jobs:
acceptance:
name: Acceptance Tests
runs-on: ubuntu-latest
environment: test
concurrency:
group: test
cancel-in-progress: true # would be nice if GH offered cancel-pending instead, https://github.com/orgs/community/discussions/63136
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.5.*'
terraform_wrapper: false
- name: Check if generated files are up to date
run: |
go generate
git diff --color --exit-code --ignore-matching-lines='//'
if [ $? -ne 0 ]; then
echo "generated files are not up to date, please run 'go generate' and commit the changes" >&2
exit 1
fi
- name: Run tests
run: go test -v -cover ./internal/provider/...
env:
TF_ACC: '1'
BINARYLANE_API_TOKEN: ${{ secrets.BINARYLANE_API_TOKEN }}
- name: Run sweepers
if: failure()
run: go test -v ./internal/provider/... -sweep=all
env:
TF_ACC: '1'
BINARYLANE_API_TOKEN: ${{ secrets.BINARYLANE_API_TOKEN }}
# unit:
# name: Unit Tests
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-go@v5
# with:
# go-version-file: 'go.mod'
# - run: go test -v -cover ./...
+3
View File
@@ -0,0 +1,3 @@
{
"go.testEnvFile": "${workspaceFolder}/.env",
}
+26 -4
View File
@@ -35,7 +35,16 @@ terraform apply
```sh
TF_DEBUG=1 BINARYLANE_API_TOKEN=********* TF_ACC=1 go test -v ./...
cp .env.example .env
# Add your API token to .env
eval export $(cat .env)
go test -v ./internal/provider/...
```
To run Sweepers:
```sh
go test -v ./internal/provider/... -sweep=all
```
### Update modules
@@ -131,7 +140,7 @@ Add a `Configure` method to the resource:
+ }
```
Add the new data source/resource to `provider.go`:
Add the new resource to `provider.go`:
```diff
return []func() resource.Resource{
@@ -204,12 +213,25 @@ Use the generated schema to define the data source schema.
- },
- },
- }
+ ds, err := convertResourceSchemaToDataSourceSchema(resources.ExampleResourceSchema(ctx))
+ ds, err := convertResourceSchemaToDataSourceSchema(
+ resources.ExampleResourceSchema(ctx)
+ AttributeConfig{
+ RequiredAttributes: &[]string{"id"},
+ },
+ )
+ if err != nil {
+ resp.Diagnostics.AddError("Failed to convert resource schema to data source schema", err.Error())
+ return
+ }
+ resp.Schema = *ds
+ resp.Schema.Description = "TODO"
+ // resp.Schema.Description = "TODO"
}
```
Add the data source to `provider.go`:
```diff
return []func() datasource.DataSource{
# ...
+ NewExampleDataSource,
}
+5 -5
View File
@@ -1,6 +1,6 @@
# terraform-provider-binarylane
See the examples in the [examples directory](./examples/basic/README.md).
See the examples in the [examples directory](./examples/README.md).
```terraform
resource "binarylane_server" "example" {
@@ -24,8 +24,8 @@ Planned features:
- [ ] Alerts
- [x] SSH Keys
- [x] Virtual Private Cloud
- [x] Load Balancers
- [ ] Images
- [ ] Load Balancers
- [ ] DNS
- [x] Docs
- [x] Generated docs
@@ -44,7 +44,7 @@ Images:
```sh
curl -X GET "https://api.binarylane.com.au/v2/images?type=distribution&&page=1&per_page=200" \
-H "Authorization: Bearer **********" > tmp/images.json
-H "Authorization: Bearer $BINARYLANE_API_TOKEN" > tmp/images.json
jq '[ .images[] | .slug ] | sort' tmp/images.json
```
@@ -81,7 +81,7 @@ Regions:
```sh
curl -X GET "https://api.binarylane.com.au/v2/regions" \
-H "Authorization: Bearer **********"" > tmp/regions.json
-H "Authorization: Bearer $BINARYLANE_API_TOKEN" > tmp/regions.json
jq '[ .regions[] | .slug ] | sort' tmp/regions.json
```
@@ -103,7 +103,7 @@ jq '[ .regions[] | .slug ] | sort' tmp/regions.json
```sh
curl -X GET "https://api.binarylane.com.au/v2/sizes" \
-H "Authorization: Bearer **********"" > tmp/sizes.json
-H "Authorization: Bearer $BINARYLANE_API_TOKEN" > tmp/sizes.json
jq '[ .sizes[] | .slug ] | sort' tmp/sizes.json
```
+44
View File
@@ -0,0 +1,44 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_load_balancer Data Source - terraform-provider-binarylane"
subcategory: ""
description: |-
---
# binarylane_load_balancer (Data Source)
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `id` (Number) The ID of the load balancer to fetch.
### Read-Only
- `forwarding_rules` (List of Object) The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default "HTTP" only forwarding rule. (see [below for nested schema](#nestedatt--forwarding_rules))
- `health_check` (Object) The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules. (see [below for nested schema](#nestedatt--health_check))
- `name` (String) The hostname of the load balancer.
- `region` (String) Leave null to create an anycast load balancer.
- `server_ids` (List of Number) A list of server IDs to assign to this load balancer.
<a id="nestedatt--forwarding_rules"></a>
### Nested Schema for `forwarding_rules`
Read-Only:
- `entry_protocol` (String)
<a id="nestedatt--health_check"></a>
### Nested Schema for `health_check`
Read-Only:
- `path` (String)
- `protocol` (String)
+1
View File
@@ -22,5 +22,6 @@ description: |-
### Read-Only
- `default` (Boolean) Optional: If true this will be added to all new server installations (if we support SSH Key injection for the server's operating system).
- `fingerprint` (String) The fingerprint of the SSH key.
- `name` (String) A name to help you identify the key.
- `public_key` (String) The public key in OpenSSH "authorized_keys" format.
+47
View File
@@ -0,0 +1,47 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_load_balancer Resource - terraform-provider-binarylane"
subcategory: ""
description: |-
---
# binarylane_load_balancer (Resource)
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The hostname of the load balancer.
### Optional
- `forwarding_rules` (Attributes List) The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default "HTTP" only forwarding rule. (see [below for nested schema](#nestedatt--forwarding_rules))
- `health_check` (Attributes) The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules. (see [below for nested schema](#nestedatt--health_check))
- `region` (String) Leave null to create an anycast load balancer.
- `server_ids` (List of Number) A list of server IDs to assign to this load balancer.
### Read-Only
- `id` (Number) The ID of the load balancer to fetch.
<a id="nestedatt--forwarding_rules"></a>
### Nested Schema for `forwarding_rules`
Required:
- `entry_protocol` (String) The protocol that traffic must match for this load balancer to forward traffic according to this rule.
<a id="nestedatt--health_check"></a>
### Nested Schema for `health_check`
Optional:
- `path` (String) Leave null to accept the default '/' path.
- `protocol` (String) Leave null to accept the default HTTP protocol.
+4
View File
@@ -24,3 +24,7 @@ description: |-
- `default` (Boolean) Optional: If true this will be added to all new server installations (if we support SSH Key injection for the server's operating system).
- `id` (Number) The ID or fingerprint of the SSH Key to fetch.
### Read-Only
- `fingerprint` (String) The fingerprint of the SSH key.
+8 -1
View File
@@ -2,7 +2,14 @@
This WIP example shows how to create a Kubernetes cluster on Binary Lane.
Prior art:
## Prior art
- [MartinHodges/create_k8s](https://github.com/MartinHodges/create_k8s) repo and the [medium article](https://medium.com/@martin.hodges/creating-a-kubernetes-cluster-from-scratch-in-1-hour-using-automation-a25e387be547) that goes with it.
- https://github.com/inscapist/terraform-k3s-private-cloud
- https://github.com/schnerring/schnerring.github.io/blob/07d06fb40e3d01f2483a739f516ac4d711a5742c/content/blog/use-terraform-to-deploy-an-azure-kubernetes-service-aks-cluster-traefik-2-cert-manager-and-lets-encrypt-certificates/index.md
## Remaining TODOs
- [ ] Consider rewriting module to use `microk8s` instead of `k3s`
- [ ] Use `kustomize` instead of `helm`
- [ ] Use the `binarylane_load_balancer` resource instead of `kubectl port-forward` (or use the load balancer in another example)
+8 -9
View File
@@ -134,15 +134,14 @@ resource "binarylane_server_firewall_rules" "example" {
server_id = each.value.id
firewall_rules = [
# TODO: Due to a bug, this only works if you uncomment AFTER creating servers
# {
# description = "K3s supervisor and Kubernetes API Server"
# protocol = "tcp"
# source_addresses = ["0.0.0.0/0"]
# destination_addresses = local.server_ips
# destination_ports = ["6443"]
# action = "accept"
# },
{
description = "K3s supervisor and Kubernetes API Server"
protocol = "tcp"
source_addresses = ["0.0.0.0/0"]
destination_addresses = local.server_ips
destination_ports = ["6443"]
action = "accept"
},
{
description = "Flannel VXLAN"
protocol = "udp"
+1 -1
View File
@@ -8,4 +8,4 @@ package main
//go:generate mkdir -p internal/resources internal/data_sources
//go:generate go run github.com/hashicorp/terraform-plugin-codegen-framework/cmd/tfplugingen-framework generate resources --input=./provider_code_spec.json --output=./internal/resources --package=resources
//go:generate go run github.com/hashicorp/terraform-plugin-codegen-framework/cmd/tfplugingen-framework generate data-sources --input=./provider_code_spec.json --output=./internal/data_sources --package=data_sources
//go:generate tfplugindocs generate
//go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs generate
+15 -14
View File
@@ -1,18 +1,18 @@
module terraform-provider-binarylane
go 1.22.3
go 1.22.7
require (
github.com/deepmap/oapi-codegen v1.16.3
github.com/hashicorp/terraform-plugin-codegen-framework v0.4.0
github.com/hashicorp/terraform-plugin-codegen-framework v0.4.1
github.com/hashicorp/terraform-plugin-codegen-openapi v0.3.0
github.com/hashicorp/terraform-plugin-docs v0.19.4
github.com/hashicorp/terraform-plugin-framework v1.11.0
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0
github.com/hashicorp/terraform-plugin-go v0.23.0
github.com/hashicorp/terraform-plugin-framework v1.12.0
github.com/hashicorp/terraform-plugin-framework-validators v0.14.0
github.com/hashicorp/terraform-plugin-go v0.24.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.10.0
github.com/oapi-codegen/oapi-codegen/v2 v2.3.0
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1
github.com/oapi-codegen/runtime v1.1.1
)
@@ -36,9 +36,9 @@ require (
github.com/fatih/color v1.17.0 // indirect
github.com/frankban/quicktest v1.14.6 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/getkin/kin-openapi v0.124.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/getkin/kin-openapi v0.127.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@@ -58,14 +58,14 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-codegen-spec v0.1.1 // indirect
github.com/hashicorp/terraform-plugin-codegen-spec v0.2.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -84,6 +84,7 @@ require (
github.com/posener/complete v1.2.3 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/speakeasy-api/openapi-overlay v0.9.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
@@ -106,8 +107,8 @@ require (
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.22.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
google.golang.org/grpc v1.66.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 // indirect
google.golang.org/grpc v1.67.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+28 -27
View File
@@ -63,18 +63,18 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys=
github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY=
github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q=
github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs=
github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw=
github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
@@ -139,20 +139,20 @@ github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVW
github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg=
github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec=
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-plugin-codegen-framework v0.4.0 h1:xB96Rakulnn7ZxikDj8glCyojKpTbW+SGm6QxipoNxU=
github.com/hashicorp/terraform-plugin-codegen-framework v0.4.0/go.mod h1:nXlVjqHFBDr6EXnKTT9RCLmwnuTYj/3LE4ffkIdOtog=
github.com/hashicorp/terraform-plugin-codegen-framework v0.4.1 h1:eaI/3dsu2T5QAXbA+7N+B+UBj20GdtYnsRuYypKh3S4=
github.com/hashicorp/terraform-plugin-codegen-framework v0.4.1/go.mod h1:kpYM23L7NtcfaQdWAN0QFkV/lU0w16qJ2ddAPCI4zAg=
github.com/hashicorp/terraform-plugin-codegen-openapi v0.3.0 h1:IKpc337XKk50QyQPSxLrHwdqSo1E2XqCMxFkWsZcTvc=
github.com/hashicorp/terraform-plugin-codegen-openapi v0.3.0/go.mod h1:tT6wl80h7nsMBw+1yZRgJXi+Ys85PUai11weDqysvp4=
github.com/hashicorp/terraform-plugin-codegen-spec v0.1.1 h1:7djpLeqtz0x6w01W4hckWwINJUnfXn2zGr/EhSC8rek=
github.com/hashicorp/terraform-plugin-codegen-spec v0.1.1/go.mod h1:PQn6bDD8UWoAVJoHXqFk2i/RmLbeQBjbiP38i+E+YIw=
github.com/hashicorp/terraform-plugin-codegen-spec v0.2.0 h1:91dQG1A/DxP6vRz9GiytDTrZTXDbhHPvmpYnAyWA/Vw=
github.com/hashicorp/terraform-plugin-codegen-spec v0.2.0/go.mod h1:fywrEKpordQypmAjz/HIfm2LuNVmyJ6KDe8XT9GdJxQ=
github.com/hashicorp/terraform-plugin-docs v0.19.4 h1:G3Bgo7J22OMtegIgn8Cd/CaSeyEljqjH3G39w28JK4c=
github.com/hashicorp/terraform-plugin-docs v0.19.4/go.mod h1:4pLASsatTmRynVzsjEhbXZ6s7xBlUw/2Kt0zfrq8HxA=
github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE=
github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-framework v1.12.0 h1:7HKaueHPaikX5/7cbC1r9d1m12iYHY+FlNZEGxQ42CQ=
github.com/hashicorp/terraform-plugin-framework v1.12.0/go.mod h1:N/IOQ2uYjW60Jp39Cp3mw7I/OpC/GfZ0385R0YibmkE=
github.com/hashicorp/terraform-plugin-framework-validators v0.14.0 h1:3PCn9iyzdVOgHYOBmncpSSOxjQhCTYmc+PGvbdlqSaI=
github.com/hashicorp/terraform-plugin-framework-validators v0.14.0/go.mod h1:LwDKNdzxrDY/mHBrlC6aYfE2fQ3Dk3gaJD64vNiXvo4=
github.com/hashicorp/terraform-plugin-go v0.24.0 h1:2WpHhginCdVhFIrWHxDEg6RBn3YaWzR2o6qUeIEat2U=
github.com/hashicorp/terraform-plugin-go v0.24.0/go.mod h1:tUQ53lAsOyYSckFGEefGC5C8BAaO0ENqzFd3bQeuYQg=
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.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE=
@@ -163,8 +163,8 @@ github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTV
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
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.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
@@ -173,8 +173,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY=
github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
@@ -221,8 +221,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 h1:rICjNsHbPP1LttefanBPnwsSwl09SqhCO7Ee623qR84=
github.com/oapi-codegen/oapi-codegen/v2 v2.3.0/go.mod h1:4k+cJeSq5ntkwlcpQSxLxICCxQzCL772o30PxdibRt4=
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1 h1:ykgG34472DWey7TSjd8vIfNykXgjOgYJZoQbKfEeY/Q=
github.com/oapi-codegen/oapi-codegen/v2 v2.4.1/go.mod h1:N5+lY1tiTDV3V1BeHtOxeWXHoPVeApvsvjJqegfoaz8=
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
@@ -264,6 +264,8 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A=
github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/speakeasy-api/openapi-overlay v0.9.0 h1:Wrz6NO02cNlLzx1fB093lBlYxSI54VRhy1aSutx0PQg=
github.com/speakeasy-api/openapi-overlay v0.9.0/go.mod h1:f5FloQrHA7MsxYg9djzMD5h6dxrHjVVByWKh7an8TRc=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
@@ -393,10 +395,10 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c=
google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61 h1:N9BgCIAUvn/M+p4NJccWPWb3BWh88+zyL0ll9HgbEeM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240924160255-9d4c2d233b61/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw=
google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -425,6 +427,5 @@ 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-20191026110619-0b21df46bc1d/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/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=
+49
View File
@@ -0,0 +1,49 @@
package binarylane
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httputil"
"github.com/deepmap/oapi-codegen/pkg/securityprovider"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
func NewClientWithAuth(endpoint string, token string) (*ClientWithResponses, error) {
if token == "" {
return nil, errors.New("missing or empty value for the Binary Lane API " +
"token. Set the `api_token` value in the configuration or use the " +
"BINARYLANE_API_TOKEN environment variable. If either is already set, " +
"ensure the value is not empty")
}
auth, err := securityprovider.NewSecurityProviderBearerToken(token)
if err != nil {
return nil, fmt.Errorf("failed to create API client with supplied API token: %w", err)
}
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
client, err := NewClientWithResponses(
endpoint,
WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
dump, err := httputil.DumpRequestOut(req, true)
if err != nil {
return err
}
tflog.Debug(ctx, fmt.Sprintf("%q\n", dump))
return nil
}),
WithRequestEditorFn(auth.Intercept), // include auth AFTER the request logger
)
if err != nil {
return nil, fmt.Errorf("failed to create Binary Lane API client: %w", err)
}
return client, nil
}
+1 -1
View File
@@ -1,6 +1,6 @@
// Package binarylane provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT.
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT.
package binarylane
import (
+7 -1
View File
@@ -21,7 +21,7 @@ cat <<<$(jq '.paths["/account/keys/{key_id}"].delete.parameters[0].schema |= del
# Remove the "/paths/{image_id}" path because its duplicated by "/images/{image_id_or_slug}"
cat <<<$(jq 'del(.paths."/images/{image_id}")' $OPENAPI_FILE) >$OPENAPI_FILE
# Add x-oapi-codegen-extra-tags so Go structs can be reflected
# Add x-oapi-codegen-extra-tags so structs can be reflected
## RouteEntryRequest
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.destination += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination"}}' $OPENAPI_FILE) >$OPENAPI_FILE
@@ -35,3 +35,9 @@ cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.action += {"x-o
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.destination_addresses += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination_addresses"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.destination_ports += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination_ports"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.source_addresses += {"x-oapi-codegen-extra-tags": {"tfsdk": "source_addresses"}}' $OPENAPI_FILE) >$OPENAPI_FILE
## Load Balancer
cat <<<$(jq '.components.schemas.CreateLoadBalancerRequest.properties.forwarding_rules += {"x-oapi-codegen-extra-tags": {"tfsdk": "forwarding_rules"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.ForwardingRule.properties.entry_protocol += {"x-oapi-codegen-extra-tags": {"tfsdk": "entry_protocol"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.LoadBalancer.properties.health_check += {"x-oapi-codegen-extra-tags": {"tfsdk": "health_check"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.HealthCheckProtocol |= del(.enum)' $OPENAPI_FILE) >$OPENAPI_FILE
+12 -8
View File
@@ -10057,7 +10057,10 @@
"$ref": "#/components/schemas/ForwardingRule"
},
"description": "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule.",
"nullable": true
"nullable": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "forwarding_rules"
}
},
"health_check": {
"allOf": [
@@ -10998,7 +11001,10 @@
"$ref": "#/components/schemas/LoadBalancerRuleProtocol"
}
],
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule."
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
"x-oapi-codegen-extra-tags": {
"tfsdk": "entry_protocol"
}
}
}
},
@@ -11038,11 +11044,6 @@
}
},
"HealthCheckProtocol": {
"enum": [
"http",
"https",
"both"
],
"type": "string",
"description": "\n| Value | Description |\n| ----- | ----------- |\n| http | The health check will be performed via HTTP. |\n| https | The health check will be performed via HTTPS. |\n| both | The health check will be performed via both HTTP and HTTPS. Failing a health check on one protocol will remove the server from the pool of servers only for that protocol. |\n\n",
"x-enum-descriptions": [
@@ -11844,7 +11845,10 @@
}
],
"description": "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer.",
"readOnly": true
"readOnly": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "health_check"
}
},
"region": {
"allOf": [
+7 -14
View File
@@ -1,6 +1,6 @@
// Package binarylane provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT.
// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.1 DO NOT EDIT.
package binarylane
import (
@@ -230,13 +230,6 @@ const (
EnableIpv6TypeEnableIpv6 EnableIpv6Type = "enable_ipv6"
)
// Defines values for HealthCheckProtocol.
const (
HealthCheckProtocolBoth HealthCheckProtocol = "both"
HealthCheckProtocolHttp HealthCheckProtocol = "http"
HealthCheckProtocolHttps HealthCheckProtocol = "https"
)
// Defines values for ImageQueryType.
const (
ImageQueryTypeBackup ImageQueryType = "backup"
@@ -265,8 +258,8 @@ const (
// Defines values for LoadBalancerRuleProtocol.
const (
LoadBalancerRuleProtocolHttp LoadBalancerRuleProtocol = "http"
LoadBalancerRuleProtocolHttps LoadBalancerRuleProtocol = "https"
Http LoadBalancerRuleProtocol = "http"
Https LoadBalancerRuleProtocol = "https"
)
// Defines values for LoadBalancerStatus.
@@ -1131,7 +1124,7 @@ type ConsoleResponse struct {
// CreateLoadBalancerRequest defines model for CreateLoadBalancerRequest.
type CreateLoadBalancerRequest struct {
// ForwardingRules The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default "HTTP" only forwarding rule.
ForwardingRules *[]ForwardingRule `json:"forwarding_rules"`
ForwardingRules *[]ForwardingRule `json:"forwarding_rules" tfsdk:"forwarding_rules"`
// HealthCheck The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules.
HealthCheck *HealthCheck `json:"health_check"`
@@ -1538,7 +1531,7 @@ type FailoverIpsResponse struct {
// ForwardingRule defines model for ForwardingRule.
type ForwardingRule struct {
// EntryProtocol The protocol that traffic must match for this load balancer to forward traffic according to this rule.
EntryProtocol LoadBalancerRuleProtocol `json:"entry_protocol"`
EntryProtocol LoadBalancerRuleProtocol `json:"entry_protocol" tfsdk:"entry_protocol"`
}
// ForwardingRulesRequest defines model for ForwardingRulesRequest.
@@ -1562,7 +1555,7 @@ type HealthCheck struct {
// | http | The health check will be performed via HTTP. |
// | https | The health check will be performed via HTTPS. |
// | both | The health check will be performed via both HTTP and HTTPS. Failing a health check on one protocol will remove the server from the pool of servers only for that protocol. |
type HealthCheckProtocol string
type HealthCheckProtocol = string
// Host defines model for Host.
type Host struct {
@@ -1867,7 +1860,7 @@ type LoadBalancer struct {
ForwardingRules *[]ForwardingRule `json:"forwarding_rules,omitempty"`
// HealthCheck The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer.
HealthCheck *HealthCheck `json:"health_check,omitempty"`
HealthCheck *HealthCheck `json:"health_check,omitempty" tfsdk:"health_check"`
// Id The ID of the load balancer.
Id *int64 `json:"id,omitempty"`
@@ -0,0 +1,104 @@
package provider
import (
"context"
"fmt"
"net/http"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/datasource"
)
var (
_ datasource.DataSource = &loadBalancerDataSource{}
_ datasource.DataSourceWithConfigure = &loadBalancerDataSource{}
)
func NewLoadBalancerDataSource() datasource.DataSource {
return &loadBalancerDataSource{}
}
type loadBalancerDataSource struct {
bc *BinarylaneClient
}
type loadBalancerDataSourceModel struct {
resources.LoadBalancerModel
}
func (d *loadBalancerDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_load_balancer"
}
func (d *loadBalancerDataSource) Configure(
_ context.Context,
req datasource.ConfigureRequest,
resp *datasource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
bc, ok := req.ProviderData.(BinarylaneClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData))
return
}
d.bc = &bc
}
func (d *loadBalancerDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
ds, err := convertResourceSchemaToDataSourceSchema(
resources.LoadBalancerResourceSchema(ctx),
AttributeConfig{
RequiredAttributes: &[]string{"id"},
},
)
if err != nil {
resp.Diagnostics.AddError("Failed to convert resource schema to data source schema", err.Error())
return
}
resp.Schema = *ds
// resp.Schema.Description = "TODO"
}
func (d *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data loadBalancerDataSourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
lbResp, err := d.bc.client.GetLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading load balancer",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
diags := SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
+462
View File
@@ -0,0 +1,462 @@
package provider
import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"time"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"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/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var (
_ resource.Resource = &loadBalancerResource{}
_ resource.ResourceWithConfigure = &loadBalancerResource{}
_ resource.ResourceWithImportState = &loadBalancerResource{}
)
func NewLoadBalancerResource() resource.Resource {
return &loadBalancerResource{}
}
type loadBalancerResource struct {
bc *BinarylaneClient
}
type loadBalancerResourceModel struct {
resources.LoadBalancerModel
}
func (r *loadBalancerResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_load_balancer"
}
func (d *loadBalancerResource) 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
}
d.bc = &bc
}
func (r *loadBalancerResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resources.LoadBalancerResourceSchema(ctx)
// resp.Schema.Description = "TODO"
// Overrides
id := resp.Schema.Attributes["id"]
resp.Schema.Attributes["id"] = &schema.Int64Attribute{
Description: id.GetDescription(),
MarkdownDescription: id.GetMarkdownDescription(),
// read only
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
}
region := resp.Schema.Attributes["region"]
resp.Schema.Attributes["region"] = &schema.StringAttribute{
Description: region.GetDescription(),
MarkdownDescription: region.GetMarkdownDescription(),
Optional: true,
Computed: false, // region is not computed, defined at creation
PlanModifiers: []planmodifier.String{ // region is not allowed to be changed
stringplanmodifier.RequiresReplace(),
},
}
}
func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data loadBalancerResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Create API call logic
forwardingRules := []binarylane.ForwardingRule{}
diags := data.LoadBalancerModel.ForwardingRules.ElementsAs(ctx, &forwardingRules, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serverIds := []int64{}
diags = data.ServerIds.ElementsAs(ctx, &serverIds, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
body := binarylane.CreateLoadBalancerRequest{
Name: data.Name.ValueString(),
ForwardingRules: &forwardingRules,
ServerIds: &serverIds,
}
if data.HealthCheck.Path.ValueStringPointer() != nil || data.HealthCheck.Protocol.ValueStringPointer() != nil {
body.HealthCheck = &binarylane.HealthCheck{
Path: data.HealthCheck.Path.ValueStringPointer(),
Protocol: data.HealthCheck.Protocol.ValueStringPointer(),
}
}
tflog.Info(ctx, fmt.Sprintf("Creating Load Balancer: name=%s", data.Name.ValueString()))
const maxRetries = 3
var lbResp *binarylane.PostLoadBalancersResponse
retryLoop:
for i := 0; i < maxRetries; i++ {
var err error
lbResp, err = r.bc.client.PostLoadBalancersWithResponse(ctx, body)
if err != nil {
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
fmt.Sprintf("Error sending request to create load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
switch lbResp.StatusCode() {
case http.StatusOK:
break retryLoop
case http.StatusInternalServerError:
if i < maxRetries-1 {
tflog.Warn(ctx, "Received 500 creating load balancer, retrying...")
time.Sleep(time.Second * 5)
continue
}
default:
tflog.Info(ctx, fmt.Sprintf("Attempted to create new load balancer: request=%+v", body))
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating load balancer",
fmt.Sprintf("Received %s creating new load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
}
// Check if retries exceeded
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Failed to create load balancer after retries",
fmt.Sprintf("Final status code: %d", lbResp.StatusCode()),
)
return
}
diags = SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
// Wait for server to be ready
var createActionId int64
for _, action := range *lbResp.JSON200.Links.Actions {
if *action.Rel == "create_load_balancer" {
createActionId = *action.Id
break
}
}
if createActionId == 0 {
resp.Diagnostics.AddError(
"Unable to wait for load balancer to be created, links.actions with rel=create_load_balancer missing from response",
fmt.Sprintf("Received %s creating new load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body))
return
}
timeLimit := time.Now().Add(time.Duration(60 * time.Second))
for {
tflog.Info(ctx, "Waiting for load balancer to be ready...")
readyResp, err := r.bc.client.GetActionsActionIdWithResponse(ctx, createActionId)
if err != nil {
resp.Diagnostics.AddError("Error waiting for load balancer to be ready", err.Error())
return
}
if readyResp.StatusCode() == http.StatusOK && readyResp.JSON200.Action.CompletedAt != nil {
tflog.Info(ctx, "Load balancer is ready")
break
}
if time.Now().After(timeLimit) {
resp.Diagnostics.AddError(
"Timed out waiting for load balancer to be ready",
fmt.Sprintf(
"Timed out waiting for load balancer to be created, as `wait_for_create` was surpassed without "+
"recieving a `completed_at` in response: name=%s, status=%s, details: %s",
data.Name.ValueString(), readyResp.Status(), readyResp.Body,
),
)
return
}
tflog.Debug(ctx, fmt.Sprintf("Waiting for load balancer to be ready: name=%s, status=%s, details: %s", data.Name.ValueString(), readyResp.Status(), readyResp.Body))
time.Sleep(time.Second * 5)
}
}
func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data loadBalancerResourceModel
// 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
lbResp, err := r.bc.client.GetLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading load balancer",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
diags := SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data loadBalancerResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Update API call logic
forwardingRules := &[]binarylane.ForwardingRule{}
diags := data.LoadBalancerModel.ForwardingRules.ElementsAs(ctx, forwardingRules, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
serverIds := &[]int64{}
diags = data.ServerIds.ElementsAs(ctx, serverIds, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("Updating Load Balancer: name=%s", data.Name.ValueString()))
body := binarylane.UpdateLoadBalancerRequest{
Name: data.Name.ValueString(),
ForwardingRules: forwardingRules,
HealthCheck: &binarylane.HealthCheck{
Path: data.HealthCheck.Path.ValueStringPointer(),
Protocol: data.HealthCheck.Protocol.ValueStringPointer(),
},
ServerIds: serverIds,
}
lbResp, err := r.bc.client.PutLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64(), body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading load balancer",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body),
)
return
}
diags = SetLoadBalancerModelState(ctx, &data.LoadBalancerModel, lbResp.JSON200.LoadBalancer)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data loadBalancerResourceModel
// 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
tflog.Debug(ctx, fmt.Sprintf("Deleting Load Balancer: name=%s", data.Name.ValueString()))
lbResp, err := r.bc.client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error deleting load balancer: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if lbResp.StatusCode() != http.StatusNoContent {
resp.Diagnostics.AddError(
"Unexpected HTTP status code deleting load balancer",
fmt.Sprintf("Received %s deleting load balancer: name=%s. Details: %s", lbResp.Status(), data.Name.ValueString(), lbResp.Body))
return
}
}
func (r *loadBalancerResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
// Import by ID
id, err := strconv.ParseInt(req.ID, 10, 32)
if err == nil {
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
return
}
// Import by name
var page int32 = 1
perPage := int32(200)
var loadBalancer binarylane.LoadBalancer
var nextPage bool = true
for nextPage { // Need to paginate because the API does not support filtering by name
params := binarylane.GetLoadBalancersParams{
Page: &page,
PerPage: &perPage,
}
lbResp, err := r.bc.client.GetLoadBalancersWithResponse(ctx, &params)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Error getting load balancer for import: name=%s", req.ID), err.Error())
return
}
if lbResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting load balancer for import",
fmt.Sprintf("Received %s reading load balancer: name=%s. Details: %s", lbResp.Status(), req.ID,
lbResp.Body))
return
}
loadBalancers := *lbResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if *lb.Name == req.ID {
loadBalancer = lb
nextPage = false
break
}
}
if lbResp.JSON200.Links == nil || lbResp.JSON200.Links.Pages == nil || lbResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
if loadBalancer.Id == nil {
resp.Diagnostics.AddError(
"Could not find load balancer by name",
fmt.Sprintf("Error finding load balancer: name=%s", req.ID),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("id"), *loadBalancer.Id)
resp.Diagnostics.Append(diags...)
}
func SetLoadBalancerModelState(ctx context.Context, data *resources.LoadBalancerModel, lb *binarylane.LoadBalancer) diag.Diagnostics {
var diags, diag diag.Diagnostics
data.Id = types.Int64Value(*lb.Id)
data.Name = types.StringValue(*lb.Name)
if lb.Region == nil {
data.Region = types.StringNull()
} else {
data.Region = types.StringValue(*lb.Region.Slug)
}
data.ServerIds, diags = types.ListValueFrom(ctx, types.Int64Type, lb.ServerIds)
if lb.HealthCheck == nil {
data.HealthCheck = resources.NewHealthCheckValueNull()
} else {
data.HealthCheck, diag = resources.NewHealthCheckValue(
resources.HealthCheckValue{}.AttributeTypes(ctx),
map[string]attr.Value{
"path": types.StringValue(*lb.HealthCheck.Path),
"protocol": types.StringValue(*lb.HealthCheck.Protocol),
},
)
diags.Append(diag...)
}
data.ForwardingRules, diag = types.ListValueFrom(ctx,
types.ObjectType{AttrTypes: resources.ForwardingRulesValue{}.AttributeTypes(ctx)},
lb.ForwardingRules,
)
diags.Append(diag...)
return diags
}
@@ -0,0 +1,159 @@
package provider
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"os"
"strings"
"terraform-provider-binarylane/internal/binarylane"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestLoadBalancerResource(t *testing.T) {
// Must assign a password to the server or Binary Lane will send emails
pw_bytes := make([]byte, 12)
_, err := rand.Read(pw_bytes)
if err != nil {
t.Errorf("Failed to generate password: %s", err)
return
}
password := base64.URLEncoding.EncodeToString(pw_bytes)
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_server" "test" {
count = 2
name = "tf-test-lb-server-${count.index}"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
wait_for_create = 60
}
resource "binarylane_load_balancer" "test" {
name = "tf-test-lb"
server_ids = [binarylane_server.test.0.id, binarylane_server.test.1.id]
forwarding_rules = [{ entry_protocol = "http" }]
}
data "binarylane_load_balancer" "test" {
depends_on = [binarylane_load_balancer.test]
id = binarylane_load_balancer.test.id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values
resource.TestCheckResourceAttrSet("binarylane_load_balancer.test", "id"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "name", "tf-test-lb"),
resource.TestCheckNoResourceAttr("binarylane_load_balancer.test", "region"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "server_ids.#", "2"),
resource.TestCheckResourceAttrPair("binarylane_load_balancer.test", "server_ids.0", "binarylane_server.test.0", "id"),
resource.TestCheckResourceAttrPair("binarylane_load_balancer.test", "server_ids.1", "binarylane_server.test.1", "id"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "forwarding_rules.#", "1"),
resource.TestCheckResourceAttr("binarylane_load_balancer.test", "forwarding_rules.0.entry_protocol", "http"),
// Verify data source values
resource.TestCheckResourceAttrPair("data.binarylane_load_balancer.test", "id", "binarylane_load_balancer.test", "id"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "name", "tf-test-lb"),
resource.TestCheckNoResourceAttr("data.binarylane_load_balancer.test", "region"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "server_ids.#", "2"),
resource.TestCheckResourceAttrPair("data.binarylane_load_balancer.test", "server_ids.0", "binarylane_server.test.0", "id"),
resource.TestCheckResourceAttrPair("data.binarylane_load_balancer.test", "server_ids.1", "binarylane_server.test.1", "id"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "forwarding_rules.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_load_balancer.test", "forwarding_rules.0.entry_protocol", "http"),
),
},
// Test import by ID
{
ResourceName: "binarylane_load_balancer.test",
ImportState: true,
ImportStateVerify: true,
},
// Test import by name
{
ResourceName: "binarylane_load_balancer.test",
ImportState: true,
ImportStateId: "tf-test-lb",
ImportStateVerify: true,
},
},
})
}
func init() {
resource.AddTestSweepers("load_balancer", &resource.Sweeper{
Name: "load_balancer",
Dependencies: []string{"server"},
F: func(_ string) error {
endpoint := os.Getenv("BINARYLANE_API_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
token := os.Getenv("BINARYLANE_API_TOKEN")
client, err := binarylane.NewClientWithAuth(
endpoint,
token,
)
if err != nil {
return fmt.Errorf("Error creating Binary Lane API client: %w", err)
}
ctx := context.Background()
var page int32 = 1
perPage := int32(200)
var nextPage bool = true
for nextPage {
params := binarylane.GetLoadBalancersParams{
Page: &page,
PerPage: &perPage,
}
lbResp, err := client.GetLoadBalancersWithResponse(ctx, &params)
if err != nil {
return fmt.Errorf("Error getting load balancers for test sweep: %w", err)
}
if lbResp.StatusCode() != http.StatusOK {
return fmt.Errorf("Unexpected status code getting load balancers for test sweep: %s", lbResp.Body)
}
loadBalancers := *lbResp.JSON200.LoadBalancers
for _, lb := range loadBalancers {
if strings.HasPrefix(*lb.Name, "tf-test-") {
lbResp, err := client.DeleteLoadBalancersLoadBalancerIdWithResponse(ctx, *lb.Id)
if err != nil {
return fmt.Errorf("Error deleting load balancer %d for test sweep: %w", *lb.Id, err)
}
if lbResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting load balancer %d in test sweep: %s", lbResp.StatusCode(), *lb.Id, lbResp.Body)
}
log.Println("Deleted load balancer during test sweep:", *lb.Id)
}
}
if lbResp.JSON200.Links == nil || lbResp.JSON200.Links.Pages == nil || lbResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
return nil
},
})
}
+17 -53
View File
@@ -5,9 +5,7 @@ import (
"os"
"terraform-provider-binarylane/internal/binarylane"
"github.com/deepmap/oapi-codegen/pkg/securityprovider"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -67,60 +65,24 @@ func (p *binarylaneProvider) Configure(ctx context.Context, req provider.Configu
return
}
if config.Endpoint.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("api_endpoint"),
"Unknown Binary Lane API endpoint",
"The provider cannot create the Binary Lane API client as there is an unknown configuration value for the "+
"Binary Lane API endpoint. Either target apply the source of the value first, set the value statically in "+
"the configuration, or use the BINARYLANE_API_ENDPOINT environment variable.",
)
}
if config.Token.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("api_token"),
"Unknown Binary Lane token",
"The provider cannot create the Binary Lane API client as there is an unknown configuration value for the "+
"Binary Lane API token. Either target apply the source of the value first, set the value statically in the "+
"configuration, or use the BINARYLANE_API_TOKEN environment variable.",
)
}
if resp.Diagnostics.HasError() {
return
}
// Default values to environment variables, but override
// with Terraform configuration value if set.
endpoint := os.Getenv("BINARYLANE_API_ENDPOINT")
token := os.Getenv("BINARYLANE_API_TOKEN")
if !config.Endpoint.IsNull() {
endpoint = config.Endpoint.ValueString()
}
if !config.Token.IsNull() {
token = config.Token.ValueString()
}
endpoint := config.Endpoint.ValueString()
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
if token == "" {
resp.Diagnostics.AddAttributeError(
path.Root("api_token"),
"Missing Binary Lane API token",
"The provider cannot create the Binary Lane API client as there is a missing or empty value for the Binary "+
"Lane API token. Set the token value in the configuration or use the BINARYLANE_API_TOKEN environment "+
"variable. If either is already set, ensure the value is not empty.",
)
}
if resp.Diagnostics.HasError() {
return
}
auth, err := securityprovider.NewSecurityProviderBearerToken(token)
if err != nil {
resp.Diagnostics.AddError("Failed to create security provider with supplied token", err.Error())
return
endpoint = os.Getenv("BINARYLANE_API_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
}
client, err := binarylane.NewClientWithResponses(endpoint, binarylane.WithRequestEditorFn(auth.Intercept))
token := config.Token.ValueString()
if token == "" {
token = os.Getenv("BINARYLANE_API_TOKEN")
}
client, err := binarylane.NewClientWithAuth(
endpoint,
token,
)
if err != nil {
resp.Diagnostics.AddError("Failed to create Binary Lane API client", err.Error())
return
@@ -147,6 +109,7 @@ func (p *binarylaneProvider) DataSources(_ context.Context) []func() datasource.
NewSshKeyDataSource,
NewVpcDataSource,
NewVpcRouteEntriesDataSource,
NewLoadBalancerDataSource,
}
}
@@ -157,5 +120,6 @@ func (p *binarylaneProvider) Resources(_ context.Context) []func() resource.Reso
NewSshKeyResource,
NewVpcResource,
NewVpcRouteEntriesResource,
NewLoadBalancerResource,
}
}
@@ -15,7 +15,7 @@ func TestServerFirewallRulesResource(t *testing.T) {
rand.Read(pw_bytes)
password := base64.URLEncoding.EncodeToString(pw_bytes)
resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
+52 -18
View File
@@ -50,6 +50,7 @@ type serverModel struct {
PublicIpv4Addresses types.List `tfsdk:"public_ipv4_addresses"`
PrivateIPv4Addresses types.List `tfsdk:"private_ipv4_addresses"`
Permalink types.String `tfsdk:"permalink"`
Password types.String `tfsdk:"password"`
}
func (d *serverResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
@@ -92,15 +93,6 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
},
}
pw := resp.Schema.Attributes["password"]
resp.Schema.Attributes["password"] = &schema.StringAttribute{
Description: pw.GetDescription(),
MarkdownDescription: pw.GetMarkdownDescription(),
Optional: pw.IsOptional(),
Computed: false, // Computed must be false to allow server to be created without password
Sensitive: true, // Mark password as sensitive
}
backups := resp.Schema.Attributes["backups"]
resp.Schema.Attributes["backups"] = &schema.BoolAttribute{
Description: backups.GetDescription(),
@@ -152,6 +144,18 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
}
// Additional attributes
pwDescription :=
"If this is provided the specified or default remote user's account password will be set to this value. " +
"Only valid if the server supports password change actions. If omitted and the server supports password " +
"change actions a random password will be generated and emailed to the account email address."
resp.Schema.Attributes["password"] = &schema.StringAttribute{
Description: pwDescription,
MarkdownDescription: pwDescription,
Optional: true, // Password optional, if not set will be emailed to user
Computed: false, // Computed must be false to allow server to be created without password
Sensitive: true, // Mark password as sensitive
}
waitDescription := "The number of seconds to wait for the server to be created, after which, a timeout error will " +
"be reported. If `wait_seconds` is left empty or set to 0, Terraform will succeed without waiting for the " +
"server creation to complete."
@@ -251,21 +255,51 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
data.Password = types.StringNull()
} else {
body.Password = data.Password.ValueStringPointer()
ctx = tflog.MaskMessageStrings(ctx, data.Password.String())
}
serverResp, err := r.bc.client.PostServersWithResponse(ctx, body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating server: name=%s", data.Name.ValueString()),
err.Error(),
)
return
const maxRetries = 3
var serverResp *binarylane.PostServersResponse
retryLoop:
for i := 0; i < maxRetries; i++ {
var err error
serverResp, err = r.bc.client.PostServersWithResponse(ctx, body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating server: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
switch serverResp.StatusCode() {
case http.StatusOK:
break retryLoop
case http.StatusInternalServerError:
if i < maxRetries-1 {
tflog.Warn(ctx, "Received 500 creating server, retrying...")
time.Sleep(time.Second * 5)
continue
}
default:
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server",
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", serverResp.Status(), data.Name.ValueString(), serverResp.Body),
)
return
}
}
// Check if retries exceeded
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server",
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", serverResp.Status(), data.Name.ValueString(), serverResp.Body))
"Failed to create server after retries",
fmt.Sprintf("Final status code: %d", serverResp.StatusCode()),
)
return
}
+81 -1
View File
@@ -1,8 +1,15 @@
package provider
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net/http"
"os"
"strings"
"terraform-provider-binarylane/internal/binarylane"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
@@ -18,7 +25,7 @@ func TestServerResource(t *testing.T) {
}
password := base64.URLEncoding.EncodeToString(pw_bytes)
resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
@@ -91,12 +98,14 @@ echo "Hello World" > /var/tmp/output.txt
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "permalink", "binarylane_server.test", "permalink"),
),
},
// Test import by ID
{
ResourceName: "binarylane_server.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "ssh_keys", "user_data", "wait_for_create", "public_ipv4_count"},
},
// Test import by name
{
ResourceName: "binarylane_server.test",
ImportState: true,
@@ -107,3 +116,74 @@ echo "Hello World" > /var/tmp/output.txt
},
})
}
func init() {
resource.AddTestSweepers("server", &resource.Sweeper{
Name: "server",
F: func(_ string) error {
endpoint := os.Getenv("BINARYLANE_API_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
token := os.Getenv("BINARYLANE_API_TOKEN")
client, err := binarylane.NewClientWithAuth(
endpoint,
token,
)
if err != nil {
return fmt.Errorf("Error creating Binary Lane API client: %w", err)
}
ctx := context.Background()
var page int32 = 1
perPage := int32(200)
nextPage := true
for nextPage {
params := binarylane.GetServersParams{
Page: &page,
PerPage: &perPage,
}
serverResp, err := client.GetServersWithResponse(ctx, &params)
if err != nil {
return fmt.Errorf("Error getting servers for test sweep: %w", err)
}
if serverResp.StatusCode() != http.StatusOK {
return fmt.Errorf("Unexpected status code getting servers in test sweep: %s", serverResp.Body)
}
servers := *serverResp.JSON200.Servers
for _, s := range servers {
if strings.HasPrefix(*s.Name, "tf-test-") {
reason := "Terraform deletion"
params := binarylane.DeleteServersServerIdParams{
Reason: &reason,
}
serverResp, err := client.DeleteServersServerIdWithResponse(ctx, *s.Id, &params)
if err != nil {
return fmt.Errorf("Error deleting server %d during test sweep: %w", *s.Id, err)
}
if serverResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting server %d in test sweep: %s", serverResp.StatusCode(), *s.Id, serverResp.Body)
}
log.Println("Deleted server during test sweep:", *s.Id)
}
}
if serverResp.JSON200.Links == nil || serverResp.JSON200.Links.Pages == nil || serverResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
return nil
},
})
}
+13 -2
View File
@@ -7,6 +7,7 @@ import (
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -59,10 +60,20 @@ func (d *sshKeyDataSource) Schema(ctx context.Context, req datasource.SchemaRequ
}
resp.Schema = *ds
// resp.Schema.Description = "TODO"
// Additional attributes
fingerprintDescription := "The fingerprint of the SSH key."
resp.Schema.Attributes["fingerprint"] = &schema.StringAttribute{
Description: fingerprintDescription,
MarkdownDescription: fingerprintDescription,
Optional: false,
Required: false,
Computed: true,
}
}
func (d *sshKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data resources.SshKeyModel
var data sshKeyModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
@@ -88,11 +99,11 @@ func (d *sshKeyDataSource) Read(ctx context.Context, req datasource.ReadRequest,
return
}
// Example data value setting
data.Id = types.Int64Value(*sshResp.JSON200.SshKey.Id)
data.Default = types.BoolValue(*sshResp.JSON200.SshKey.Default)
data.Name = types.StringValue(*sshResp.JSON200.SshKey.Name)
data.PublicKey = types.StringValue(*sshResp.JSON200.SshKey.PublicKey)
data.Fingerprint = types.StringValue(*sshResp.JSON200.SshKey.Fingerprint)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+133 -24
View File
@@ -4,8 +4,10 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"time"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -14,13 +16,14 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &sshKeyResource{}
_ resource.ResourceWithConfigure = &sshKeyResource{}
_ resource.ResourceWithImportState = &serverResource{}
_ resource.ResourceWithImportState = &sshKeyResource{}
)
func NewSshKeyResource() resource.Resource {
@@ -31,6 +34,11 @@ type sshKeyResource struct {
bc *BinarylaneClient
}
type sshKeyModel struct {
resources.SshKeyModel
Fingerprint types.String `tfsdk:"fingerprint"`
}
func (d *sshKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
@@ -74,10 +82,20 @@ func (r *sshKeyResource) Schema(ctx context.Context, req resource.SchemaRequest,
MarkdownDescription: public_key.GetMarkdownDescription(),
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
}
// Additional attributes
fingerprintDescription := "The fingerprint of the SSH key."
resp.Schema.Attributes["fingerprint"] = &schema.StringAttribute{
Description: fingerprintDescription,
MarkdownDescription: fingerprintDescription,
Optional: false,
Required: false,
Computed: true,
}
}
func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data resources.SshKeyModel
var data sshKeyModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
@@ -87,28 +105,58 @@ func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest,
}
// Create API call logic
sshResp, err := r.bc.client.PostAccountKeysWithResponse(ctx, binarylane.SshKeyRequest{
Name: data.Name.ValueString(),
Default: data.Default.ValueBoolPointer(),
PublicKey: data.PublicKey.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating SSH Key: name=%s", data.Name.ValueString()),
err.Error(),
)
return
const maxRetries = 3
var sshResp *binarylane.PostAccountKeysResponse
retryLoop:
for i := 0; i < maxRetries; i++ {
var err error
sshResp, err = r.bc.client.PostAccountKeysWithResponse(ctx, binarylane.SshKeyRequest{
Name: data.Name.ValueString(),
Default: data.Default.ValueBoolPointer(),
PublicKey: data.PublicKey.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating SSH Key: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
switch sshResp.StatusCode() {
case http.StatusOK:
break retryLoop
case http.StatusInternalServerError:
if i < maxRetries-1 {
tflog.Warn(ctx, "Received 500 creating SSH key, retrying...")
time.Sleep(time.Second * 5)
continue
}
default:
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating SSH key",
fmt.Sprintf("Received %s creating SSH key: name=%s. Details: %s", sshResp.Status(), data.Name.ValueString(), sshResp.Body),
)
return
}
}
// Check if retries exceeded
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server",
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", sshResp.Status(), data.Name.ValueString(), sshResp.Body),
"Failed to create SSH key after retries",
fmt.Sprintf("Final status code: %d", sshResp.StatusCode()),
)
return
}
// Set data values
data.Id = types.Int64Value(*sshResp.JSON200.SshKey.Id)
data.Fingerprint = types.StringValue(*sshResp.JSON200.SshKey.Fingerprint)
if resp.Diagnostics.HasError() {
return
@@ -119,7 +167,7 @@ func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest,
}
func (r *sshKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data resources.SshKeyModel
var data sshKeyModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
@@ -141,8 +189,8 @@ func (r *sshKeyResource) Read(ctx context.Context, req resource.ReadRequest, res
}
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server",
fmt.Sprintf("Received %s creating new server: name=%s. Details: %s", sshResp.Status(), data.Name.ValueString(), sshResp.Body),
"Unexpected HTTP status code getting SSH key",
fmt.Sprintf("Received %s getting SSH key: name=%s. Details: %s", sshResp.Status(), data.Name.ValueString(), sshResp.Body),
)
return
}
@@ -151,14 +199,15 @@ func (r *sshKeyResource) Read(ctx context.Context, req resource.ReadRequest, res
data.Id = types.Int64Value(*sshResp.JSON200.SshKey.Id)
data.Default = types.BoolValue(*sshResp.JSON200.SshKey.Default)
data.Name = types.StringValue(*sshResp.JSON200.SshKey.Name)
// data.PublicKey = types.StringValue(*sshResp.JSON200.SshKey.PublicKey) // don't set or it will force replacement every time
data.PublicKey = types.StringValue(*sshResp.JSON200.SshKey.PublicKey)
data.Fingerprint = types.StringValue(*sshResp.JSON200.SshKey.Fingerprint)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *sshKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data resources.SshKeyModel
var data sshKeyModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
@@ -191,7 +240,7 @@ func (r *sshKeyResource) Update(ctx context.Context, req resource.UpdateRequest,
}
func (r *sshKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data resources.SshKeyModel
var data sshKeyModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
@@ -218,7 +267,67 @@ func (r *sshKeyResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}
func (r *sshKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Retrieve import ID and save to id attribute
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
func (r *sshKeyResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
// Import by ID
id, err := strconv.ParseInt(req.ID, 10, 32)
if err == nil {
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
return
}
// Import by name
var page int32 = 1
perPage := int32(200)
var sshKey binarylane.SshKey
var nextPage bool = true
for nextPage { // Need to paginate because the API does not support filtering by fingerprint
params := binarylane.GetAccountKeysParams{
Page: &page,
PerPage: &perPage,
}
sshResp, err := r.bc.client.GetAccountKeysWithResponse(ctx, &params)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Error getting SSH key for import: fingerprint=%s", req.ID), err.Error())
return
}
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting SSH key for import",
fmt.Sprintf("Received %s getting SSH key for import: fingerprint=%s. Details: %s", sshResp.Status(), req.ID,
sshResp.Body))
return
}
sshKeys := sshResp.JSON200.SshKeys
for _, key := range sshKeys {
if *key.Fingerprint == req.ID {
sshKey = key
nextPage = false
break
}
}
if sshResp.JSON200.Links == nil || sshResp.JSON200.Links.Pages == nil || sshResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
if sshKey.Id == nil {
resp.Diagnostics.AddError(
"Could not find SSH key by fingerprint",
fmt.Sprintf("Error finding SSH key: fingerprint=%s", req.ID),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("id"), *sshKey.Id)
resp.Diagnostics.Append(diags...)
}
+112 -14
View File
@@ -1,24 +1,32 @@
package provider
import (
"context"
"crypto/ed25519"
"encoding/base64"
"fmt"
"log"
"net/http"
"os"
"strings"
"terraform-provider-binarylane/internal/binarylane"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)
func TestSshKeyResource(t *testing.T) {
publicKey := GeneratePublicKey(t)
resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_ssh_key" "test" {
name = "tf_ssh_key_resource_test"
name = "tf-test-key-resource-test"
public_key = "` + publicKey + `"
}
@@ -30,38 +38,44 @@ data "binarylane_ssh_key" "test" {
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values
resource.TestCheckResourceAttr("binarylane_ssh_key.test", "name", "tf_ssh_key_resource_test"),
resource.TestCheckResourceAttr("binarylane_ssh_key.test", "name", "tf-test-key-resource-test"),
resource.TestCheckResourceAttr("binarylane_ssh_key.test", "public_key", publicKey),
resource.TestCheckResourceAttr("binarylane_ssh_key.test", "default", "false"),
resource.TestCheckResourceAttrSet("binarylane_ssh_key.test", "id"),
// Verify data source values
resource.TestCheckResourceAttr("data.binarylane_ssh_key.test", "name", "tf_ssh_key_resource_test"),
resource.TestCheckResourceAttr("data.binarylane_ssh_key.test", "name", "tf-test-key-resource-test"),
resource.TestCheckResourceAttrSet("data.binarylane_ssh_key.test", "public_key"), // Ideally would check this is identical, but whitespace is not preserved
resource.TestCheckResourceAttr("data.binarylane_ssh_key.test", "default", "false"),
resource.TestCheckResourceAttrSet("data.binarylane_ssh_key.test", "id"),
),
},
// ImportState testing
// TODO
// {
// ResourceName: "binarylane_ssh_key.test",
// ImportState: true,
// ImportStateVerify: true,
// ImportStateVerifyIgnore: []string{}, // nothing to ignore
// },
{
ResourceName: "binarylane_ssh_key.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{}, // nothing to ignore
},
{
ResourceName: "binarylane_ssh_key.test",
ImportStateIdFunc: ImportByFingerprint,
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{}, // nothing to ignore
},
// TODO: Update and Read testing
// {
// Config: providerConfig + `
// resource "binarylane_ssh_key" "test" {
// name = "tf_ssh_key_resource_test"
// name = "tf-test-key-resource-test"
// public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJCsuosklP0T4fJcQDgkeVh7dQu+eV+vev1CfwdUkj7h test@company.internal"
// default = true
// }
// `,
// Check: resource.ComposeAggregateTestCheckFunc(
// // Verify resource values
// resource.TestCheckResourceAttr("binarylane_ssh_key.test", "name", "tf_ssh_key_resource_test"),
// resource.TestCheckResourceAttr("binarylane_ssh_key.test", "name", "tf-test-key-resource-test"),
// resource.TestCheckResourceAttr("data.binarylane_ssh_key.test", "default", "true"),
// ),
// },
@@ -73,7 +87,7 @@ data "binarylane_ssh_key" "test" {
func GeneratePublicKey(t *testing.T) string {
t.Helper()
_, pub, err := ed25519.GenerateKey(nil)
pub, _, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
@@ -81,3 +95,87 @@ func GeneratePublicKey(t *testing.T) string {
encoded := base64.StdEncoding.EncodeToString(pub)
return fmt.Sprintf("ssh-ed25519 %s test@company.internal", encoded)
}
func ImportByFingerprint(state *terraform.State) (fingerprint string, err error) {
resourceName := "binarylane_ssh_key.test"
var rawState map[string]string
for _, m := range state.Modules {
if len(m.Resources) > 0 {
if v, ok := m.Resources[resourceName]; ok {
rawState = v.Primary.Attributes
}
}
}
if rawState == nil {
return "", fmt.Errorf("resource not found: %s", resourceName)
}
return rawState["fingerprint"], nil
}
func init() {
resource.AddTestSweepers("ssh_key", &resource.Sweeper{
Name: "ssh_key",
F: func(_ string) error {
endpoint := os.Getenv("BINARYLANE_API_ENDPOINT")
if endpoint == "" {
endpoint = "https://api.binarylane.com.au/v2"
}
token := os.Getenv("BINARYLANE_API_TOKEN")
client, err := binarylane.NewClientWithAuth(
endpoint,
token,
)
if err != nil {
return fmt.Errorf("Error creating Binary Lane API client: %w", err)
}
ctx := context.Background()
var page int32 = 1
perPage := int32(200)
nextPage := true
for nextPage {
params := binarylane.GetAccountKeysParams{
Page: &page,
PerPage: &perPage,
}
keyResp, err := client.GetAccountKeysWithResponse(ctx, &params)
if err != nil {
return fmt.Errorf("Error getting SSH keys for test sweep: %w", err)
}
if keyResp.StatusCode() != http.StatusOK {
return fmt.Errorf("Unexpected status code getting SSH keys for test sweep: %s", keyResp.Body)
}
keys := keyResp.JSON200.SshKeys
for _, k := range keys {
if strings.HasPrefix(*k.Name, "tf-test-") {
keyResp, err := client.DeleteAccountKeysKeyIdWithResponse(ctx, int(*k.Id))
if err != nil {
return fmt.Errorf("Error deleting SSH key %d for test sweep: %w", *k.Id, err)
}
if keyResp.StatusCode() != http.StatusNoContent {
return fmt.Errorf("Unexpected status %d deleting SSH key %d for test sweep: %s", keyResp.StatusCode(), *k.Id, keyResp.Body)
}
log.Println("Deleted SSH key for test sweep:", *k.Id)
}
}
if keyResp.JSON200.Links == nil || keyResp.JSON200.Links.Pages == nil || keyResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
return nil
},
})
}
+11
View File
@@ -0,0 +1,11 @@
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestMain(m *testing.M) {
resource.TestMain(m)
}
+64 -44
View File
@@ -3,8 +3,8 @@ package provider
import (
"fmt"
"slices"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
d_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
r_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -24,110 +24,130 @@ func convertResourceSchemaToDataSourceSchema(rs r_schema.Schema, cfg AttributeCo
MarkdownDescription: rs.GetMarkdownDescription(),
DeprecationMessage: rs.GetDeprecationMessage(),
}
for name, attr := range rs.Attributes {
for name, attribute := range rs.Attributes {
if cfg.ExcludedAttributes != nil && slices.Contains(*cfg.ExcludedAttributes, name) {
continue
}
required := cfg.RequiredAttributes != nil && slices.Contains(*cfg.RequiredAttributes, name)
optional := cfg.OptionalAttributes != nil && slices.Contains(*cfg.OptionalAttributes, name)
switch attr.GetType() {
switch attribute.GetType() {
case types.BoolType:
ds.Attributes[name] = d_schema.BoolAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.DynamicType:
ds.Attributes[name] = d_schema.DynamicAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Float32Type:
ds.Attributes[name] = d_schema.Float32Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Float64Type:
ds.Attributes[name] = d_schema.Float64Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Int32Type:
ds.Attributes[name] = d_schema.Int32Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.Int64Type:
ds.Attributes[name] = d_schema.Int64Attribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.NumberType:
ds.Attributes[name] = d_schema.NumberAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
case types.StringType:
ds.Attributes[name] = d_schema.StringAttribute{
Description: attr.GetDescription(),
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
default:
// Feel free to to raise a PR and remove this hack
if strings.HasPrefix(attr.GetType().String(), "types.ListType") {
if listType, isList := attribute.GetType().(types.ListType); isList {
ds.Attributes[name] = d_schema.ListAttribute{
ElementType: attr.GetType().(types.ListType).ElemType,
Description: attr.GetDescription(),
ElementType: listType.ElemType,
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
}
} else {
return nil, fmt.Errorf("failed to convert resource schema attribute to data source schema attribute: name=%s, type=%s", name, attr.GetType())
continue
}
if objType, isObject := attribute.(r_schema.SingleNestedAttribute); isObject {
attributeTypes := make(map[string]attr.Type, len(objType.GetAttributes()))
for name, attribute := range objType.GetAttributes() {
attributeTypes[name] = attribute.GetType()
}
ds.Attributes[name] = d_schema.ObjectAttribute{
CustomType: objType.CustomType,
Description: attribute.GetDescription(),
Required: required,
Optional: optional,
Computed: !required,
Sensitive: attribute.IsSensitive(),
MarkdownDescription: attribute.GetMarkdownDescription(),
DeprecationMessage: attribute.GetDeprecationMessage(),
AttributeTypes: attributeTypes,
}
continue
}
return nil, fmt.Errorf("failed to convert resource schema attribute to data source schema attribute: name=%s, type=%s", name, attribute.GetType())
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
)
func TestVpcResource(t *testing.T) {
resource.Test(t, resource.TestCase{
resource.ParallelTest(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
@@ -0,0 +1,819 @@
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
package resources
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"regexp"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
func LoadBalancerResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"forwarding_rules": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"entry_protocol": schema.StringAttribute{
Required: true,
Description: "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
MarkdownDescription: "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
Validators: []validator.String{
stringvalidator.OneOf(
"http",
"https",
),
},
},
},
CustomType: ForwardingRulesType{
ObjectType: types.ObjectType{
AttrTypes: ForwardingRulesValue{}.AttributeTypes(ctx),
},
},
},
Optional: true,
Computed: true,
Description: "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule.",
MarkdownDescription: "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule.",
},
"health_check": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"path": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Leave null to accept the default '/' path.",
MarkdownDescription: "Leave null to accept the default '/' path.",
Validators: []validator.String{
stringvalidator.RegexMatches(regexp.MustCompile("/[A-Za-z0-9/.?=&+%_-]*"), ""),
},
},
"protocol": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Leave null to accept the default HTTP protocol.",
MarkdownDescription: "Leave null to accept the default HTTP protocol.",
},
},
CustomType: HealthCheckType{
ObjectType: types.ObjectType{
AttrTypes: HealthCheckValue{}.AttributeTypes(ctx),
},
},
Optional: true,
Computed: true,
Description: "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules.",
MarkdownDescription: "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules.",
},
"id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The ID of the load balancer to fetch.",
MarkdownDescription: "The ID of the load balancer to fetch.",
},
"name": schema.StringAttribute{
Required: true,
Description: "The hostname of the load balancer.",
MarkdownDescription: "The hostname of the load balancer.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"region": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Leave null to create an anycast load balancer.",
MarkdownDescription: "Leave null to create an anycast load balancer.",
},
"server_ids": schema.ListAttribute{
ElementType: types.Int64Type,
Optional: true,
Computed: true,
Description: "A list of server IDs to assign to this load balancer.",
MarkdownDescription: "A list of server IDs to assign to this load balancer.",
},
},
}
}
type LoadBalancerModel struct {
ForwardingRules types.List `tfsdk:"forwarding_rules"`
HealthCheck HealthCheckValue `tfsdk:"health_check"`
Id types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Region types.String `tfsdk:"region"`
ServerIds types.List `tfsdk:"server_ids"`
}
var _ basetypes.ObjectTypable = ForwardingRulesType{}
type ForwardingRulesType struct {
basetypes.ObjectType
}
func (t ForwardingRulesType) Equal(o attr.Type) bool {
other, ok := o.(ForwardingRulesType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t ForwardingRulesType) String() string {
return "ForwardingRulesType"
}
func (t ForwardingRulesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
entryProtocolAttribute, ok := attributes["entry_protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`entry_protocol is missing from object`)
return nil, diags
}
entryProtocolVal, ok := entryProtocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`entry_protocol expected to be basetypes.StringValue, was: %T`, entryProtocolAttribute))
}
if diags.HasError() {
return nil, diags
}
return ForwardingRulesValue{
EntryProtocol: entryProtocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewForwardingRulesValueNull() ForwardingRulesValue {
return ForwardingRulesValue{
state: attr.ValueStateNull,
}
}
func NewForwardingRulesValueUnknown() ForwardingRulesValue {
return ForwardingRulesValue{
state: attr.ValueStateUnknown,
}
}
func NewForwardingRulesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (ForwardingRulesValue, diag.Diagnostics) {
var diags diag.Diagnostics
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
ctx := context.Background()
for name, attributeType := range attributeTypes {
attribute, ok := attributes[name]
if !ok {
diags.AddError(
"Missing ForwardingRulesValue Attribute Value",
"While creating a ForwardingRulesValue value, a missing attribute value was detected. "+
"A ForwardingRulesValue must contain values for all attributes, even if null or unknown. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("ForwardingRulesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid ForwardingRulesValue Attribute Type",
"While creating a ForwardingRulesValue value, an invalid attribute value was detected. "+
"A ForwardingRulesValue must use a matching attribute type for the value. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("ForwardingRulesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("ForwardingRulesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra ForwardingRulesValue Attribute Value",
"While creating a ForwardingRulesValue value, an extra attribute value was detected. "+
"A ForwardingRulesValue must not contain values beyond the expected attribute types. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Extra ForwardingRulesValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewForwardingRulesValueUnknown(), diags
}
entryProtocolAttribute, ok := attributes["entry_protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`entry_protocol is missing from object`)
return NewForwardingRulesValueUnknown(), diags
}
entryProtocolVal, ok := entryProtocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`entry_protocol expected to be basetypes.StringValue, was: %T`, entryProtocolAttribute))
}
if diags.HasError() {
return NewForwardingRulesValueUnknown(), diags
}
return ForwardingRulesValue{
EntryProtocol: entryProtocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewForwardingRulesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) ForwardingRulesValue {
object, diags := NewForwardingRulesValue(attributeTypes, attributes)
if diags.HasError() {
// This could potentially be added to the diag package.
diagsStrings := make([]string, 0, len(diags))
for _, diagnostic := range diags {
diagsStrings = append(diagsStrings, fmt.Sprintf(
"%s | %s | %s",
diagnostic.Severity(),
diagnostic.Summary(),
diagnostic.Detail()))
}
panic("NewForwardingRulesValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t ForwardingRulesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewForwardingRulesValueNull(), nil
}
if !in.Type().Equal(t.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
return NewForwardingRulesValueUnknown(), nil
}
if in.IsNull() {
return NewForwardingRulesValueNull(), nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
return NewForwardingRulesValueMust(ForwardingRulesValue{}.AttributeTypes(ctx), attributes), nil
}
func (t ForwardingRulesType) ValueType(ctx context.Context) attr.Value {
return ForwardingRulesValue{}
}
var _ basetypes.ObjectValuable = ForwardingRulesValue{}
type ForwardingRulesValue struct {
EntryProtocol basetypes.StringValue `tfsdk:"entry_protocol"`
state attr.ValueState
}
func (v ForwardingRulesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 1)
var val tftypes.Value
var err error
attrTypes["entry_protocol"] = basetypes.StringType{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 1)
val, err = v.EntryProtocol.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["entry_protocol"] = val
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
case attr.ValueStateNull:
return tftypes.NewValue(objectType, nil), nil
case attr.ValueStateUnknown:
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
default:
panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
}
}
func (v ForwardingRulesValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v ForwardingRulesValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v ForwardingRulesValue) String() string {
return "ForwardingRulesValue"
}
func (v ForwardingRulesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"entry_protocol": basetypes.StringType{},
}
if v.IsNull() {
return types.ObjectNull(attributeTypes), diags
}
if v.IsUnknown() {
return types.ObjectUnknown(attributeTypes), diags
}
objVal, diags := types.ObjectValue(
attributeTypes,
map[string]attr.Value{
"entry_protocol": v.EntryProtocol,
})
return objVal, diags
}
func (v ForwardingRulesValue) Equal(o attr.Value) bool {
other, ok := o.(ForwardingRulesValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.EntryProtocol.Equal(other.EntryProtocol) {
return false
}
return true
}
func (v ForwardingRulesValue) Type(ctx context.Context) attr.Type {
return ForwardingRulesType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v ForwardingRulesValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"entry_protocol": basetypes.StringType{},
}
}
var _ basetypes.ObjectTypable = HealthCheckType{}
type HealthCheckType struct {
basetypes.ObjectType
}
func (t HealthCheckType) Equal(o attr.Type) bool {
other, ok := o.(HealthCheckType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t HealthCheckType) String() string {
return "HealthCheckType"
}
func (t HealthCheckType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
pathAttribute, ok := attributes["path"]
if !ok {
diags.AddError(
"Attribute Missing",
`path is missing from object`)
return nil, diags
}
pathVal, ok := pathAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`path expected to be basetypes.StringValue, was: %T`, pathAttribute))
}
protocolAttribute, ok := attributes["protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`protocol is missing from object`)
return nil, diags
}
protocolVal, ok := protocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`protocol expected to be basetypes.StringValue, was: %T`, protocolAttribute))
}
if diags.HasError() {
return nil, diags
}
return HealthCheckValue{
Path: pathVal,
Protocol: protocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewHealthCheckValueNull() HealthCheckValue {
return HealthCheckValue{
state: attr.ValueStateNull,
}
}
func NewHealthCheckValueUnknown() HealthCheckValue {
return HealthCheckValue{
state: attr.ValueStateUnknown,
}
}
func NewHealthCheckValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (HealthCheckValue, diag.Diagnostics) {
var diags diag.Diagnostics
// Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/521
ctx := context.Background()
for name, attributeType := range attributeTypes {
attribute, ok := attributes[name]
if !ok {
diags.AddError(
"Missing HealthCheckValue Attribute Value",
"While creating a HealthCheckValue value, a missing attribute value was detected. "+
"A HealthCheckValue must contain values for all attributes, even if null or unknown. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("HealthCheckValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid HealthCheckValue Attribute Type",
"While creating a HealthCheckValue value, an invalid attribute value was detected. "+
"A HealthCheckValue must use a matching attribute type for the value. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("HealthCheckValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("HealthCheckValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra HealthCheckValue Attribute Value",
"While creating a HealthCheckValue value, an extra attribute value was detected. "+
"A HealthCheckValue must not contain values beyond the expected attribute types. "+
"This is always an issue with the provider and should be reported to the provider developers.\n\n"+
fmt.Sprintf("Extra HealthCheckValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewHealthCheckValueUnknown(), diags
}
pathAttribute, ok := attributes["path"]
if !ok {
diags.AddError(
"Attribute Missing",
`path is missing from object`)
return NewHealthCheckValueUnknown(), diags
}
pathVal, ok := pathAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`path expected to be basetypes.StringValue, was: %T`, pathAttribute))
}
protocolAttribute, ok := attributes["protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`protocol is missing from object`)
return NewHealthCheckValueUnknown(), diags
}
protocolVal, ok := protocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`protocol expected to be basetypes.StringValue, was: %T`, protocolAttribute))
}
if diags.HasError() {
return NewHealthCheckValueUnknown(), diags
}
return HealthCheckValue{
Path: pathVal,
Protocol: protocolVal,
state: attr.ValueStateKnown,
}, diags
}
func NewHealthCheckValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) HealthCheckValue {
object, diags := NewHealthCheckValue(attributeTypes, attributes)
if diags.HasError() {
// This could potentially be added to the diag package.
diagsStrings := make([]string, 0, len(diags))
for _, diagnostic := range diags {
diagsStrings = append(diagsStrings, fmt.Sprintf(
"%s | %s | %s",
diagnostic.Severity(),
diagnostic.Summary(),
diagnostic.Detail()))
}
panic("NewHealthCheckValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t HealthCheckType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewHealthCheckValueNull(), nil
}
if !in.Type().Equal(t.TerraformType(ctx)) {
return nil, fmt.Errorf("expected %s, got %s", t.TerraformType(ctx), in.Type())
}
if !in.IsKnown() {
return NewHealthCheckValueUnknown(), nil
}
if in.IsNull() {
return NewHealthCheckValueNull(), nil
}
attributes := map[string]attr.Value{}
val := map[string]tftypes.Value{}
err := in.As(&val)
if err != nil {
return nil, err
}
for k, v := range val {
a, err := t.AttrTypes[k].ValueFromTerraform(ctx, v)
if err != nil {
return nil, err
}
attributes[k] = a
}
return NewHealthCheckValueMust(HealthCheckValue{}.AttributeTypes(ctx), attributes), nil
}
func (t HealthCheckType) ValueType(ctx context.Context) attr.Value {
return HealthCheckValue{}
}
var _ basetypes.ObjectValuable = HealthCheckValue{}
type HealthCheckValue struct {
Path basetypes.StringValue `tfsdk:"path"`
Protocol basetypes.StringValue `tfsdk:"protocol"`
state attr.ValueState
}
func (v HealthCheckValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 2)
var val tftypes.Value
var err error
attrTypes["path"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["protocol"] = basetypes.StringType{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 2)
val, err = v.Path.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["path"] = val
val, err = v.Protocol.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["protocol"] = val
if err := tftypes.ValidateValue(objectType, vals); err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
return tftypes.NewValue(objectType, vals), nil
case attr.ValueStateNull:
return tftypes.NewValue(objectType, nil), nil
case attr.ValueStateUnknown:
return tftypes.NewValue(objectType, tftypes.UnknownValue), nil
default:
panic(fmt.Sprintf("unhandled Object state in ToTerraformValue: %s", v.state))
}
}
func (v HealthCheckValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v HealthCheckValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v HealthCheckValue) String() string {
return "HealthCheckValue"
}
func (v HealthCheckValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"path": basetypes.StringType{},
"protocol": basetypes.StringType{},
}
if v.IsNull() {
return types.ObjectNull(attributeTypes), diags
}
if v.IsUnknown() {
return types.ObjectUnknown(attributeTypes), diags
}
objVal, diags := types.ObjectValue(
attributeTypes,
map[string]attr.Value{
"path": v.Path,
"protocol": v.Protocol,
})
return objVal, diags
}
func (v HealthCheckValue) Equal(o attr.Value) bool {
other, ok := o.(HealthCheckValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.Path.Equal(other.Path) {
return false
}
if !v.Protocol.Equal(other.Protocol) {
return false
}
return true
}
func (v HealthCheckValue) Type(ctx context.Context) attr.Type {
return HealthCheckType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v HealthCheckValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"path": basetypes.StringType{},
"protocol": basetypes.StringType{},
}
}
@@ -621,11 +621,19 @@ func (v FirewallRulesValue) String() string {
func (v FirewallRulesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
destinationAddressesVal, d := types.ListValue(types.StringType, v.DestinationAddresses.Elements())
var destinationAddressesVal basetypes.ListValue
switch {
case v.DestinationAddresses.IsUnknown():
destinationAddressesVal = types.ListUnknown(types.StringType)
case v.DestinationAddresses.IsNull():
destinationAddressesVal = types.ListNull(types.StringType)
default:
var d diag.Diagnostics
destinationAddressesVal, d = types.ListValue(types.StringType, v.DestinationAddresses.Elements())
diags.Append(d...)
}
diags.Append(d...)
if d.HasError() {
if diags.HasError() {
return types.ObjectUnknown(map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
@@ -642,11 +650,19 @@ func (v FirewallRulesValue) ToObjectValue(ctx context.Context) (basetypes.Object
}), diags
}
destinationPortsVal, d := types.ListValue(types.StringType, v.DestinationPorts.Elements())
var destinationPortsVal basetypes.ListValue
switch {
case v.DestinationPorts.IsUnknown():
destinationPortsVal = types.ListUnknown(types.StringType)
case v.DestinationPorts.IsNull():
destinationPortsVal = types.ListNull(types.StringType)
default:
var d diag.Diagnostics
destinationPortsVal, d = types.ListValue(types.StringType, v.DestinationPorts.Elements())
diags.Append(d...)
}
diags.Append(d...)
if d.HasError() {
if diags.HasError() {
return types.ObjectUnknown(map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
@@ -663,11 +679,19 @@ func (v FirewallRulesValue) ToObjectValue(ctx context.Context) (basetypes.Object
}), diags
}
sourceAddressesVal, d := types.ListValue(types.StringType, v.SourceAddresses.Elements())
var sourceAddressesVal basetypes.ListValue
switch {
case v.SourceAddresses.IsUnknown():
sourceAddressesVal = types.ListUnknown(types.StringType)
case v.SourceAddresses.IsNull():
sourceAddressesVal = types.ListNull(types.StringType)
default:
var d diag.Diagnostics
sourceAddressesVal, d = types.ListValue(types.StringType, v.SourceAddresses.Elements())
diags.Append(d...)
}
diags.Append(d...)
if d.HasError() {
if diags.HasError() {
return types.ObjectUnknown(map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
@@ -37,12 +37,6 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
Description: "The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.",
MarkdownDescription: "The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.",
},
"password": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "If this is provided the specified or default remote user's account password will be set to this value. Only valid if the server supports password change actions. If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.\n",
MarkdownDescription: "If this is provided the specified or default remote user's account password will be set to this value. Only valid if the server supports password change actions. If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.\n",
},
"port_blocking": schema.BoolAttribute{
Optional: true,
Computed: true,
@@ -96,7 +90,6 @@ type ServerModel struct {
Id types.Int64 `tfsdk:"id"`
Image types.String `tfsdk:"image"`
Name types.String `tfsdk:"name"`
Password types.String `tfsdk:"password"`
PortBlocking types.Bool `tfsdk:"port_blocking"`
Region types.String `tfsdk:"region"`
Size types.String `tfsdk:"size"`
+118 -7
View File
@@ -3,6 +3,124 @@
"name": "binarylane"
},
"resources": [
{
"name": "load_balancer",
"schema": {
"attributes": [
{
"name": "forwarding_rules",
"list_nested": {
"computed_optional_required": "computed_optional",
"nested_object": {
"attributes": [
{
"name": "entry_protocol",
"string": {
"computed_optional_required": "required",
"description": "The protocol that traffic must match for this load balancer to forward traffic according to this rule.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.OneOf(\n\"http\",\n\"https\",\n)"
}
}
]
}
}
]
},
"description": "The rules that control which traffic the load balancer will forward to servers in the pool. Leave null to accept a default \"HTTP\" only forwarding rule."
}
},
{
"name": "health_check",
"single_nested": {
"computed_optional_required": "computed_optional",
"attributes": [
{
"name": "path",
"string": {
"computed_optional_required": "computed_optional",
"description": "Leave null to accept the default '/' path.",
"validators": [
{
"custom": {
"imports": [
{
"path": "regexp"
},
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.RegexMatches(regexp.MustCompile(\"/[A-Za-z0-9/.?=\u0026+%_-]*\"), \"\")"
}
}
]
}
},
{
"name": "protocol",
"string": {
"computed_optional_required": "computed_optional",
"description": "Leave null to accept the default HTTP protocol."
}
}
],
"description": "The rules that determine which servers are considered 'healthy' and in the server pool for the load balancer. Leave this null to accept appropriate defaults based on the forwarding_rules."
}
},
{
"name": "name",
"string": {
"computed_optional_required": "required",
"description": "The hostname of the load balancer.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthAtLeast(1)"
}
}
]
}
},
{
"name": "region",
"string": {
"computed_optional_required": "computed_optional",
"description": "Leave null to create an anycast load balancer."
}
},
{
"name": "server_ids",
"list": {
"computed_optional_required": "computed_optional",
"element_type": {
"int64": {}
},
"description": "A list of server IDs to assign to this load balancer."
}
},
{
"name": "id",
"int64": {
"computed_optional_required": "computed_optional",
"description": "The ID of the load balancer to fetch."
}
}
]
}
},
{
"name": "server",
"schema": {
@@ -28,13 +146,6 @@
"description": "The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name."
}
},
{
"name": "password",
"string": {
"computed_optional_required": "computed_optional",
"description": "If this is provided the specified or default remote user's account password will be set to this value. Only valid if the server supports password change actions. If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.\n"
}
},
{
"name": "port_blocking",
"bool": {
+21 -5
View File
@@ -34,11 +34,6 @@ resources:
value if provided. Setting this to false has no effect.
image:
description: The slug of the selected operating system.
password:
description: >
If this is provided the specified or default remote user's account password will be set to this value.
Only valid if the server supports password change actions. If omitted and the server supports password
change actions a random password will be generated and emailed to the account email address.
ssh_keys:
description: >
This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as
@@ -50,6 +45,27 @@ resources:
- links
- server
- options
- password
load_balancer:
create:
path: /load_balancers
method: POST
read:
path: /load_balancers/{load_balancer_id}
method: GET
update:
path: /load_balancers/{load_balancer_id}
method: PUT
delete:
path: /load_balancers/{load_balancer_id}
method: DELETE
schema:
attributes:
aliases:
load_balancer_id: id
ignores:
- links
- load_balancer
server_firewall_rules:
create:
path: /servers/{server_id}/actions#ChangeAdvancedFirewallRules