Compare commits

..

8 Commits

Author SHA1 Message Date
Oscar Hermoso dd3aaa2d75 ⚡ allow importing of binarylane_server resources (#10) 2024-08-31 14:39:31 +08:00
Oscar Hermoso 6cafba5330 🚰 add SSH Keys attribute to servers (#9) 2024-08-31 11:48:43 +08:00
Oscar Hermoso 63632d3abb 🔨 Fix some resource attributes to be read only or required (#8) 2024-08-27 20:27:24 +08:00
Oscar Hermoso d5f45788b2 🚒 Create server_firewall_rules resource (#7) 2024-08-26 17:45:48 +08:00
Oscar Hermoso d0716f58af Add data source for VPC Route Entries, imports for VPC, fix update/tests (#6) 2024-08-25 12:47:14 +08:00
Oscar Hermoso 1f24f45af7 ☁️ Add VPC resource (#4)
* WIP commit - Adding VPC resource

* Tests passing for VPC resource

* Eject vpc_route_entries into its own resource so it can be applied later
2024-08-24 15:03:55 +08:00
Oscar Hermoso 924e785de2 Remove cached .terraform.lock.hcl file from git 2024-08-19 12:55:42 +08:00
Oscar Hermoso f4c91d2f94 🐳 Remove docker and terraform from dependabot.yml 2024-08-19 12:50:03 +08:00
53 changed files with 4113 additions and 295 deletions
-8
View File
@@ -8,11 +8,3 @@ updates:
directory: '/'
schedule:
interval: 'weekly'
- package-ecosystem: 'terraform'
directory: '/'
schedule:
interval: 'weekly'
- package-ecosystem: 'docker'
directory: '/'
schedule:
interval: 'weekly'
+2 -3
View File
@@ -27,7 +27,6 @@ go.work.sum
# Misc
tmp
# Normally, you wouldn't want to exclude lock files, but in this case, we're
# using Terraform's lock file to store the provider version, so we need to
# ignore it.
# Normally, you wouldn't want to exclude lock files, but our examples are tested
# with a locally built provider and won't be useful to others.
.terraform.lock.hcl
+97 -14
View File
@@ -63,19 +63,20 @@ tfplugingen-framework scaffold data-source \
4. Populate the template scaffolding:
Define the interfaces that the resource/data source should implement:
#### Resource
Define the interfaces that the resource should implement:
```diff
- var _ resource.Resource = (*exampleResource)(nil)
+ // Ensure the implementation satisfies the expected interfaces.
+ var (
+ _ resource.Resource = &exampleResource{}
+ _ resource.ResourceWithConfigure = &exampleResource{}
+ // _ resource.ResourceWithImportState = &exampleResource{}
+ _ resource.Resource = &exampleResource{}
+ _ resource.ResourceWithConfigure = &exampleResource{}
+ _ resource.ResourceWithImportState = &exampleResource{}
+ )
```
Pass the API client to the resource:
Add the API client to the resource:
```diff
- type exampleResource struct{}
@@ -84,6 +85,16 @@ Pass the API client to the resource:
+ }
```
Extend the resource model from the generated `resources.*Model`.
```diff
type exampleResourceModel struct {
- Id types.String `tfsdk:"id"`
+ resources.ExampleModel
+ // Add any additional fields here
}
```
Add a `Configure` method to the resource:
```diff
@@ -95,7 +106,7 @@ Add a `Configure` method to the resource:
+ bc, ok := req.ProviderData.(BinarylaneClient)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ "Unexpected Resource Configure Type",
+ fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData),
+ )
@@ -106,14 +117,12 @@ Add a `Configure` method to the resource:
+ }
```
Extend the resource model from the generated `resources.*Model`.
```diff
type exampleResourceModel struct {
- Id types.String `tfsdk:"id"`
+ *resources.ExampleModel
+ // Add any additional fields here
}
+ func (r *exampleResource) ImportState(
+ ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
+ ) {
+ resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
+ }
```
Add the new data source/resource to `provider.go`:
@@ -124,3 +133,77 @@ Add the new data source/resource to `provider.go`:
+ NewExampleResource,
}
```
#### Data Source
Define the interfaces that the data source should implement:
```diff
- var _ resource.Resource = (*exampleDataSource)(nil)
+ var (
+ _ datasource.DataSource = &exampleDataSource{}
+ _ datasource.DataSourceWithConfigure = &exampleDataSource{}
+ )
Add the API client to the data source:
```diff
- type exampleResource struct{}
+ type exampleResource struct {
+ bc *BinarylaneClient
+ }
```
Derive the data source model from the generated `resources.*Model`.
```diff
type exampleDataSourceModel struct {
- Id types.String `tfsdk:"id"`
+ resources.ExampleModel
}
```
Add a `Configure` method to the data source.
```diff
+ func (d *exampleDataSource) 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
+ }
```
Use the generated schema to define the data source schema.
```diff
func (d *exampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
- resp.Schema = schema.Schema{
- Attributes: map[string]schema.Attribute{
- "id": schema.StringAttribute{
- Computed: true,
- },
- },
- }
+ ds, err := convertResourceSchemaToDataSourceSchema(resources.ExampleResourceSchema(ctx))
+ 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"
}
```
+3 -2
View File
@@ -15,8 +15,9 @@ resource "binarylane_server" "example" {
If somehow you use this in production I would be pretty impressed.
- [x] Create/delete a server when runing locally
- [x] [Publish to Terraform Registry](https://registry.terraform.io/providers/oscarhermoso/binarylane/latest/docs/resources/server)
- [ ] (and maybe OpenTofu?)
- [x] Publish
- [x] [Terraform Registry](https://registry.terraform.io/providers/oscarhermoso/binarylane/latest/docs/resources/server)
- [x] OpenTofu
- [ ] Deploy the rest of the owl
## Parameters
+4 -1
View File
@@ -21,10 +21,13 @@ TODO
### Optional
- `backups` (Boolean) If true this will enable two daily backups for the server. Options.daily_backups will override this value if provided. Setting this to false has no effect.
- `backups` (Boolean) If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.
- `name` (String) The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.
- `password` (String) 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 (check server.password_change_supported via the servers endpoint). If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.
- `port_blocking` (Boolean) Port blocking of outgoing connections for email, SSH and Remote Desktop (TCP ports 22, 25, and 3389) is enabled by default for all new servers. If this is false port blocking will be disabled. Disabling port blocking is only available to reviewed accounts.
- `ssh_keys` (List of Number) This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.
- `user_data` (String) If provided this will be used to initialise the new server. This must be left null if the Image does not support UserData, see DistributionInfo.Features for more information.
- `vpc_id` (Number) Leave null to use default (public) network for the selected region.
### Read-Only
@@ -0,0 +1,36 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_server_firewall_rules Data Source - terraform-provider-binarylane"
subcategory: ""
description: |-
TODO
---
# binarylane_server_firewall_rules (Data Source)
TODO
<!-- schema generated by tfplugindocs -->
## Schema
### Optional
- `server_id` (Number) The ID of the server for which firewall rules should be listed.
### Read-Only
- `firewall_rules` (List of Object) A list of rules for the server. NB: that any existing rules that are not included will be removed. Submit an empty list to clear all rules. (see [below for nested schema](#nestedatt--firewall_rules))
<a id="nestedatt--firewall_rules"></a>
### Nested Schema for `firewall_rules`
Read-Only:
- `action` (String)
- `description` (String)
- `destination_addresses` (List of String)
- `destination_ports` (List of String)
- `protocol` (String)
- `source_addresses` (List of String)
+25
View File
@@ -0,0 +1,25 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_vpc Data Source - terraform-provider-binarylane"
subcategory: ""
description: |-
TODO
---
# binarylane_vpc (Data Source)
TODO
<!-- schema generated by tfplugindocs -->
## Schema
### Optional
- `id` (Number) The target vpc id.
- `ip_range` (String) A private address range that you select during creation, such as the default value of 10.240.0.0/16. Because the virtual network is dedicated to your use, you may use whatever IP address range you like.
### Read-Only
- `name` (String) A name to help identify this VPC.
+33
View File
@@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_vpc_route_entries Data Source - terraform-provider-binarylane"
subcategory: ""
description: |-
TODO
---
# binarylane_vpc_route_entries (Data Source)
TODO
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `vpc_id` (Number) The target vpc id.
### Optional
- `route_entries` (List of Object) Submit null to leave unaltered, submit an empty list to clear all route entries. It is not possible to PATCH individual route entries, to alter a route entry submit the entire list of route entries you wish to save. (see [below for nested schema](#nestedatt--route_entries))
<a id="nestedatt--route_entries"></a>
### Nested Schema for `route_entries`
Optional:
- `description` (String)
- `destination` (String)
- `router` (String)
+11 -2
View File
@@ -23,9 +23,18 @@ Provides a Binary Lane Server resource. This can be used to create and delete se
### Optional
- `backups` (Boolean) If true this will enable two daily backups for the server. Options.daily_backups will override this value if provided. Setting this to false has no effect.
- `id` (Number) The ID of the server to fetch.
- `backups` (Boolean) If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.
- `name` (String) The hostname of your server, such as vps01.yourcompany.com. If not provided, the server will be created with a random name.
- `password` (String, Sensitive) 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 (check server.password_change_supported via the servers endpoint). If omitted and the server supports password change actions a random password will be generated and emailed to the account email address.
- `port_blocking` (Boolean) Port blocking of outgoing connections for email, SSH and Remote Desktop (TCP ports 22, 25, and 3389) is enabled by default for all new servers. If this is false port blocking will be disabled. Disabling port blocking is only available to reviewed accounts.
- `public_ipv4_count` (Number) The number of public IPv4 addresses to assign to the server. If this is not provided, the server will be created with the default number of public IPv4 addresses.
- `ssh_keys` (List of Number) This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.
- `user_data` (String) If provided this will be used to initialise the new server. This must be left null if the Image does not support UserData, see DistributionInfo.Features for more information.
- `vpc_id` (Number) Leave null to use default (public) network for the selected region.
- `wait_for_create` (Number) 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.
### Read-Only
- `id` (Number) The ID of the server to fetch.
- `private_ipv4_addresses` (List of String) The private IPv4 addresses assigned to the server.
- `public_ipv4_addresses` (List of String) The public IPv4 addresses assigned to the server.
+36
View File
@@ -0,0 +1,36 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_server_firewall_rules Resource - terraform-provider-binarylane"
subcategory: ""
description: |-
TODO
---
# binarylane_server_firewall_rules (Resource)
TODO
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `firewall_rules` (Attributes List) A list of rules for the server. NB: that any existing rules that are not included will be removed. Submit an empty list to clear all rules. (see [below for nested schema](#nestedatt--firewall_rules))
- `server_id` (Number) The ID of the server for which firewall rules should be listed.
<a id="nestedatt--firewall_rules"></a>
### Nested Schema for `firewall_rules`
Required:
- `action` (String) The action to take when there is a match on this rule.
- `destination_addresses` (List of String) The destination addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.
- `protocol` (String) The protocol to match for this rule.
- `source_addresses` (List of String) The source addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.
Optional:
- `description` (String) A description to assist in identifying this rule. Commonly used to record the reason for the rule or the intent behind it, e.g. "Block access to RDP" or "Allow access from HQ".
- `destination_ports` (List of String) The destination ports to match for this rule. Leave null or empty to match on all ports.
+28
View File
@@ -0,0 +1,28 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_vpc Resource - terraform-provider-binarylane"
subcategory: ""
description: |-
TODO
---
# binarylane_vpc (Resource)
TODO
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) A name to help identify this VPC.
### Optional
- `ip_range` (String) A private address range that you select during creation, such as the default value of 10.240.0.0/16. Because the virtual network is dedicated to your use, you may use whatever IP address range you like.
### Read-Only
- `id` (Number) The target vpc id.
+36
View File
@@ -0,0 +1,36 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "binarylane_vpc_route_entries Resource - terraform-provider-binarylane"
subcategory: ""
description: |-
TODO
---
# binarylane_vpc_route_entries (Resource)
TODO
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `vpc_id` (Number) The target vpc id.
### Optional
- `route_entries` (Attributes List) Submit null to leave unaltered, submit an empty list to clear all route entries. It is not possible to PATCH individual route entries, to alter a route entry submit the entire list of route entries you wish to save. (see [below for nested schema](#nestedatt--route_entries))
<a id="nestedatt--route_entries"></a>
### Nested Schema for `route_entries`
Required:
- `destination` (String) The destination address for this route entry. This may be in CIDR format.
- `router` (String) The server that will receive traffic sent to the destination property in this VPC.
Optional:
- `description` (String) An optional description for the route.
+10
View File
@@ -0,0 +1,10 @@
# examples
In order of increasing complexity, these examples show how to use the provider.
- [./basic](./basic/main.tf) - A basic example that creates a server.
- [./ssh](./ssh/main.tf) - Create a server and registers an SSH key as a global default.
- [./cloud-init](./cloud-init/main.tf) - Create a server with a cloud-init configuration.
- [./nix](./nix/main.tf) - WIP - Create a server with a NixOS configuration.
- [./vpc](./vpc/main.tf) - WIP - Create a server with a Virtual Private Cloud.
- [./k3s](./k3s/main.tf) - WIP - Create Kubernetes cluster using k3s.
-24
View File
@@ -1,24 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/oscarhermoso/binarylane" {
version = "0.1.0"
hashes = [
"h1:M/GVWGdH+W26pw8o6O2zvzorVZvi0hZbaKCT8scoqT8=",
"zh:07c855b20ad789e78602918ce1565b5e1ac411df92b1f9f4a654ef3da77bcd1e",
"zh:09530d6532dcff2ccdf48de853d7144f0c66d7be203883461b256f52e719c06d",
"zh:2c118c8c7572f60739b4f0ac5cecbadd56636fa63f7f4e7f0c6cdec427fbd609",
"zh:3a51045b18ec6f7bd0ff642370f45b7ba936afa9d0e79ff52481a447adb99b66",
"zh:64608398b9da27f5b1efa2d164fc20e4cb924ebef6cdf80da7b4329f344dbf13",
"zh:a9808dacc075dd53774fe559862f22b9ab02a22b789dde59cff0ea3b42f37c73",
"zh:b22dc91a867a470da75f8887f432938cf5db314943c1f90203eff39ce8e24e85",
"zh:c110e831eafd8a138f9af9efc1730b8c3295cec54561918676744b0f6968498c",
"zh:cd4a98509c740beff50979c1e50c00e5b3915bf35185c89b9f306a2432b54a2c",
"zh:cf3b046ec96b5140fb6dacceead8d9ed723debcf580462ab93a5ed1578eaeab8",
"zh:d395d9a1d681980f0068e2146a4961e0a39c572ffdf131d5d5c1f3a91296a5e8",
"zh:eaf6c069dc35c9a6c97502b1b283d4a815730f70b0cfb637d26e29f2a70c2a51",
"zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
"zh:f31a253fc4c7f0d37ec2bfe2ca2e40c7c51a0db355039d56123160c97bdf7c90",
"zh:fdbadc51fc6c9ac6f2a68d5846fdb743be7b0344307c9dbf6d90252753894633",
]
}
+1 -2
View File
@@ -7,7 +7,6 @@ terraform {
}
provider "binarylane" {
# api_url = "" # Defaults to "https://api.binarylane.com"
# api_token = "" # Recommend setting with environment variable BINARYLANE_API_TOKEN
}
@@ -17,5 +16,5 @@ resource "binarylane_server" "example" {
image = "ubuntu-24.04"
size = "std-min" # 1 VPCU, 1 GB Memory, 20 GB NVME Storage, 1000 GB Data Transfer
# password will be generated automatically and mailed to the account holder
# Password will be generated automatically and mailed to the account holder
}
+1 -1
View File
@@ -14,7 +14,7 @@ terraform {
}
provider "binarylane" {
# api_url = "" # Defaults to "https://api.binarylane.com"
# api_url = "" # Defaults to "https://api.binarylane.com.au/v2"
# api_token = "" # Recommend setting with environment variable BINARYLANE_API_TOKEN
}
+1 -4
View File
@@ -6,10 +6,7 @@ terraform {
}
}
provider "binarylane" {
# api_url = "" # Defaults to "https://api.binarylane.com"
# api_token = "" # Recommend setting with environment variable BINARYLANE_API_TOKEN
}
provider "binarylane" {}
resource "binarylane_server" "example" {
name = "tf-cloud-init-example"
+37
View File
@@ -0,0 +1,37 @@
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore transient lock info files created by terraform apply
.terraform.tfstate.lock.info
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
+7
View File
@@ -0,0 +1,7 @@
# WIP - examples/k8s
This WIP example shows how to create a Kubernetes cluster on Binary Lane.
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
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash -x
exec >/tmp/k3s-agent-join-debug.log 2>&1
export K3S_TOKEN="${cluster_token}"
export K3S_URL="https://${cluster_server}:6443"
curl -sfL https://get.k3s.io | sh -s - agent \
--node-name="$(hostname -f)"
unset K3S_TOKEN
unset K3S_URL
echo "K3s Node Join Completed"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash -x
exec >/tmp/k3s-server-install-debug.log 2>&1
export INSTALL_K3S_NAME="${cluster_id}"
export K3S_TOKEN="${cluster_token}"
export K3S_KUBECONFIG_MODE="644"
curl -sfL https://get.k3s.io | sh -s - server \
--node-name="$(hostname -f)"
# --disable-cloud-controller \
# --disable servicelb \
# --disable local-storage \
# --disable traefik \
unset INSTALL_K3S_NAME
unset K3S_TOKEN
unset K3S_KUBECONFIG_MODE
echo "Installing Helm"
curl -sfL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
echo "K3s Setup Completed"
+138
View File
@@ -0,0 +1,138 @@
locals {
cluster_id = "tf-example-k8s"
custom_domain = "tf-example-k8s.internal"
}
resource "random_password" "binarylane" {
length = 18
}
resource "random_password" "cluster_token" {
length = 64
special = false
}
# SSH Key
# -------
resource "binarylane_ssh_key" "example" {
name = "tf-example-k8s"
public_key = file("~/.ssh/id_ed25519.pub")
}
# Virtual Private Cloud
# ---------------------
resource "binarylane_vpc" "example" {
name = local.cluster_id
ip_range = "10.240.0.0/16"
}
# k3s Servers
# -----------
data "cloudinit_config" "server" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = templatefile("${path.module}/k3s-server-install.sh", {
cluster_id = local.cluster_id,
cluster_token = random_password.cluster_token.result,
})
}
}
resource "binarylane_server" "server" {
count = 1
name = "${local.cluster_id}-server-${count.index + 1}"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
password = random_password.binarylane.result
ssh_keys = [binarylane_ssh_key.example.id]
vpc_id = binarylane_vpc.example.id
public_ipv4_count = 1
user_data = sensitive(data.cloudinit_config.server.rendered)
wait_for_create = 60 # Must wait for the server to be ready before creating firewall rules
}
# k3s Agents
# ----------
data "cloudinit_config" "agent" {
gzip = false
base64_encode = false
part {
content_type = "text/x-shellscript"
content = templatefile("${path.module}/k3s-agent-join.sh", {
cluster_token = random_password.cluster_token.result,
cluster_server = binarylane_server.server.0.private_ipv4_addresses.0
})
}
}
resource "binarylane_server" "agent" {
count = 2
name = "${local.cluster_id}-agent-${count.index + 1}"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
password = random_password.binarylane.result
ssh_keys = [binarylane_ssh_key.example.id]
vpc_id = binarylane_vpc.example.id
public_ipv4_count = 1
user_data = sensitive(data.cloudinit_config.agent.rendered)
wait_for_create = 60 # Must wait for the server to be ready before creating firewall rules
}
# Virtual Private Cloud Routing
# -----------------------------
resource "binarylane_vpc_route_entries" "example" {
vpc_id = binarylane_vpc.example.id
route_entries = [
# {
# description = "NAT"
# destination = "0.0.0.0/0"
# router = binarylane_server.gateway.private_ipv4_addresses.0
# }
]
}
locals {
agent_ips = flatten([for _, a in binarylane_server.agent : a.private_ipv4_addresses])
server_ips = flatten([for _, s in binarylane_server.server : s.private_ipv4_addresses])
}
resource "binarylane_server_firewall_rules" "example" {
for_each = { for server in concat(binarylane_server.server, binarylane_server.agent) : server.name => server }
server_id = each.value.id
firewall_rules = [
{
description = "K3s supervisor and Kubernetes API Server"
protocol = "tcp"
source_addresses = local.agent_ips
destination_addresses = local.server_ips
destination_ports = ["6443"]
action = "accept"
},
{
description = "Flannel VXLAN"
protocol = "udp"
source_addresses = [binarylane_vpc.example.ip_range]
destination_addresses = [binarylane_vpc.example.ip_range]
destination_ports = ["8472"]
action = "accept"
},
{
description = "Kubelet metrics"
protocol = "tcp"
source_addresses = [binarylane_vpc.example.ip_range]
destination_addresses = [binarylane_vpc.example.ip_range]
destination_ports = ["10250"]
action = "accept"
},
]
}
+9
View File
@@ -0,0 +1,9 @@
terraform {
required_providers {
binarylane = {
source = "oscarhermoso/binarylane"
}
}
}
provider "binarylane" {}
+1 -4
View File
@@ -6,10 +6,7 @@ terraform {
}
}
provider "binarylane" {
# api_url = "" # Defaults to "https://api.binarylane.com"
# api_token = "" # Recommend setting with environment variable BINARYLANE_API_TOKEN
}
provider "binarylane" {}
resource "binarylane_server" "example" {
name = "tf-nix-example"
-24
View File
@@ -1,24 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/oscarhermoso/binarylane" {
version = "0.1.1"
hashes = [
"h1:GvTza7TvwgMCegUJ/q+kmteckZ8Sc/lqsIUK2UCxxAU=",
"zh:025de19988848b2e6b8dacd61b7e99274641f32ea9a9e1a67f7dad006630916f",
"zh:0b5ce83392db3e7693a8c9a71b9c1eb60983c43f758f8a2623a6b64778e683b9",
"zh:0bf621429caa7c4468e2996f6c517178761b0492f8611603efd912797ef4da7c",
"zh:1c4db19c124efecde6910a7e9ecbdb5f3f9042685457a3828ca0c5a43da9f43d",
"zh:30fe222bc7cdfadfe3872d443a8f6db957711fa3961f62601a9b974bda995637",
"zh:3b21a79e2d69d76364a043319bd54c86080eee3016f11b73f821f22629e8b0ef",
"zh:3bbc5ea741c85267ff25bd6db1c0863548f2da2c2277bfbf30955080282fa4f0",
"zh:414887df5094afe07ce13d521c57e161301437f8891876d9d480409ffc51e5e2",
"zh:594827a9be3de7ab4fe634be3d933c5c5a0de665293feab34037da6af5355bf6",
"zh:7d73d2ca49b8a6d6f8fe23cf0adfc1fae17fcbb2b1f0a6fb7eef0b3cca5995e1",
"zh:ad0b99751f3ee17630edeed6bec065defcbb08abd377b09169a7826d0f5896ef",
"zh:c4c1ed1f41a238b50a802efc2e8fd025d5e3a1ebd0a6a0b3d84cde1b0a7eda4a",
"zh:cfb3b91d255258583d41382f3adea82af76afff7ba607daff739b951a4a27012",
"zh:d865a4a7886c8354c0636d005904ef090dfad2ea2f5ef27b1ecad2e2dcd9964b",
"zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
]
}
+24 -12
View File
@@ -6,25 +6,37 @@ terraform {
}
}
provider "binarylane" {
# api_url = "" # Defaults to "https://api.binarylane.com"
# api_token = "" # Recommend setting with environment variable BINARYLANE_API_TOKEN
}
provider "binarylane" {}
/**
* This example uses the public key in the git repository, but your SSH key will
* be different. You can use the following command to generate a new key pair:
*
* ssh-keygen -t ed25519 -C "your_email@example.com"
*
* Then, update the "public_key" attribute to point to the location of your
* generated public key.
*/
resource "binarylane_ssh_key" "example" {
name = "test-user"
public_key = file("./id_ed25519.pub") # Likely "~/.ssh/id_ed25519.pub"
default = true # Currently, the server will only register SSH keys that are added as global defaults
}
name = "tf-example-ssh"
public_key = file("./id_ed25519.pub") # Change to "~/.ssh/id_ed25519.pub"
# You also have the option to register an SSH key as a global default, for any
# new VM but it is not the recommended approach.
# default = false
}
resource "binarylane_server" "example" {
name = "tf-example"
name = "tf-example-ssh"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
ssh_keys = [
binarylane_ssh_key.example.id
]
depends_on = [binarylane_ssh_key.example] # Wait for SSH key to be created as a global default
# TODO: Extend provider with "ssh_key_id" option for explicit registraton of keys
# If you are using a global default SSH key, you need to explicitly wait for
# it to be created before creating the server.
# depends_on = [binarylane_ssh_key.example]
}
+37
View File
@@ -0,0 +1,37 @@
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
*.tfvars
*.tfvars.json
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Ignore transient lock info files created by terraform apply
.terraform.tfstate.lock.info
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
+60
View File
@@ -0,0 +1,60 @@
terraform {
required_providers {
binarylane = {
source = "oscarhermoso/binarylane"
}
}
}
provider "binarylane" {}
resource "binarylane_vpc" "example" {
name = "tf-example"
ip_range = "10.240.0.0/16"
}
# Web & NAT server
resource "binarylane_server" "web" {
name = "tf-vpc-example-web"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
vpc_id = binarylane_vpc.example.id
public_ipv4_count = 1
}
# Database server
resource "binarylane_server" "db" {
name = "tf-vpc-example-db"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
vpc_id = binarylane_vpc.example.id
public_ipv4_count = 0
}
# VPN server
resource "binarylane_server" "vpn" {
name = "tf-vpc-example-vpn"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
vpc_id = binarylane_vpc.example.id
public_ipv4_count = 1
}
resource "binarylane_vpc_route_entries" "example" {
vpc_id = binarylane_vpc.example.id
route_entries = [
{
description = "NAT"
destination = "0.0.0.0/0"
router = binarylane_server.web.private_ipv4_addresses.0
},
{
description = "VPN"
destination = "192.168.1.0/24"
router = binarylane_server.vpn.private_ipv4_addresses.0
}
]
}
+24 -3
View File
@@ -4,16 +4,37 @@ OPENAPI_FILE=$(dirname "$0")/openapi.json
# Fetch the latest OpenAPI spec
curl https://api.binarylane.com.au/reference/openapi.json --output $OPENAPI_FILE
# Remove the /v2 prefix from the paths
cat <<<$(jq '.paths |= with_entries(.key |= sub("/v2/"; "/"))' $OPENAPI_FILE) >$OPENAPI_FILE
# Set the base URL with a /v2 suffix
# Move the /v2 prefix to the base URL and remove it from the paths
cat <<<$(jq '.servers[0].url = "https://api.binarylane.com.au/v2"' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.paths |= with_entries(.key |= sub("/v2/"; "/"))' $OPENAPI_FILE) >$OPENAPI_FILE
# Terraform can't handle oneOf types, so we need to replace them with basic types
cat <<<$(jq '.components.schemas.CreateServerRequest.properties.image |= del(.oneOf) + {type:"string"}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.CreateServerRequest.properties.ssh_keys.items |= del(.oneOf) + {type:"integer"}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.paths["/account/keys/{key_id}"].delete.parameters[0].schema |= del(.oneOf) + {type:"integer"}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.paths["/account/keys/{key_id}"].put.parameters[0].schema |= del(.oneOf) + {type:"integer"}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.paths["/account/keys/{key_id}"].get.parameters[0].schema |= del(.oneOf) + {type:"integer"}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.paths["/account/keys/{key_id}"].delete.parameters[0].schema |= del(.oneOf) + {type:"integer"}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.paths["/account/keys/{key_id}"].delete.parameters[0].schema |= del(.oneOf) + {type:"integer"}' $OPENAPI_FILE) >$OPENAPI_FILE
# 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
# Override descriptions
cat <<<$(jq '.components.schemas.CreateServerRequest.properties.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 default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys."' $OPENAPI_FILE) >$OPENAPI_FILE
# Add x-oapi-codegen-extra-tags so Go structs can be reflected
## RouteEntryRequest
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.destination += {"x-oapi-codegen-extra-tags": {"tfsdk": "destination"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.description += {"x-oapi-codegen-extra-tags": {"tfsdk": "description"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.RouteEntryRequest.properties.router += {"x-oapi-codegen-extra-tags": {"tfsdk": "router"}}' $OPENAPI_FILE) >$OPENAPI_FILE
## AdvancedFirewallRule
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.description += {"x-oapi-codegen-extra-tags": {"tfsdk": "description"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.protocol += {"x-oapi-codegen-extra-tags": {"tfsdk": "protocol"}}' $OPENAPI_FILE) >$OPENAPI_FILE
cat <<<$(jq '.components.schemas.AdvancedFirewallRule.properties.action += {"x-oapi-codegen-extra-tags": {"tfsdk": "action"}}' $OPENAPI_FILE) >$OPENAPI_FILE
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
+39 -19
View File
@@ -8811,7 +8811,10 @@
"type": "string"
},
"description": "The source addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
"x-cli-format": 1
"x-cli-format": 1,
"x-oapi-codegen-extra-tags": {
"tfsdk": "source_addresses"
}
},
"destination_addresses": {
"minItems": 1,
@@ -8820,7 +8823,10 @@
"type": "string"
},
"description": "The destination addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
"x-cli-format": 2
"x-cli-format": 2,
"x-oapi-codegen-extra-tags": {
"tfsdk": "destination_addresses"
}
},
"destination_ports": {
"type": "array",
@@ -8829,7 +8835,10 @@
},
"description": "The destination ports to match for this rule. Leave null or empty to match on all ports.",
"nullable": true,
"x-cli-format": 4
"x-cli-format": 4,
"x-oapi-codegen-extra-tags": {
"tfsdk": "destination_ports"
}
},
"protocol": {
"allOf": [
@@ -8838,7 +8847,10 @@
}
],
"description": "The protocol to match for this rule.",
"x-cli-format": 3
"x-cli-format": 3,
"x-oapi-codegen-extra-tags": {
"tfsdk": "protocol"
}
},
"action": {
"allOf": [
@@ -8847,7 +8859,10 @@
}
],
"description": "The action to take when there is a match on this rule.",
"x-cli-format": 5
"x-cli-format": 5,
"x-oapi-codegen-extra-tags": {
"tfsdk": "action"
}
},
"description": {
"maxLength": 250,
@@ -8855,7 +8870,10 @@
"type": "string",
"description": "A description to assist in identifying this rule. Commonly used to record the reason for the rule or the intent behind it, e.g. \"Block access to RDP\" or \"Allow access from HQ\".",
"nullable": true,
"x-cli-format": 6
"x-cli-format": 6,
"x-oapi-codegen-extra-tags": {
"tfsdk": "description"
}
}
}
},
@@ -10082,17 +10100,10 @@
"ssh_keys": {
"type": "array",
"items": {
"oneOf": [
{
"type": "integer"
},
{
"type": "string"
}
],
"example": 5
"example": 5,
"type": "integer"
},
"description": "This may be either the SSH keys Ids or fingerprints. If this is null or not provided any SSH keys that have been marked as default will be deployed (if the operating system supports SSH keys). Submit an empty array to disable deployment of default 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 default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.",
"nullable": true
},
"options": {
@@ -12765,18 +12776,27 @@
"router": {
"minLength": 1,
"type": "string",
"description": "The server that will receive traffic sent to the destination property in this VPC."
"description": "The server that will receive traffic sent to the destination property in this VPC.",
"x-oapi-codegen-extra-tags": {
"tfsdk": "router"
}
},
"destination": {
"minLength": 1,
"type": "string",
"description": "The destination address for this route entry. This may be in CIDR format."
"description": "The destination address for this route entry. This may be in CIDR format.",
"x-oapi-codegen-extra-tags": {
"tfsdk": "destination"
}
},
"description": {
"maxLength": 250,
"type": "string",
"description": "An optional description for the route.",
"nullable": true
"nullable": true,
"x-oapi-codegen-extra-tags": {
"tfsdk": "description"
}
}
}
},
+11 -84
View File
@@ -593,22 +593,22 @@ type AdvancedFeature string
// AdvancedFirewallRule defines model for AdvancedFirewallRule.
type AdvancedFirewallRule struct {
// Action The action to take when there is a match on this rule.
Action AdvancedFirewallRuleAction `json:"action"`
Action AdvancedFirewallRuleAction `json:"action" tfsdk:"action"`
// Description A description to assist in identifying this rule. Commonly used to record the reason for the rule or the intent behind it, e.g. "Block access to RDP" or "Allow access from HQ".
Description *string `json:"description"`
Description *string `json:"description" tfsdk:"description"`
// DestinationAddresses The destination addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.
DestinationAddresses []string `json:"destination_addresses"`
DestinationAddresses []string `json:"destination_addresses" tfsdk:"destination_addresses"`
// DestinationPorts The destination ports to match for this rule. Leave null or empty to match on all ports.
DestinationPorts *[]string `json:"destination_ports"`
DestinationPorts *[]string `json:"destination_ports" tfsdk:"destination_ports"`
// Protocol The protocol to match for this rule.
Protocol AdvancedFirewallRuleProtocol `json:"protocol"`
Protocol AdvancedFirewallRuleProtocol `json:"protocol" tfsdk:"protocol"`
// SourceAddresses The source addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.
SourceAddresses []string `json:"source_addresses"`
SourceAddresses []string `json:"source_addresses" tfsdk:"source_addresses"`
}
// AdvancedFirewallRuleAction
@@ -1184,8 +1184,8 @@ type CreateServerRequest struct {
// Size The slug of the selected size.
Size string `json:"size"`
// SshKeys This may be either the SSH keys Ids or fingerprints. If this is null or not provided any SSH keys that have been marked as default will be deployed (if the operating system supports SSH keys). Submit an empty array to disable deployment of default keys.
SshKeys *[]CreateServerRequest_SshKeys_Item `json:"ssh_keys"`
// SshKeys This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.
SshKeys *[]int `json:"ssh_keys"`
// UserData If provided this will be used to initialise the new server. This must be left null if the Image does not support UserData, see DistributionInfo.Features for more information.
UserData *string `json:"user_data"`
@@ -1194,17 +1194,6 @@ type CreateServerRequest struct {
VpcId *int64 `json:"vpc_id"`
}
// CreateServerRequestSshKeys0 defines model for .
type CreateServerRequestSshKeys0 = int
// CreateServerRequestSshKeys1 defines model for .
type CreateServerRequestSshKeys1 = string
// CreateServerRequest_SshKeys_Item defines model for CreateServerRequest.ssh_keys.Item.
type CreateServerRequest_SshKeys_Item struct {
union json.RawMessage
}
// CreateServerResponse defines model for CreateServerResponse.
type CreateServerResponse struct {
Links *ActionsLinks `json:"links,omitempty"`
@@ -2349,13 +2338,13 @@ type RouteEntry struct {
// RouteEntryRequest defines model for RouteEntryRequest.
type RouteEntryRequest struct {
// Description An optional description for the route.
Description *string `json:"description"`
Description *string `json:"description" tfsdk:"description"`
// Destination The destination address for this route entry. This may be in CIDR format.
Destination string `json:"destination"`
Destination string `json:"destination" tfsdk:"destination"`
// Router The server that will receive traffic sent to the destination property in this VPC.
Router string `json:"router"`
Router string `json:"router" tfsdk:"router"`
}
// Sample defines model for Sample.
@@ -4020,68 +4009,6 @@ func (t *ChangeImage_Image) UnmarshalJSON(b []byte) error {
return err
}
// AsCreateServerRequestSshKeys0 returns the union data inside the CreateServerRequest_SshKeys_Item as a CreateServerRequestSshKeys0
func (t CreateServerRequest_SshKeys_Item) AsCreateServerRequestSshKeys0() (CreateServerRequestSshKeys0, error) {
var body CreateServerRequestSshKeys0
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromCreateServerRequestSshKeys0 overwrites any union data inside the CreateServerRequest_SshKeys_Item as the provided CreateServerRequestSshKeys0
func (t *CreateServerRequest_SshKeys_Item) FromCreateServerRequestSshKeys0(v CreateServerRequestSshKeys0) error {
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeCreateServerRequestSshKeys0 performs a merge with any union data inside the CreateServerRequest_SshKeys_Item, using the provided CreateServerRequestSshKeys0
func (t *CreateServerRequest_SshKeys_Item) MergeCreateServerRequestSshKeys0(v CreateServerRequestSshKeys0) error {
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
// AsCreateServerRequestSshKeys1 returns the union data inside the CreateServerRequest_SshKeys_Item as a CreateServerRequestSshKeys1
func (t CreateServerRequest_SshKeys_Item) AsCreateServerRequestSshKeys1() (CreateServerRequestSshKeys1, error) {
var body CreateServerRequestSshKeys1
err := json.Unmarshal(t.union, &body)
return body, err
}
// FromCreateServerRequestSshKeys1 overwrites any union data inside the CreateServerRequest_SshKeys_Item as the provided CreateServerRequestSshKeys1
func (t *CreateServerRequest_SshKeys_Item) FromCreateServerRequestSshKeys1(v CreateServerRequestSshKeys1) error {
b, err := json.Marshal(v)
t.union = b
return err
}
// MergeCreateServerRequestSshKeys1 performs a merge with any union data inside the CreateServerRequest_SshKeys_Item, using the provided CreateServerRequestSshKeys1
func (t *CreateServerRequest_SshKeys_Item) MergeCreateServerRequestSshKeys1(v CreateServerRequestSshKeys1) error {
b, err := json.Marshal(v)
if err != nil {
return err
}
merged, err := runtime.JSONMerge(t.union, b)
t.union = merged
return err
}
func (t CreateServerRequest_SshKeys_Item) MarshalJSON() ([]byte, error) {
b, err := t.union.MarshalJSON()
return b, err
}
func (t *CreateServerRequest_SshKeys_Item) UnmarshalJSON(b []byte) error {
err := t.union.UnmarshalJSON(b)
return err
}
// AsImageOptionsSshKeys0 returns the union data inside the ImageOptions_SshKeys_Item as a ImageOptionsSshKeys0
func (t ImageOptions_SshKeys_Item) AsImageOptionsSshKeys0() (ImageOptionsSshKeys0, error) {
var body ImageOptionsSshKeys0
+6
View File
@@ -143,13 +143,19 @@ func (p *binarylaneProvider) Metadata(_ context.Context, _ provider.MetadataRequ
func (p *binarylaneProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewServerDataSource,
NewServerFirewallRulesDataSource,
NewSshKeyDataSource,
NewVpcDataSource,
NewVpcRouteEntriesDataSource,
}
}
func (p *binarylaneProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewServerResource,
NewServerFirewallRulesResource,
NewSshKeyResource,
NewVpcResource,
NewVpcRouteEntriesResource,
}
}
+9 -4
View File
@@ -48,7 +48,12 @@ func (d *serverDataSource) Metadata(ctx context.Context, req datasource.Metadata
}
func (d *serverDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = *convertResourceSchemaToDataSourceSchema(ctx, resources.ServerResourceSchema(ctx))
ds, err := convertResourceSchemaToDataSourceSchema(resources.ServerResourceSchema(ctx))
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"
// Overrides
@@ -76,15 +81,15 @@ func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest,
serverResp, err := d.bc.client.GetServersServerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading server: name=%s", data.Id.String()),
fmt.Sprintf("Error reading server: name=%s, id=%s", data.Name.ValueString(), data.Id.String()),
err.Error(),
)
return
}
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
fmt.Sprintf("Unexpected HTTP status code reading server: name=%s", data.Id.String()),
fmt.Sprintf("Unexpected HTTP status code reading server: %s", serverResp.Body),
fmt.Sprintf("Unexpected HTTP status code reading server: name=%s, id=%s", data.Name.ValueString(), data.Id.String()),
string(serverResp.Body),
)
return
}
@@ -0,0 +1,100 @@
package provider
import (
"context"
"fmt"
"net/http"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var (
_ datasource.DataSource = &serverFirewallRulesDataSource{}
_ datasource.DataSourceWithConfigure = &serverFirewallRulesDataSource{}
)
func NewServerFirewallRulesDataSource() datasource.DataSource {
return &serverFirewallRulesDataSource{}
}
type serverFirewallRulesDataSource struct {
bc *BinarylaneClient
}
type serverFirewallRulesDataSourceModel struct {
resources.ServerFirewallRulesModel
}
func (d *serverFirewallRulesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_firewall_rules"
}
func (d *serverFirewallRulesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
ds, err := convertResourceSchemaToDataSourceSchema(resources.ServerFirewallRulesResourceSchema(ctx))
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 *serverFirewallRulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data serverFirewallRulesDataSourceModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
tflog.Debug(ctx, fmt.Sprintf("Reading server firewall rules: server_id=%d", data.ServerId.ValueInt64()))
fwResp, err := d.bc.client.GetServersServerIdAdvancedFirewallRulesWithResponse(ctx, data.ServerId.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading server firewall rules: server_id=%d", data.ServerId.ValueInt64()),
err.Error(),
)
return
}
if fwResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading server firewall rules",
fmt.Sprintf("Received %s reading server firewall rules: server_id=%d. Details: %s", fwResp.Status(), data.ServerId.ValueInt64(), fwResp.Body))
return
}
firewallRulesValue, diags := GetFirewallRulesState(ctx, &fwResp.JSON200.FirewallRules)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.FirewallRules = firewallRulesValue
// Save into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (d *serverFirewallRulesDataSource) 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
}
@@ -0,0 +1,305 @@
package provider
import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"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/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"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-log/tflog"
)
var (
_ resource.Resource = &serverFirewallRulesResource{}
_ resource.ResourceWithConfigure = &serverFirewallRulesResource{}
_ resource.ResourceWithImportState = &serverFirewallRulesResource{}
)
func NewServerFirewallRulesResource() resource.Resource {
return &serverFirewallRulesResource{}
}
type serverFirewallRulesResource struct {
bc *BinarylaneClient
}
type serverFirewallRulesResourceModel struct {
resources.ServerFirewallRulesModel
}
func (d *serverFirewallRulesResource) 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 *serverFirewallRulesResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_server_firewall_rules"
}
func (r *serverFirewallRulesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resources.ServerFirewallRulesResourceSchema(ctx)
resp.Schema.Description = "TODO"
// Overrides
serverId := resp.Schema.Attributes["server_id"]
resp.Schema.Attributes["server_id"] = &schema.Int64Attribute{
Description: serverId.GetDescription(),
MarkdownDescription: serverId.GetMarkdownDescription(),
Required: true, // Server ID is required to define the firewall rules
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
}
}
func (r *serverFirewallRulesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data serverFirewallRulesResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Create API call logic
firewallRules := []binarylane.AdvancedFirewallRule{}
diags := data.FirewallRules.ElementsAs(ctx, &firewallRules, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if len(firewallRules) == 0 {
return
}
tflog.Debug(ctx, fmt.Sprintf("Creating server firewall rules: server_id=%s", data.ServerId.String()))
serverResp, err := r.bc.client.PostServersServerIdActionsChangeAdvancedFirewallRulesWithResponse(
ctx,
data.ServerId.ValueInt64(),
binarylane.PostServersServerIdActionsChangeAdvancedFirewallRulesJSONRequestBody{
Type: "change_advanced_firewall_rules",
FirewallRules: firewallRules,
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating server firewall rules: server_id=%s", data.ServerId.String()),
err.Error(),
)
return
}
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating server firewall rules",
fmt.Sprintf("Received %s creating server firewall rules: server_id=%s. Details: %s", serverResp.Status(), data.ServerId.String(), serverResp.Body))
return
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *serverFirewallRulesResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data serverFirewallRulesResourceModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
tflog.Debug(ctx, fmt.Sprintf("Reading server firewall rules: server_id=%d", data.ServerId.ValueInt64()))
fwResp, err := r.bc.client.GetServersServerIdAdvancedFirewallRulesWithResponse(ctx, data.ServerId.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading server firewall rules: server_id=%d", data.ServerId.ValueInt64()),
err.Error(),
)
return
}
if fwResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading server firewall rules",
fmt.Sprintf("Received %s reading server firewall rules: server_id=%d. Details: %s", fwResp.Status(), data.ServerId.ValueInt64(), fwResp.Body))
return
}
firewallRulesValue, diags := GetFirewallRulesState(ctx, &fwResp.JSON200.FirewallRules)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.FirewallRules = firewallRulesValue
// Save into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *serverFirewallRulesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data serverFirewallRulesResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Update API call logic
firewallRules := []binarylane.AdvancedFirewallRule{}
diags := data.FirewallRules.ElementsAs(ctx, &firewallRules, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
if len(firewallRules) == 0 {
return
}
tflog.Debug(ctx, fmt.Sprintf("Updating server firewall rules: server_id=%s", data.ServerId.String()))
serverResp, err := r.bc.client.PostServersServerIdActionsChangeAdvancedFirewallRulesWithResponse(
ctx,
data.ServerId.ValueInt64(),
binarylane.PostServersServerIdActionsChangeAdvancedFirewallRulesJSONRequestBody{
Type: "change_advanced_firewall_rules",
FirewallRules: firewallRules,
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error updating server firewall rules: server_id=%s", data.ServerId.String()),
err.Error(),
)
return
}
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code updating server firewall rules",
fmt.Sprintf("Received %s updating server firewall rules: server_id=%s. Details: %s", serverResp.Status(), data.ServerId.String(), serverResp.Body))
return
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *serverFirewallRulesResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data serverFirewallRulesResourceModel
// 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 server firewall rules: server_id=%s", data.ServerId.String()))
serverResp, err := r.bc.client.PostServersServerIdActionsChangeAdvancedFirewallRulesWithResponse(
ctx,
data.ServerId.ValueInt64(),
binarylane.PostServersServerIdActionsChangeAdvancedFirewallRulesJSONRequestBody{
Type: "change_advanced_firewall_rules",
FirewallRules: []binarylane.AdvancedFirewallRule{},
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error deleting server firewall rules: server_id=%s", data.ServerId.String()),
err.Error(),
)
return
}
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code deleting server firewall rules",
fmt.Sprintf("Received %s deleting server firewall rules: server_id=%s. Details: %s", serverResp.Status(), data.ServerId.String(), serverResp.Body))
return
}
}
func GetFirewallRulesState(ctx context.Context, firewallRules *[]binarylane.AdvancedFirewallRule) (basetypes.ListValue, diag.Diagnostics) {
var diags diag.Diagnostics
firewallRulesValue := resources.FirewallRulesValue{}
var firewallRulesValues []resources.FirewallRulesValue
if *firewallRules == nil || len(*firewallRules) == 0 {
firewallRulesValues = []resources.FirewallRulesValue{}
} else {
for _, rule := range *firewallRules {
destinationAddresses, diag := types.ListValueFrom(ctx, types.StringType, rule.DestinationAddresses)
diags.Append(diag...)
var destinationPorts basetypes.ListValue
if rule.DestinationPorts != nil {
destinationPorts, diag = types.ListValueFrom(ctx, types.StringType, rule.DestinationPorts)
diags.Append(diag...)
}
sourceAddresses, diag := types.ListValueFrom(ctx, types.StringType, rule.SourceAddresses)
diags.Append(diag...)
if diags.HasError() {
return basetypes.NewListUnknown(firewallRulesValue.Type(ctx)), diags
}
ruleValue := resources.NewFirewallRulesValueMust(firewallRulesValue.AttributeTypes(ctx), map[string]attr.Value{
"description": types.StringPointerValue(rule.Description),
"action": types.StringValue(string(rule.Action)),
"protocol": types.StringValue(string(rule.Protocol)),
"destination_addresses": destinationAddresses,
"destination_ports": destinationPorts,
"source_addresses": sourceAddresses,
})
firewallRulesValues = append(firewallRulesValues, ruleValue)
}
}
firewallRulesListValue, diag := types.ListValueFrom(ctx, firewallRulesValue.Type(ctx), firewallRulesValues)
diags.Append(diag...)
if diags.HasError() {
return basetypes.NewListUnknown(firewallRulesValue.Type(ctx)), diags
}
return firewallRulesListValue, diags
}
func (r *serverFirewallRulesResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
id, err := strconv.ParseInt(req.ID, 10, 32)
if err != nil {
resp.Diagnostics.AddError(
"Error importing server firewall rules",
"Could not import server firewall rules, unexpected error (ID should be an integer): "+err.Error(),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("server_id"), id)
resp.Diagnostics.Append(diags...)
}
@@ -0,0 +1,139 @@
package provider
import (
"crypto/rand"
"encoding/base64"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)
func TestServerFirewallRulesResource(t *testing.T) {
// Must assign a password to the server or Binary Lane will send emails
pw_bytes := make([]byte, 12)
rand.Read(pw_bytes)
password := base64.URLEncoding.EncodeToString(pw_bytes)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_server" "test" {
name = "tf-test-server-fw-rules"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
wait_for_create = 60 # Must wait for the server to be ready before creating firewall rules
}
resource "binarylane_server_firewall_rules" "test" {
server_id = binarylane_server.test.id
firewall_rules = [
{
description = "Allow SSH"
protocol = "tcp"
source_addresses = ["0.0.0.0/0"]
destination_addresses = [binarylane_server.test.private_ipv4_addresses.0]
destination_ports = ["22"]
action = "accept"
}
]
}
data "binarylane_server_firewall_rules" "test" {
server_id = binarylane_server_firewall_rules.test.server_id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "server_id"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.description", "Allow SSH"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.protocol", "tcp"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.0", "0.0.0.0/0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.#", "1"),
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.0", "22"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.action", "accept"),
// Verify data source values
resource.TestCheckResourceAttrSet("data.binarylane_server_firewall_rules.test", "server_id"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.description", "Allow SSH"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.protocol", "tcp"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.0", "0.0.0.0/0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.#", "1"),
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.0"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.0", "22"),
resource.TestCheckResourceAttr("data.binarylane_server_firewall_rules.test", "firewall_rules.0.action", "accept"),
),
},
// ImportState testing
// {
// ResourceName: "binarylane_server.test",
// ImportState: true,
// ImportStateVerify: true,
// },
{
ResourceName: "binarylane_server_firewall_rules.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIdentifierAttribute: "server_id",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
resourceState := s.RootModule().Resources["binarylane_server_firewall_rules.test"]
return resourceState.Primary.Attributes["server_id"], nil
},
},
// Update and Read testing
{
Config: providerConfig + `
resource "binarylane_server" "test" {
name = "tf-test-server-fw-rules"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
wait_for_create = 60 # Must wait for the server to be ready before creating firewall rules
}
resource "binarylane_server_firewall_rules" "test" {
server_id = binarylane_server.test.id
firewall_rules = [
{
description = "Allow HTTP" # Updated description
protocol = "tcp"
source_addresses = ["0.0.0.0/0"]
destination_addresses = [binarylane_server.test.private_ipv4_addresses.0]
destination_ports = ["80"] # Updated port
action = "accept"
},
]
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values updated
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "server_id"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.description", "Allow HTTP"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.protocol", "tcp"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.source_addresses.0", "0.0.0.0/0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.#", "1"),
resource.TestCheckResourceAttrSet("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_addresses.0"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.#", "1"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.destination_ports.0", "80"),
resource.TestCheckResourceAttr("binarylane_server_firewall_rules.test", "firewall_rules.0.action", "accept"),
),
},
// Delete testing automatically occurs in TestCase
},
})
}
+177 -19
View File
@@ -4,16 +4,24 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"time"
"github.com/hashicorp/terraform-plugin-framework-validators/int32validator"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"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/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int32default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
@@ -36,7 +44,10 @@ type serverResource struct {
type serverModel struct {
resources.ServerModel
WaitForCreateSeconds int32 `tfsdk:"wait_for_create"`
WaitForCreateSeconds types.Int32 `tfsdk:"wait_for_create"`
PublicIpv4Count types.Int32 `tfsdk:"public_ipv4_count"`
PublicIpv4Addresses types.List `tfsdk:"public_ipv4_addresses"`
PrivateIPv4Addresses types.List `tfsdk:"private_ipv4_addresses"`
}
func (d *serverResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
@@ -47,7 +58,7 @@ func (d *serverResource) Configure(_ context.Context, req resource.ConfigureRequ
bc, ok := req.ProviderData.(BinarylaneClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData),
)
@@ -66,6 +77,19 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
resp.Schema.Description = "Provides a Binary Lane Server resource. This can be used to create and delete servers."
// 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(),
},
}
pw := resp.Schema.Attributes["password"]
resp.Schema.Attributes["password"] = &schema.StringAttribute{
Description: pw.GetDescription(),
@@ -79,8 +103,8 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
resp.Schema.Attributes["backups"] = &schema.BoolAttribute{
Description: backups.GetDescription(),
MarkdownDescription: backups.GetMarkdownDescription(),
Optional: backups.IsComputed(),
Computed: backups.IsOptional(),
Optional: backups.IsOptional(),
Computed: backups.IsComputed(),
Default: booldefault.StaticBool(false), // Add default to backups
}
@@ -88,10 +112,40 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
resp.Schema.Attributes["user_data"] = &schema.StringAttribute{
Description: user_data.GetDescription(),
MarkdownDescription: user_data.GetMarkdownDescription(),
Optional: user_data.IsComputed(),
Computed: user_data.IsOptional(),
Optional: true, // Optional as not all servers have an initialization script
Computed: false, // User defined
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.RequiresReplace(), // If user changes init script, assume they want to replace server
},
}
vpcId := resp.Schema.Attributes["vpc_id"]
resp.Schema.Attributes["vpc_id"] = &schema.Int64Attribute{
Description: vpcId.GetDescription(),
MarkdownDescription: vpcId.GetMarkdownDescription(),
Optional: vpcId.IsOptional(),
Computed: false, // VPC ID is not computed, defined at creation
}
portBlocking := resp.Schema.Attributes["port_blocking"]
resp.Schema.Attributes["port_blocking"] = &schema.BoolAttribute{
Description: portBlocking.GetDescription(),
MarkdownDescription: portBlocking.GetMarkdownDescription(),
Optional: portBlocking.IsOptional(),
Computed: portBlocking.IsComputed(),
Default: booldefault.StaticBool(true), // Add default to port_blocking
}
sshKeys := resp.Schema.Attributes["ssh_keys"]
resp.Schema.Attributes["ssh_keys"] = &schema.ListAttribute{
ElementType: types.Int64Type,
Description: sshKeys.GetMarkdownDescription(),
MarkdownDescription: sshKeys.GetDescription(),
Optional: sshKeys.IsOptional(),
Computed: false, // SSH keys are not computed, defined at creation
PlanModifiers: []planmodifier.List{listplanmodifier.RequiresReplace()}, // Cannot update SSH keys with API
Validators: []validator.List{
listvalidator.ValueInt64sAre(int64validator.AtLeast(1)),
},
}
@@ -103,8 +157,51 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
Description: waitDescription,
MarkdownDescription: waitDescription,
Optional: true,
Computed: false,
}
publicIpv4CountDescription := "The number of public IPv4 addresses to assign to the server. If this is not provided, the " +
"server will be created with the default number of public IPv4 addresses."
resp.Schema.Attributes["public_ipv4_count"] = &schema.Int32Attribute{
Description: publicIpv4CountDescription,
MarkdownDescription: publicIpv4CountDescription,
Optional: true,
Computed: true,
Default: int32default.StaticInt32(0),
Default: int32default.StaticInt32(1),
Validators: []validator.Int32{
int32validator.AtLeast(0),
},
// PlanModifiers: []planmodifier.Int32{
// int32planmodifier.UseStateForUnknown(),
// },
}
publicIpv4AddressesDescription := "The public IPv4 addresses assigned to the server."
resp.Schema.Attributes["public_ipv4_addresses"] = &schema.ListAttribute{
Description: publicIpv4AddressesDescription,
MarkdownDescription: publicIpv4AddressesDescription,
ElementType: types.StringType,
// read only
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
}
privateIpv4AddressesDescription := "The private IPv4 addresses assigned to the server."
resp.Schema.Attributes["private_ipv4_addresses"] = &schema.ListAttribute{
Description: privateIpv4AddressesDescription,
MarkdownDescription: privateIpv4AddressesDescription,
ElementType: types.StringType,
// read only
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
}
}
@@ -121,12 +218,24 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
// Create API call logic
tflog.Debug(ctx, fmt.Sprintf("Creating server: name=%s", data.Name.ValueString()))
sshKeys := []int{}
diags = data.SshKeys.ElementsAs(ctx, &sshKeys, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
body := binarylane.CreateServerRequest{
Name: data.Name.ValueStringPointer(),
Image: data.Image.ValueString(),
Region: data.Region.ValueString(),
Size: data.Size.ValueString(),
UserData: data.UserData.ValueStringPointer(),
VpcId: data.VpcId.ValueInt64Pointer(),
SshKeys: &sshKeys,
Options: &binarylane.SizeOptionsRequest{
Ipv4Addresses: data.PublicIpv4Count.ValueInt32Pointer(),
},
}
if data.Password.IsNull() {
@@ -157,11 +266,28 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
data.Region = types.StringValue(*serverResp.JSON200.Server.Region.Slug)
data.Size = types.StringValue(*serverResp.JSON200.Server.Size.Slug)
data.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
data.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
data.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
var publicIpv4Addresses []string
var privateIpv4Addresses []string
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
if v4address.Type == "public" {
publicIpv4Addresses = append(publicIpv4Addresses, v4address.IpAddress)
} else {
privateIpv4Addresses = append(privateIpv4Addresses, v4address.IpAddress)
}
}
data.PublicIpv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, publicIpv4Addresses)
resp.Diagnostics.Append(diags...)
data.PrivateIPv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, privateIpv4Addresses)
resp.Diagnostics.Append(diags...)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if data.WaitForCreateSeconds <= 0 {
if data.WaitForCreateSeconds.ValueInt32() <= 0 {
return
}
@@ -180,7 +306,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
return
}
timeLimit := time.Now().Add(time.Duration(data.WaitForCreateSeconds) * time.Second)
timeLimit := time.Now().Add(time.Duration(data.WaitForCreateSeconds.ValueInt32()) * time.Second)
for {
tflog.Info(ctx, "Waiting for server to be ready...")
@@ -213,18 +339,19 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
var data serverModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
diags := req.State.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
tflog.Debug(ctx, fmt.Sprintf("Reading server: name=%s", data.Id.String()))
tflog.Debug(ctx, fmt.Sprintf("Reading server: id=%s, name=%s", data.Id.String(), data.Name.ValueString()))
serverResp, err := r.bc.client.GetServersServerIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading server: name=%s", data.Id.String()),
fmt.Sprintf("Error reading server: id=%s, name=%s", data.Id.String(), data.Name.ValueString()),
err.Error(),
)
return
@@ -232,8 +359,9 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading server",
fmt.Sprintf("Received %s reading server: name=%s. Details: %s", serverResp.Status(), data.Id.String(), serverResp.Body))
fmt.Sprintf("Unexpected HTTP status code %s reading server: name=%s, id=%s", serverResp.Status(), data.Name.ValueString(), data.Id.String()),
string(serverResp.Body),
)
return
}
@@ -242,6 +370,24 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res
data.Image = types.StringValue(*serverResp.JSON200.Server.Image.Slug)
data.Region = types.StringValue(*serverResp.JSON200.Server.Region.Slug)
data.Size = types.StringValue(*serverResp.JSON200.Server.Size.Slug)
data.Backups = types.BoolValue(serverResp.JSON200.Server.NextBackupWindow != nil)
data.PortBlocking = types.BoolValue(serverResp.JSON200.Server.Networks.PortBlocking)
data.VpcId = types.Int64PointerValue(serverResp.JSON200.Server.VpcId)
var publicIpv4Addresses []string
var privateIpv4Addresses []string
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
if v4address.Type == "public" {
publicIpv4Addresses = append(publicIpv4Addresses, v4address.IpAddress)
} else {
privateIpv4Addresses = append(privateIpv4Addresses, v4address.IpAddress)
}
}
data.PublicIpv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, publicIpv4Addresses)
resp.Diagnostics.Append(diags...)
data.PrivateIPv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, privateIpv4Addresses)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
@@ -303,7 +449,19 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}
// func (r *serverResource) 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 *serverResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
id, err := strconv.ParseInt(req.ID, 10, 32)
if err != nil {
resp.Diagnostics.AddError(
"Error importing server",
"Could not import server, unexpected error (ID should be an integer): "+err.Error(),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
}
+42 -5
View File
@@ -11,7 +11,11 @@ import (
func TestServerResource(t *testing.T) {
// Must assign a password to the server or Binary Lane will send emails
pw_bytes := make([]byte, 12)
rand.Read(pw_bytes)
_, err := rand.Read(pw_bytes)
if err != nil {
t.Errorf("Failed to generate password: %s", err)
return
}
password := base64.URLEncoding.EncodeToString(pw_bytes)
resource.Test(t, resource.TestCase{
@@ -20,16 +24,36 @@ func TestServerResource(t *testing.T) {
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_vpc" "test" {
name = "tf-test-server-resource"
ip_range = "10.240.0.0/16"
}
resource "binarylane_ssh_key" "test" {
name = "tf-test-server-resource"
public_key = "` + GeneratePublicKey(t) + `"
default = true
}
resource "binarylane_ssh_key" "unused" {
name = "tf-test-server-resource-unused"
public_key = "` + GeneratePublicKey(t) + `"
default = true
}
resource "binarylane_server" "test" {
name = "tf-server-resource-test"
name = "tf-test-server-resource"
region = "per"
image = "debian-12"
size = "std-min"
password = "` + password + `"
vpc_id = binarylane_vpc.test.id
public_ipv4_count = 0
user_data = <<EOT
#cloud-config
echo "Hello World" > /var/tmp/output.txt
EOT
ssh_keys = [binarylane_ssh_key.test.id]
}
data "binarylane_server" "test" {
@@ -41,23 +65,36 @@ data "binarylane_server" "test" {
Check: resource.ComposeAggregateTestCheckFunc(
// Verify resource values
resource.TestCheckResourceAttrSet("binarylane_server.test", "id"),
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-server-resource-test"),
resource.TestCheckResourceAttr("binarylane_server.test", "name", "tf-test-server-resource"),
resource.TestCheckResourceAttr("binarylane_server.test", "region", "per"),
resource.TestCheckResourceAttr("binarylane_server.test", "image", "debian-12"),
resource.TestCheckResourceAttr("binarylane_server.test", "size", "std-min"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "vpc_id"),
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv4_count", "0"),
resource.TestCheckResourceAttr("binarylane_server.test", "password", password),
resource.TestCheckResourceAttr("binarylane_server.test", "user_data", `#cloud-config
echo "Hello World" > /var/tmp/output.txt
`),
resource.TestCheckResourceAttr("binarylane_server.test", "public_ipv4_addresses.#", "0"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "private_ipv4_addresses.0"),
resource.TestCheckResourceAttr("binarylane_server.test", "port_blocking", "true"),
resource.TestCheckResourceAttr("binarylane_server.test", "ssh_keys.#", "1"), // Only the test SSH key should be registered
resource.TestCheckResourceAttrPair("binarylane_server.test", "ssh_keys.0", "binarylane_ssh_key.test", "id"),
// Verify data source values
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "id"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "name", "tf-server-resource-test"),
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "id", "binarylane_server.test", "id"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "name", "tf-test-server-resource"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "region", "per"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "image", "debian-12"),
resource.TestCheckResourceAttr("data.binarylane_server.test", "size", "std-min"),
),
},
{
ResourceName: "binarylane_server.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "ssh_keys", "user_data", "wait_for_create", "public_ipv4_count"},
},
},
})
}
+6 -1
View File
@@ -47,7 +47,12 @@ func (d *sshKeyDataSource) Metadata(ctx context.Context, req datasource.Metadata
}
func (d *sshKeyDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = *convertResourceSchemaToDataSourceSchema(ctx, resources.SshKeyResourceSchema(ctx))
ds, err := convertResourceSchemaToDataSourceSchema(resources.SshKeyResourceSchema(ctx))
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"
}
+1 -1
View File
@@ -38,7 +38,7 @@ func (d *sshKeyResource) Configure(_ context.Context, req resource.ConfigureRequ
bc, ok := req.ProviderData.(BinarylaneClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData),
)
+19 -3
View File
@@ -1,12 +1,16 @@
package provider
import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestSshKeyResource(t *testing.T) {
publicKey := GeneratePublicKey(t)
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
@@ -15,7 +19,7 @@ func TestSshKeyResource(t *testing.T) {
Config: providerConfig + `
resource "binarylane_ssh_key" "test" {
name = "tf_ssh_key_resource_test"
public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJCsuosklP0T4fJcQDgkeVh7dQu+eV+vev1CfwdUkj7h test@company.internal"
public_key = "` + publicKey + `"
}
data "binarylane_ssh_key" "test" {
@@ -27,13 +31,13 @@ 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", "public_key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJCsuosklP0T4fJcQDgkeVh7dQu+eV+vev1CfwdUkj7h test@company.internal"),
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", "public_key", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJCsuosklP0T4fJcQDgkeVh7dQu+eV+vev1CfwdUkj7h test@company.internal"),
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"),
),
@@ -65,3 +69,15 @@ data "binarylane_ssh_key" "test" {
},
})
}
func GeneratePublicKey(t *testing.T) string {
t.Helper()
_, pub, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
encoded := base64.StdEncoding.EncodeToString(pub)
return fmt.Sprintf("ssh-ed25519 %s test@company.internal", encoded)
}
+18 -8
View File
@@ -1,15 +1,15 @@
package provider
import (
"context"
"fmt"
"strings"
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"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
func convertResourceSchemaToDataSourceSchema(ctx context.Context, rs r_schema.Schema) *d_schema.Schema {
func convertResourceSchemaToDataSourceSchema(rs r_schema.Schema) (*d_schema.Schema, error) {
ds := d_schema.Schema{
Attributes: make(map[string]d_schema.Attribute, len(rs.Attributes)),
Blocks: make(map[string]d_schema.Block),
@@ -92,12 +92,22 @@ func convertResourceSchemaToDataSourceSchema(ctx context.Context, rs r_schema.Sc
DeprecationMessage: attr.GetDeprecationMessage(),
}
default:
tflog.Error(ctx, "Failed to convert resource schema attribute to data source schema attribute", map[string]interface{}{
"attribute": name,
"type": attr.GetType(),
})
// Feel free to to raise a PR and remove this hack
if strings.HasPrefix(attr.GetType().String(), "types.ListType") {
ds.Attributes[name] = d_schema.ListAttribute{
ElementType: attr.GetType().(types.ListType).ElemType,
Description: attr.GetDescription(),
Optional: attr.IsOptional(),
Computed: true,
Sensitive: attr.IsSensitive(),
MarkdownDescription: attr.GetMarkdownDescription(),
DeprecationMessage: attr.GetDeprecationMessage(),
}
} else {
return nil, fmt.Errorf("failed to convert resource schema attribute to data source schema attribute: name=%s, type=%s", name, attr.GetType())
}
}
}
return &ds
return &ds, nil
}
+93
View File
@@ -0,0 +1,93 @@
package provider
import (
"context"
"fmt"
"net/http"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var (
_ datasource.DataSource = &sshKeyDataSource{}
_ datasource.DataSourceWithConfigure = &sshKeyDataSource{}
)
func NewVpcDataSource() datasource.DataSource {
return &vpcDataSource{}
}
type vpcDataSource struct {
bc *BinarylaneClient
}
type vpcDataSourceModel struct {
resources.VpcModel
}
func (d *vpcDataSource) 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 *vpcDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vpc"
}
func (d *vpcDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
ds, err := convertResourceSchemaToDataSourceSchema(resources.VpcResourceSchema(ctx))
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 *vpcDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data vpcDataSourceModel
// Read Terraform configuration data into the model
diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
vpcResp, err := d.bc.client.GetVpcsVpcIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading VPC: id=%d", data.Id.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading VPC",
fmt.Sprintf("Received %s reading VPC: id=%d. Details: %s", vpcResp.Status(), data.Id.ValueInt64(), vpcResp.Body))
return
}
data.IpRange = types.StringValue(*vpcResp.JSON200.Vpc.IpRange)
data.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
+231
View File
@@ -0,0 +1,231 @@
package provider
import (
"context"
"fmt"
"net/http"
"reflect"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"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/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
var (
_ resource.Resource = &vpcResource{}
_ resource.ResourceWithConfigure = &vpcResource{}
_ resource.ResourceWithImportState = &vpcResource{}
)
func NewVpcResource() resource.Resource {
return &vpcResource{}
}
type vpcResource struct {
bc *BinarylaneClient
}
type vpcResourceModel struct {
resources.VpcModel
}
func (r *vpcResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
bc, ok := req.ProviderData.(BinarylaneClient)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *BinarylaneClient, got: %T.", req.ProviderData))
return
}
r.bc = &bc
}
func (r *vpcResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vpc"
}
func (r *vpcResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resources.VpcResourceSchema(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,
}
}
func (r *vpcResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data vpcResourceModel
// Read Terraform plan data into the model
diags := req.Plan.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create API call logic
tflog.Debug(ctx, fmt.Sprintf("Creating VPC: name=%s", data.Name.ValueString()))
body := binarylane.CreateVpcRequest{
Name: data.Name.ValueString(),
IpRange: data.IpRange.ValueStringPointer(),
}
vpcResp, err := r.bc.client.PostVpcsWithResponse(ctx, body)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating VPC: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating VPC",
fmt.Sprintf("Received %s creating new VPC: name=%s. Details: %s", vpcResp.Status(), data.Name.ValueString(), vpcResp.Body))
return
}
data.Id = types.Int64Value(*vpcResp.JSON200.Vpc.Id)
data.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *vpcResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data vpcResourceModel
// 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
vpcResp, err := r.bc.client.GetVpcsVpcIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading VPC: id=%d", data.Id.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading VPC",
fmt.Sprintf("Received %s reading VPC: id=%d. Details: %s", vpcResp.Status(), data.Id.ValueInt64(), vpcResp.Body))
return
}
data.Id = types.Int64Value(*vpcResp.JSON200.Vpc.Id)
data.IpRange = types.StringValue(*vpcResp.JSON200.Vpc.IpRange)
data.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *vpcResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state vpcResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Update API call logic
patchRequest := binarylane.PatchVpcRequest{}
if !plan.Name.Equal(state.Name) {
patchRequest.Name = plan.Name.ValueStringPointer()
}
if reflect.ValueOf(plan).NumField() == 0 {
return
}
tflog.Debug(ctx, fmt.Sprintf("Updating VPC: id=%d", state.Id.ValueInt64()))
vpcResp, err := r.bc.client.PatchVpcsVpcIdWithResponse(ctx, state.Id.ValueInt64(), patchRequest)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error updating VPC: id=%d", state.Id.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code updating VPC",
fmt.Sprintf("Received %s updating VPC: id=%d. Details: %s", vpcResp.Status(), state.Id.ValueInt64(), vpcResp.Body))
return
}
state.Id = types.Int64Value(*vpcResp.JSON200.Vpc.Id)
state.Name = types.StringValue(*vpcResp.JSON200.Vpc.Name)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *vpcResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data vpcResourceModel
// 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 VPC: id=%d", data.Id.ValueInt64()))
vpcResp, err := r.bc.client.DeleteVpcsVpcIdWithResponse(ctx, data.Id.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error deleting VPC: id=%d", data.Id.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusNoContent {
resp.Diagnostics.AddError(
"Unexpected HTTP status code deleting VPC",
fmt.Sprintf("Received %s deleting VPC: id=%d. Details: %s", vpcResp.Status(), data.Id.ValueInt64(), vpcResp.Body))
return
}
}
func (r *vpcResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, err := strconv.ParseInt(req.ID, 10, 64)
if err != nil {
resp.Diagnostics.AddError(
"Error importing VPC",
fmt.Sprintf("Could not import VPC, unable to parse ID (should be an integer): %s ", err.Error()),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("id"), id)
resp.Diagnostics.Append(diags...)
}
+116
View File
@@ -0,0 +1,116 @@
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
)
func TestVpcResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Create and Read testing
{
Config: providerConfig + `
resource "binarylane_vpc" "test" {
name = "tf-test-vpc"
ip_range = "10.240.0.0/16"
}
data "binarylane_vpc" "test" {
depends_on = [binarylane_vpc.test]
id = binarylane_vpc.test.id
}
resource "binarylane_vpc_route_entries" "test" {
vpc_id = binarylane_vpc.test.id
route_entries = [
{
description = "test"
destination = "0.0.0.0/0"
router = "10.240.0.1"
}
]
}
data "binarylane_vpc_route_entries" "test" {
vpc_id = binarylane_vpc_route_entries.test.vpc_id
}
`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify binarylane_vpc resource values
resource.TestCheckResourceAttr("binarylane_vpc.test", "name", "tf-test-vpc"),
resource.TestCheckResourceAttr("binarylane_vpc.test", "ip_range", "10.240.0.0/16"),
resource.TestCheckResourceAttrSet("binarylane_vpc.test", "id"),
// Verify binarylane_vpc data source values
resource.TestCheckResourceAttr("data.binarylane_vpc.test", "name", "tf-test-vpc"),
resource.TestCheckResourceAttr("data.binarylane_vpc.test", "ip_range", "10.240.0.0/16"),
resource.TestCheckResourceAttrSet("data.binarylane_vpc.test", "id"),
// Verify binarylane_vpc_route_entries resource values
resource.TestCheckResourceAttrSet("binarylane_vpc_route_entries.test", "vpc_id"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.#", "1"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.destination", "0.0.0.0/0"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.router", "10.240.0.1"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.description", "test"),
// Verify binarylane_vpc_route_entries data source values
resource.TestCheckResourceAttrSet("data.binarylane_vpc_route_entries.test", "vpc_id"),
resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.#", "1"),
resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.0.destination", "0.0.0.0/0"),
resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.0.router", "10.240.0.1"),
resource.TestCheckResourceAttr("data.binarylane_vpc_route_entries.test", "route_entries.0.description", "test"),
),
},
// ImportState testing
{
ResourceName: "binarylane_vpc.test",
ImportState: true,
ImportStateVerify: true,
},
{
ResourceName: "binarylane_vpc_route_entries.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIdentifierAttribute: "vpc_id",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
resourceState := s.RootModule().Resources["binarylane_vpc_route_entries.test"]
return resourceState.Primary.Attributes["vpc_id"], nil
},
},
// Update and Read testing
{
Config: providerConfig + `
resource "binarylane_vpc" "test" {
name = "tf-test-vpc-renamed"
ip_range = "10.240.0.0/16"
}
resource "binarylane_vpc_route_entries" "test" {
vpc_id = binarylane_vpc.test.id
route_entries = [
{
description = "test-renamed"
destination = "0.0.0.0/0"
router = "10.240.0.2"
}
]
}`,
Check: resource.ComposeAggregateTestCheckFunc(
// Verify binarylane_vpc resource values updated
resource.TestCheckResourceAttr("binarylane_vpc.test", "name", "tf-test-vpc-renamed"), // Updated name
resource.TestCheckResourceAttr("binarylane_vpc.test", "ip_range", "10.240.0.0/16"),
resource.TestCheckResourceAttrSet("binarylane_vpc.test", "id"),
// Verify binarylane_vpc_route_entries resource values updated
resource.TestCheckResourceAttrSet("binarylane_vpc_route_entries.test", "vpc_id"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.#", "1"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.destination", "0.0.0.0/0"),
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.router", "10.240.0.2"), // Updated router
resource.TestCheckResourceAttr("binarylane_vpc_route_entries.test", "route_entries.0.description", "test-renamed"), // Updated description
),
},
// Delete testing automatically occurs in TestCase
},
})
}
@@ -0,0 +1,109 @@
package provider
import (
"context"
"fmt"
"net/http"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
)
var (
_ datasource.DataSource = &vpcRouteEntriesDataSource{}
_ datasource.DataSourceWithConfigure = &vpcRouteEntriesDataSource{}
)
func NewVpcRouteEntriesDataSource() datasource.DataSource {
return &vpcRouteEntriesDataSource{}
}
type vpcRouteEntriesDataSource struct {
bc *BinarylaneClient
}
type vpcRouteEntriesDataSourceModel struct {
resources.VpcRouteEntriesModel
}
func (d *vpcRouteEntriesDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vpc_route_entries"
}
func (d *vpcRouteEntriesDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
ds, err := convertResourceSchemaToDataSourceSchema(resources.VpcRouteEntriesResourceSchema(ctx))
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"
// Overrides
vpcId := resp.Schema.Attributes["vpc_id"]
resp.Schema.Attributes["vpc_id"] = &schema.Int64Attribute{
Description: vpcId.GetDescription(),
MarkdownDescription: vpcId.GetMarkdownDescription(),
Required: true, // VPC ID is required to define the route entries
}
}
func (d *vpcRouteEntriesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data vpcRouteEntriesDataSourceModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Read API call logic
vpcResp, err := d.bc.client.GetVpcsVpcIdWithResponse(ctx, data.VpcId.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading VPC: vpc_id=%d", data.VpcId.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading VPC",
fmt.Sprintf("Received %s reading VPC: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body))
return
}
routeEntries, routeEntriesDiags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries)
resp.Diagnostics.Append(routeEntriesDiags...)
if resp.Diagnostics.HasError() {
return
}
data.RouteEntries = routeEntries
// Example data value setting
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (d *vpcRouteEntriesDataSource) 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
}
@@ -0,0 +1,278 @@
package provider
import (
"context"
"fmt"
"net/http"
"strconv"
"terraform-provider-binarylane/internal/binarylane"
"terraform-provider-binarylane/internal/resources"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"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/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
var (
_ resource.Resource = &vpcRouteEntriesResource{}
_ resource.ResourceWithConfigure = &vpcRouteEntriesResource{}
_ resource.ResourceWithImportState = &vpcRouteEntriesResource{}
)
func NewVpcRouteEntriesResource() resource.Resource {
return &vpcRouteEntriesResource{}
}
type vpcRouteEntriesResource struct {
bc *BinarylaneClient
}
type vpcRouteEntriesResourceModel struct {
resources.VpcRouteEntriesModel
}
func (r *vpcRouteEntriesResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_vpc_route_entries"
}
func (r *vpcRouteEntriesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resources.VpcRouteEntriesResourceSchema(ctx)
resp.Schema.Description = "TODO"
// Overrides
vpcId := resp.Schema.Attributes["vpc_id"]
resp.Schema.Attributes["vpc_id"] = &schema.Int64Attribute{
Description: vpcId.GetDescription(),
MarkdownDescription: vpcId.GetMarkdownDescription(),
Required: true, // VPC ID is required to define the route entries
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
}
}
func (r *vpcRouteEntriesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data vpcRouteEntriesResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// Create API call logic
routeEntries := []binarylane.RouteEntryRequest{}
diags := data.VpcRouteEntriesModel.RouteEntries.ElementsAs(ctx, &routeEntries, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
vpcResp, err := r.bc.client.PatchVpcsVpcIdWithResponse(ctx, data.VpcId.ValueInt64(), binarylane.PatchVpcRequest{
RouteEntries: &routeEntries,
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating VPC route entries: vpc_id=%d", data.VpcId.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating VPC route entries",
fmt.Sprintf("Received %s creating VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body))
return
}
routeEntriesState, diags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
data.RouteEntries = routeEntriesState
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *vpcRouteEntriesResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data vpcRouteEntriesResourceModel
// 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
vpcResp, err := r.bc.client.GetVpcsVpcIdWithResponse(ctx, data.VpcId.ValueInt64())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error reading VPC route entries: vpc_id=%d", data.VpcId.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code reading VPC route entries",
fmt.Sprintf("Received %s reading VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body))
return
}
routeEntries, routeEntriesDiags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries)
resp.Diagnostics.Append(routeEntriesDiags...)
if resp.Diagnostics.HasError() {
return
}
data.RouteEntries = routeEntries
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *vpcRouteEntriesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state vpcRouteEntriesResourceModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Update API call logic
routeEntries := []binarylane.RouteEntryRequest{}
diags := plan.VpcRouteEntriesModel.RouteEntries.ElementsAs(ctx, &routeEntries, true)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
vpcResp, err := r.bc.client.PatchVpcsVpcIdWithResponse(ctx, state.VpcId.ValueInt64(), binarylane.PatchVpcRequest{
RouteEntries: &routeEntries,
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error updating VPC route entries: vpc_id=%d", state.VpcId.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code updating VPC route entries",
fmt.Sprintf("Received %s updating VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), state.VpcId.ValueInt64(), vpcResp.Body))
return
}
routeEntriesState, diags := GetRouteEntriesState(ctx, vpcResp.JSON200.Vpc.RouteEntries)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
state.RouteEntries = routeEntriesState
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *vpcRouteEntriesResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data vpcRouteEntriesResourceModel
// 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
var routeEntries []binarylane.RouteEntryRequest
vpcResp, err := r.bc.client.PatchVpcsVpcIdWithResponse(ctx, data.VpcId.ValueInt64(), binarylane.PatchVpcRequest{
RouteEntries: &routeEntries,
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error deleting VPC route entries: vpc_id=%d", data.VpcId.ValueInt64()),
err.Error(),
)
return
}
if vpcResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code deleting VPC route entries",
fmt.Sprintf("Received %s deleting VPC route entries: vpc_id=%d. Details: %s", vpcResp.Status(), data.VpcId.ValueInt64(), vpcResp.Body))
return
}
}
func (d *vpcRouteEntriesResource) 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 GetRouteEntriesState(ctx context.Context, routeEntries *[]binarylane.RouteEntry) (basetypes.ListValue, diag.Diagnostics) {
var diags diag.Diagnostics
routeEntriesValue := resources.RouteEntriesValue{}
var routeEntriesValues []resources.RouteEntriesValue
if *routeEntries == nil || len(*routeEntries) == 0 {
routeEntriesValues = []resources.RouteEntriesValue{}
} else {
for _, route := range *routeEntries {
r := resources.NewRouteEntriesValueMust(routeEntriesValue.AttributeTypes(ctx), map[string]attr.Value{
"description": types.StringValue(*route.Description),
"destination": types.StringValue(*route.Destination),
"router": types.StringValue(*route.Router),
})
routeEntriesValues = append(routeEntriesValues, r)
}
}
routeEntriesListValue, diag := types.ListValueFrom(ctx, routeEntriesValue.Type(ctx), routeEntriesValues)
diags.Append(diag...)
if diags.HasError() {
return basetypes.NewListUnknown(routeEntriesValue.Type(ctx)), diags
}
return routeEntriesListValue, diags
}
func (r *vpcRouteEntriesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, err := strconv.ParseInt(req.ID, 10, 64)
if err != nil {
resp.Diagnostics.AddError(
"Error importing VPC route entries",
"Could not import VPC route entries, unexpected error (ID should be an integer): "+err.Error(),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("vpc_id"), id)
resp.Diagnostics.Append(diags...)
}
@@ -0,0 +1,789 @@
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
package resources
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"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"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
func ServerFirewallRulesResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"firewall_rules": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"action": schema.StringAttribute{
Required: true,
Description: "The action to take when there is a match on this rule.",
MarkdownDescription: "The action to take when there is a match on this rule.",
Validators: []validator.String{
stringvalidator.OneOf(
"drop",
"accept",
),
},
},
"description": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "A description to assist in identifying this rule. Commonly used to record the reason for the rule or the intent behind it, e.g. \"Block access to RDP\" or \"Allow access from HQ\".",
MarkdownDescription: "A description to assist in identifying this rule. Commonly used to record the reason for the rule or the intent behind it, e.g. \"Block access to RDP\" or \"Allow access from HQ\".",
Validators: []validator.String{
stringvalidator.LengthBetween(0, 250),
},
},
"destination_addresses": schema.ListAttribute{
ElementType: types.StringType,
Required: true,
Description: "The destination addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
MarkdownDescription: "The destination addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
},
},
"destination_ports": schema.ListAttribute{
ElementType: types.StringType,
Optional: true,
Computed: true,
Description: "The destination ports to match for this rule. Leave null or empty to match on all ports.",
MarkdownDescription: "The destination ports to match for this rule. Leave null or empty to match on all ports.",
},
"protocol": schema.StringAttribute{
Required: true,
Description: "The protocol to match for this rule.",
MarkdownDescription: "The protocol to match for this rule.",
Validators: []validator.String{
stringvalidator.OneOf(
"all",
"icmp",
"tcp",
"udp",
),
},
},
"source_addresses": schema.ListAttribute{
ElementType: types.StringType,
Required: true,
Description: "The source addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
MarkdownDescription: "The source addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
Validators: []validator.List{
listvalidator.SizeAtLeast(1),
},
},
},
CustomType: FirewallRulesType{
ObjectType: types.ObjectType{
AttrTypes: FirewallRulesValue{}.AttributeTypes(ctx),
},
},
},
Required: true,
Description: "A list of rules for the server. NB: that any existing rules that are not included will be removed. Submit an empty list to clear all rules.",
MarkdownDescription: "A list of rules for the server. NB: that any existing rules that are not included will be removed. Submit an empty list to clear all rules.",
},
"server_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The ID of the server for which firewall rules should be listed.",
MarkdownDescription: "The ID of the server for which firewall rules should be listed.",
},
},
}
}
type ServerFirewallRulesModel struct {
FirewallRules types.List `tfsdk:"firewall_rules"`
ServerId types.Int64 `tfsdk:"server_id"`
}
var _ basetypes.ObjectTypable = FirewallRulesType{}
type FirewallRulesType struct {
basetypes.ObjectType
}
func (t FirewallRulesType) Equal(o attr.Type) bool {
other, ok := o.(FirewallRulesType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t FirewallRulesType) String() string {
return "FirewallRulesType"
}
func (t FirewallRulesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
actionAttribute, ok := attributes["action"]
if !ok {
diags.AddError(
"Attribute Missing",
`action is missing from object`)
return nil, diags
}
actionVal, ok := actionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`action expected to be basetypes.StringValue, was: %T`, actionAttribute))
}
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return nil, diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
destinationAddressesAttribute, ok := attributes["destination_addresses"]
if !ok {
diags.AddError(
"Attribute Missing",
`destination_addresses is missing from object`)
return nil, diags
}
destinationAddressesVal, ok := destinationAddressesAttribute.(basetypes.ListValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`destination_addresses expected to be basetypes.ListValue, was: %T`, destinationAddressesAttribute))
}
destinationPortsAttribute, ok := attributes["destination_ports"]
if !ok {
diags.AddError(
"Attribute Missing",
`destination_ports is missing from object`)
return nil, diags
}
destinationPortsVal, ok := destinationPortsAttribute.(basetypes.ListValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`destination_ports expected to be basetypes.ListValue, was: %T`, destinationPortsAttribute))
}
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))
}
sourceAddressesAttribute, ok := attributes["source_addresses"]
if !ok {
diags.AddError(
"Attribute Missing",
`source_addresses is missing from object`)
return nil, diags
}
sourceAddressesVal, ok := sourceAddressesAttribute.(basetypes.ListValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`source_addresses expected to be basetypes.ListValue, was: %T`, sourceAddressesAttribute))
}
if diags.HasError() {
return nil, diags
}
return FirewallRulesValue{
Action: actionVal,
Description: descriptionVal,
DestinationAddresses: destinationAddressesVal,
DestinationPorts: destinationPortsVal,
Protocol: protocolVal,
SourceAddresses: sourceAddressesVal,
state: attr.ValueStateKnown,
}, diags
}
func NewFirewallRulesValueNull() FirewallRulesValue {
return FirewallRulesValue{
state: attr.ValueStateNull,
}
}
func NewFirewallRulesValueUnknown() FirewallRulesValue {
return FirewallRulesValue{
state: attr.ValueStateUnknown,
}
}
func NewFirewallRulesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (FirewallRulesValue, 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 FirewallRulesValue Attribute Value",
"While creating a FirewallRulesValue value, a missing attribute value was detected. "+
"A FirewallRulesValue 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("FirewallRulesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid FirewallRulesValue Attribute Type",
"While creating a FirewallRulesValue value, an invalid attribute value was detected. "+
"A FirewallRulesValue 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("FirewallRulesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("FirewallRulesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra FirewallRulesValue Attribute Value",
"While creating a FirewallRulesValue value, an extra attribute value was detected. "+
"A FirewallRulesValue 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 FirewallRulesValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewFirewallRulesValueUnknown(), diags
}
actionAttribute, ok := attributes["action"]
if !ok {
diags.AddError(
"Attribute Missing",
`action is missing from object`)
return NewFirewallRulesValueUnknown(), diags
}
actionVal, ok := actionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`action expected to be basetypes.StringValue, was: %T`, actionAttribute))
}
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return NewFirewallRulesValueUnknown(), diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
destinationAddressesAttribute, ok := attributes["destination_addresses"]
if !ok {
diags.AddError(
"Attribute Missing",
`destination_addresses is missing from object`)
return NewFirewallRulesValueUnknown(), diags
}
destinationAddressesVal, ok := destinationAddressesAttribute.(basetypes.ListValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`destination_addresses expected to be basetypes.ListValue, was: %T`, destinationAddressesAttribute))
}
destinationPortsAttribute, ok := attributes["destination_ports"]
if !ok {
diags.AddError(
"Attribute Missing",
`destination_ports is missing from object`)
return NewFirewallRulesValueUnknown(), diags
}
destinationPortsVal, ok := destinationPortsAttribute.(basetypes.ListValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`destination_ports expected to be basetypes.ListValue, was: %T`, destinationPortsAttribute))
}
protocolAttribute, ok := attributes["protocol"]
if !ok {
diags.AddError(
"Attribute Missing",
`protocol is missing from object`)
return NewFirewallRulesValueUnknown(), diags
}
protocolVal, ok := protocolAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`protocol expected to be basetypes.StringValue, was: %T`, protocolAttribute))
}
sourceAddressesAttribute, ok := attributes["source_addresses"]
if !ok {
diags.AddError(
"Attribute Missing",
`source_addresses is missing from object`)
return NewFirewallRulesValueUnknown(), diags
}
sourceAddressesVal, ok := sourceAddressesAttribute.(basetypes.ListValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`source_addresses expected to be basetypes.ListValue, was: %T`, sourceAddressesAttribute))
}
if diags.HasError() {
return NewFirewallRulesValueUnknown(), diags
}
return FirewallRulesValue{
Action: actionVal,
Description: descriptionVal,
DestinationAddresses: destinationAddressesVal,
DestinationPorts: destinationPortsVal,
Protocol: protocolVal,
SourceAddresses: sourceAddressesVal,
state: attr.ValueStateKnown,
}, diags
}
func NewFirewallRulesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) FirewallRulesValue {
object, diags := NewFirewallRulesValue(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("NewFirewallRulesValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t FirewallRulesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewFirewallRulesValueNull(), 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 NewFirewallRulesValueUnknown(), nil
}
if in.IsNull() {
return NewFirewallRulesValueNull(), 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 NewFirewallRulesValueMust(FirewallRulesValue{}.AttributeTypes(ctx), attributes), nil
}
func (t FirewallRulesType) ValueType(ctx context.Context) attr.Value {
return FirewallRulesValue{}
}
var _ basetypes.ObjectValuable = FirewallRulesValue{}
type FirewallRulesValue struct {
Action basetypes.StringValue `tfsdk:"action"`
Description basetypes.StringValue `tfsdk:"description"`
DestinationAddresses basetypes.ListValue `tfsdk:"destination_addresses"`
DestinationPorts basetypes.ListValue `tfsdk:"destination_ports"`
Protocol basetypes.StringValue `tfsdk:"protocol"`
SourceAddresses basetypes.ListValue `tfsdk:"source_addresses"`
state attr.ValueState
}
func (v FirewallRulesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 6)
var val tftypes.Value
var err error
attrTypes["action"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["description"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["destination_addresses"] = basetypes.ListType{
ElemType: types.StringType,
}.TerraformType(ctx)
attrTypes["destination_ports"] = basetypes.ListType{
ElemType: types.StringType,
}.TerraformType(ctx)
attrTypes["protocol"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["source_addresses"] = basetypes.ListType{
ElemType: types.StringType,
}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 6)
val, err = v.Action.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["action"] = val
val, err = v.Description.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["description"] = val
val, err = v.DestinationAddresses.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["destination_addresses"] = val
val, err = v.DestinationPorts.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["destination_ports"] = val
val, err = v.Protocol.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["protocol"] = val
val, err = v.SourceAddresses.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["source_addresses"] = 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 FirewallRulesValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v FirewallRulesValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v FirewallRulesValue) String() string {
return "FirewallRulesValue"
}
func (v FirewallRulesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
destinationAddressesVal, d := types.ListValue(types.StringType, v.DestinationAddresses.Elements())
diags.Append(d...)
if d.HasError() {
return types.ObjectUnknown(map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
"destination_addresses": basetypes.ListType{
ElemType: types.StringType,
},
"destination_ports": basetypes.ListType{
ElemType: types.StringType,
},
"protocol": basetypes.StringType{},
"source_addresses": basetypes.ListType{
ElemType: types.StringType,
},
}), diags
}
destinationPortsVal, d := types.ListValue(types.StringType, v.DestinationPorts.Elements())
diags.Append(d...)
if d.HasError() {
return types.ObjectUnknown(map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
"destination_addresses": basetypes.ListType{
ElemType: types.StringType,
},
"destination_ports": basetypes.ListType{
ElemType: types.StringType,
},
"protocol": basetypes.StringType{},
"source_addresses": basetypes.ListType{
ElemType: types.StringType,
},
}), diags
}
sourceAddressesVal, d := types.ListValue(types.StringType, v.SourceAddresses.Elements())
diags.Append(d...)
if d.HasError() {
return types.ObjectUnknown(map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
"destination_addresses": basetypes.ListType{
ElemType: types.StringType,
},
"destination_ports": basetypes.ListType{
ElemType: types.StringType,
},
"protocol": basetypes.StringType{},
"source_addresses": basetypes.ListType{
ElemType: types.StringType,
},
}), diags
}
attributeTypes := map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
"destination_addresses": basetypes.ListType{
ElemType: types.StringType,
},
"destination_ports": basetypes.ListType{
ElemType: types.StringType,
},
"protocol": basetypes.StringType{},
"source_addresses": basetypes.ListType{
ElemType: types.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{
"action": v.Action,
"description": v.Description,
"destination_addresses": destinationAddressesVal,
"destination_ports": destinationPortsVal,
"protocol": v.Protocol,
"source_addresses": sourceAddressesVal,
})
return objVal, diags
}
func (v FirewallRulesValue) Equal(o attr.Value) bool {
other, ok := o.(FirewallRulesValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.Action.Equal(other.Action) {
return false
}
if !v.Description.Equal(other.Description) {
return false
}
if !v.DestinationAddresses.Equal(other.DestinationAddresses) {
return false
}
if !v.DestinationPorts.Equal(other.DestinationPorts) {
return false
}
if !v.Protocol.Equal(other.Protocol) {
return false
}
if !v.SourceAddresses.Equal(other.SourceAddresses) {
return false
}
return true
}
func (v FirewallRulesValue) Type(ctx context.Context) attr.Type {
return FirewallRulesType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v FirewallRulesValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"action": basetypes.StringType{},
"description": basetypes.StringType{},
"destination_addresses": basetypes.ListType{
ElemType: types.StringType,
},
"destination_ports": basetypes.ListType{
ElemType: types.StringType,
},
"protocol": basetypes.StringType{},
"source_addresses": basetypes.ListType{
ElemType: types.StringType,
},
}
}
+32 -10
View File
@@ -17,8 +17,8 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
"backups": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "If true this will enable two daily backups for the server. Options.daily_backups will override this value if provided. Setting this to false has no effect.\n",
MarkdownDescription: "If true this will enable two daily backups for the server. Options.daily_backups will override this value if provided. Setting this to false has no effect.\n",
Description: "If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.\n",
MarkdownDescription: "If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.\n",
},
"id": schema.Int64Attribute{
Optional: true,
@@ -43,6 +43,12 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
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 (check server.password_change_supported via the servers endpoint). 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 (check server.password_change_supported via the servers endpoint). 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,
Description: "Port blocking of outgoing connections for email, SSH and Remote Desktop (TCP ports 22, 25, and 3389) is enabled by default for all new servers. If this is false port blocking will be disabled. Disabling port blocking is only available to reviewed accounts.",
MarkdownDescription: "Port blocking of outgoing connections for email, SSH and Remote Desktop (TCP ports 22, 25, and 3389) is enabled by default for all new servers. If this is false port blocking will be disabled. Disabling port blocking is only available to reviewed accounts.",
},
"region": schema.StringAttribute{
Required: true,
Description: "The slug of the selected region.",
@@ -59,6 +65,13 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
stringvalidator.LengthAtLeast(1),
},
},
"ssh_keys": schema.ListAttribute{
ElementType: types.Int64Type,
Optional: true,
Computed: true,
Description: "This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.",
MarkdownDescription: "This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys.",
},
"user_data": schema.StringAttribute{
Optional: true,
Computed: true,
@@ -68,17 +81,26 @@ func ServerResourceSchema(ctx context.Context) schema.Schema {
stringvalidator.LengthAtMost(65536),
},
},
"vpc_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "Leave null to use default (public) network for the selected region.",
MarkdownDescription: "Leave null to use default (public) network for the selected region.",
},
},
}
}
type ServerModel struct {
Backups types.Bool `tfsdk:"backups"`
Id types.Int64 `tfsdk:"id"`
Image types.String `tfsdk:"image"`
Name types.String `tfsdk:"name"`
Password types.String `tfsdk:"password"`
Region types.String `tfsdk:"region"`
Size types.String `tfsdk:"size"`
UserData types.String `tfsdk:"user_data"`
Backups types.Bool `tfsdk:"backups"`
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"`
SshKeys types.List `tfsdk:"ssh_keys"`
UserData types.String `tfsdk:"user_data"`
VpcId types.Int64 `tfsdk:"vpc_id"`
}
+45
View File
@@ -0,0 +1,45 @@
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
package resources
import (
"context"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
func VpcResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The target vpc id.",
MarkdownDescription: "The target vpc id.",
},
"ip_range": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "A private address range that you select during creation, such as the default value of 10.240.0.0/16. Because the virtual network is dedicated to your use, you may use whatever IP address range you like.",
MarkdownDescription: "A private address range that you select during creation, such as the default value of 10.240.0.0/16. Because the virtual network is dedicated to your use, you may use whatever IP address range you like.",
},
"name": schema.StringAttribute{
Required: true,
Description: "A name to help identify this VPC.",
MarkdownDescription: "A name to help identify this VPC.",
Validators: []validator.String{
stringvalidator.LengthBetween(0, 250),
},
},
},
}
}
type VpcModel struct {
Id types.Int64 `tfsdk:"id"`
IpRange types.String `tfsdk:"ip_range"`
Name types.String `tfsdk:"name"`
}
@@ -0,0 +1,510 @@
// 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"
"strings"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
)
func VpcRouteEntriesResourceSchema(ctx context.Context) schema.Schema {
return schema.Schema{
Attributes: map[string]schema.Attribute{
"route_entries": schema.ListNestedAttribute{
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"description": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "An optional description for the route.",
MarkdownDescription: "An optional description for the route.",
Validators: []validator.String{
stringvalidator.LengthAtMost(250),
},
},
"destination": schema.StringAttribute{
Required: true,
Description: "The destination address for this route entry. This may be in CIDR format.",
MarkdownDescription: "The destination address for this route entry. This may be in CIDR format.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
"router": schema.StringAttribute{
Required: true,
Description: "The server that will receive traffic sent to the destination property in this VPC.",
MarkdownDescription: "The server that will receive traffic sent to the destination property in this VPC.",
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
},
},
},
CustomType: RouteEntriesType{
ObjectType: types.ObjectType{
AttrTypes: RouteEntriesValue{}.AttributeTypes(ctx),
},
},
},
Optional: true,
Computed: true,
Description: "Submit null to leave unaltered, submit an empty list to clear all route entries. It is not possible to PATCH individual route entries, to alter a route entry submit the entire list of route entries you wish to save.",
MarkdownDescription: "Submit null to leave unaltered, submit an empty list to clear all route entries. It is not possible to PATCH individual route entries, to alter a route entry submit the entire list of route entries you wish to save.",
},
"vpc_id": schema.Int64Attribute{
Optional: true,
Computed: true,
Description: "The target vpc id.",
MarkdownDescription: "The target vpc id.",
},
},
}
}
type VpcRouteEntriesModel struct {
RouteEntries types.List `tfsdk:"route_entries"`
VpcId types.Int64 `tfsdk:"vpc_id"`
}
var _ basetypes.ObjectTypable = RouteEntriesType{}
type RouteEntriesType struct {
basetypes.ObjectType
}
func (t RouteEntriesType) Equal(o attr.Type) bool {
other, ok := o.(RouteEntriesType)
if !ok {
return false
}
return t.ObjectType.Equal(other.ObjectType)
}
func (t RouteEntriesType) String() string {
return "RouteEntriesType"
}
func (t RouteEntriesType) ValueFromObject(ctx context.Context, in basetypes.ObjectValue) (basetypes.ObjectValuable, diag.Diagnostics) {
var diags diag.Diagnostics
attributes := in.Attributes()
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return nil, diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
destinationAttribute, ok := attributes["destination"]
if !ok {
diags.AddError(
"Attribute Missing",
`destination is missing from object`)
return nil, diags
}
destinationVal, ok := destinationAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`destination expected to be basetypes.StringValue, was: %T`, destinationAttribute))
}
routerAttribute, ok := attributes["router"]
if !ok {
diags.AddError(
"Attribute Missing",
`router is missing from object`)
return nil, diags
}
routerVal, ok := routerAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`router expected to be basetypes.StringValue, was: %T`, routerAttribute))
}
if diags.HasError() {
return nil, diags
}
return RouteEntriesValue{
Description: descriptionVal,
Destination: destinationVal,
Router: routerVal,
state: attr.ValueStateKnown,
}, diags
}
func NewRouteEntriesValueNull() RouteEntriesValue {
return RouteEntriesValue{
state: attr.ValueStateNull,
}
}
func NewRouteEntriesValueUnknown() RouteEntriesValue {
return RouteEntriesValue{
state: attr.ValueStateUnknown,
}
}
func NewRouteEntriesValue(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) (RouteEntriesValue, 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 RouteEntriesValue Attribute Value",
"While creating a RouteEntriesValue value, a missing attribute value was detected. "+
"A RouteEntriesValue 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("RouteEntriesValue Attribute Name (%s) Expected Type: %s", name, attributeType.String()),
)
continue
}
if !attributeType.Equal(attribute.Type(ctx)) {
diags.AddError(
"Invalid RouteEntriesValue Attribute Type",
"While creating a RouteEntriesValue value, an invalid attribute value was detected. "+
"A RouteEntriesValue 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("RouteEntriesValue Attribute Name (%s) Expected Type: %s\n", name, attributeType.String())+
fmt.Sprintf("RouteEntriesValue Attribute Name (%s) Given Type: %s", name, attribute.Type(ctx)),
)
}
}
for name := range attributes {
_, ok := attributeTypes[name]
if !ok {
diags.AddError(
"Extra RouteEntriesValue Attribute Value",
"While creating a RouteEntriesValue value, an extra attribute value was detected. "+
"A RouteEntriesValue 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 RouteEntriesValue Attribute Name: %s", name),
)
}
}
if diags.HasError() {
return NewRouteEntriesValueUnknown(), diags
}
descriptionAttribute, ok := attributes["description"]
if !ok {
diags.AddError(
"Attribute Missing",
`description is missing from object`)
return NewRouteEntriesValueUnknown(), diags
}
descriptionVal, ok := descriptionAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`description expected to be basetypes.StringValue, was: %T`, descriptionAttribute))
}
destinationAttribute, ok := attributes["destination"]
if !ok {
diags.AddError(
"Attribute Missing",
`destination is missing from object`)
return NewRouteEntriesValueUnknown(), diags
}
destinationVal, ok := destinationAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`destination expected to be basetypes.StringValue, was: %T`, destinationAttribute))
}
routerAttribute, ok := attributes["router"]
if !ok {
diags.AddError(
"Attribute Missing",
`router is missing from object`)
return NewRouteEntriesValueUnknown(), diags
}
routerVal, ok := routerAttribute.(basetypes.StringValue)
if !ok {
diags.AddError(
"Attribute Wrong Type",
fmt.Sprintf(`router expected to be basetypes.StringValue, was: %T`, routerAttribute))
}
if diags.HasError() {
return NewRouteEntriesValueUnknown(), diags
}
return RouteEntriesValue{
Description: descriptionVal,
Destination: destinationVal,
Router: routerVal,
state: attr.ValueStateKnown,
}, diags
}
func NewRouteEntriesValueMust(attributeTypes map[string]attr.Type, attributes map[string]attr.Value) RouteEntriesValue {
object, diags := NewRouteEntriesValue(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("NewRouteEntriesValueMust received error(s): " + strings.Join(diagsStrings, "\n"))
}
return object
}
func (t RouteEntriesType) ValueFromTerraform(ctx context.Context, in tftypes.Value) (attr.Value, error) {
if in.Type() == nil {
return NewRouteEntriesValueNull(), 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 NewRouteEntriesValueUnknown(), nil
}
if in.IsNull() {
return NewRouteEntriesValueNull(), 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 NewRouteEntriesValueMust(RouteEntriesValue{}.AttributeTypes(ctx), attributes), nil
}
func (t RouteEntriesType) ValueType(ctx context.Context) attr.Value {
return RouteEntriesValue{}
}
var _ basetypes.ObjectValuable = RouteEntriesValue{}
type RouteEntriesValue struct {
Description basetypes.StringValue `tfsdk:"description"`
Destination basetypes.StringValue `tfsdk:"destination"`
Router basetypes.StringValue `tfsdk:"router"`
state attr.ValueState
}
func (v RouteEntriesValue) ToTerraformValue(ctx context.Context) (tftypes.Value, error) {
attrTypes := make(map[string]tftypes.Type, 3)
var val tftypes.Value
var err error
attrTypes["description"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["destination"] = basetypes.StringType{}.TerraformType(ctx)
attrTypes["router"] = basetypes.StringType{}.TerraformType(ctx)
objectType := tftypes.Object{AttributeTypes: attrTypes}
switch v.state {
case attr.ValueStateKnown:
vals := make(map[string]tftypes.Value, 3)
val, err = v.Description.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["description"] = val
val, err = v.Destination.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["destination"] = val
val, err = v.Router.ToTerraformValue(ctx)
if err != nil {
return tftypes.NewValue(objectType, tftypes.UnknownValue), err
}
vals["router"] = 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 RouteEntriesValue) IsNull() bool {
return v.state == attr.ValueStateNull
}
func (v RouteEntriesValue) IsUnknown() bool {
return v.state == attr.ValueStateUnknown
}
func (v RouteEntriesValue) String() string {
return "RouteEntriesValue"
}
func (v RouteEntriesValue) ToObjectValue(ctx context.Context) (basetypes.ObjectValue, diag.Diagnostics) {
var diags diag.Diagnostics
attributeTypes := map[string]attr.Type{
"description": basetypes.StringType{},
"destination": basetypes.StringType{},
"router": 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{
"description": v.Description,
"destination": v.Destination,
"router": v.Router,
})
return objVal, diags
}
func (v RouteEntriesValue) Equal(o attr.Value) bool {
other, ok := o.(RouteEntriesValue)
if !ok {
return false
}
if v.state != other.state {
return false
}
if v.state != attr.ValueStateKnown {
return true
}
if !v.Description.Equal(other.Description) {
return false
}
if !v.Destination.Equal(other.Destination) {
return false
}
if !v.Router.Equal(other.Router) {
return false
}
return true
}
func (v RouteEntriesValue) Type(ctx context.Context) attr.Type {
return RouteEntriesType{
basetypes.ObjectType{
AttrTypes: v.AttributeTypes(ctx),
},
}
}
func (v RouteEntriesValue) AttributeTypes(ctx context.Context) map[string]attr.Type {
return map[string]attr.Type{
"description": basetypes.StringType{},
"destination": basetypes.StringType{},
"router": basetypes.StringType{},
}
}
+283 -1
View File
@@ -11,7 +11,7 @@
"name": "backups",
"bool": {
"computed_optional_required": "computed_optional",
"description": "If true this will enable two daily backups for the server. Options.daily_backups will override this value if provided. Setting this to false has no effect.\n"
"description": "If true this will enable two daily backups for the server. `options.daily_backups` will override this value if provided. Setting this to false has no effect.\n"
}
},
{
@@ -35,6 +35,13 @@
"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 (check server.password_change_supported via the servers endpoint). 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": {
"computed_optional_required": "computed_optional",
"description": "Port blocking of outgoing connections for email, SSH and Remote Desktop (TCP ports 22, 25, and 3389) is enabled by default for all new servers. If this is false port blocking will be disabled. Disabling port blocking is only available to reviewed accounts."
}
},
{
"name": "region",
"string": {
@@ -73,6 +80,16 @@
]
}
},
{
"name": "ssh_keys",
"list": {
"computed_optional_required": "computed_optional",
"element_type": {
"int64": {}
},
"description": "This is a list of SSH key ids. If this is null or not provided, any SSH keys that have been marked as default will be deployed (assuming the operating system supports SSH Keys). Submit an empty list to disable deployment of default keys."
}
},
{
"name": "user_data",
"string": {
@@ -92,6 +109,13 @@
]
}
},
{
"name": "vpc_id",
"int64": {
"computed_optional_required": "computed_optional",
"description": "Leave null to use default (public) network for the selected region."
}
},
{
"name": "id",
"int64": {
@@ -102,6 +126,142 @@
]
}
},
{
"name": "server_firewall_rules",
"schema": {
"attributes": [
{
"name": "firewall_rules",
"list_nested": {
"computed_optional_required": "required",
"nested_object": {
"attributes": [
{
"name": "action",
"string": {
"computed_optional_required": "required",
"description": "The action to take when there is a match on this rule.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.OneOf(\n\"drop\",\n\"accept\",\n)"
}
}
]
}
},
{
"name": "description",
"string": {
"computed_optional_required": "computed_optional",
"description": "A description to assist in identifying this rule. Commonly used to record the reason for the rule or the intent behind it, e.g. \"Block access to RDP\" or \"Allow access from HQ\".",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthBetween(0, 250)"
}
}
]
}
},
{
"name": "destination_addresses",
"list": {
"computed_optional_required": "required",
"element_type": {
"string": {}
},
"description": "The destination addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
}
],
"schema_definition": "listvalidator.SizeAtLeast(1)"
}
}
]
}
},
{
"name": "destination_ports",
"list": {
"computed_optional_required": "computed_optional",
"element_type": {
"string": {}
},
"description": "The destination ports to match for this rule. Leave null or empty to match on all ports."
}
},
{
"name": "protocol",
"string": {
"computed_optional_required": "required",
"description": "The protocol to match for this rule.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.OneOf(\n\"all\",\n\"icmp\",\n\"tcp\",\n\"udp\",\n)"
}
}
]
}
},
{
"name": "source_addresses",
"list": {
"computed_optional_required": "required",
"element_type": {
"string": {}
},
"description": "The source addresses to match for this rule. Each address may be an individual IPv4 address or a range in IPv4 CIDR notation.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
}
],
"schema_definition": "listvalidator.SizeAtLeast(1)"
}
}
]
}
}
]
},
"description": "A list of rules for the server. NB: that any existing rules that are not included will be removed. Submit an empty list to clear all rules."
}
},
{
"name": "server_id",
"int64": {
"computed_optional_required": "computed_optional",
"description": "The ID of the server for which firewall rules should be listed."
}
}
]
}
},
{
"name": "ssh_key",
"schema": {
@@ -160,6 +320,128 @@
}
]
}
},
{
"name": "vpc",
"schema": {
"attributes": [
{
"name": "ip_range",
"string": {
"computed_optional_required": "computed_optional",
"description": "A private address range that you select during creation, such as the default value of 10.240.0.0/16. Because the virtual network is dedicated to your use, you may use whatever IP address range you like."
}
},
{
"name": "name",
"string": {
"computed_optional_required": "required",
"description": "A name to help identify this VPC.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthBetween(0, 250)"
}
}
]
}
},
{
"name": "id",
"int64": {
"computed_optional_required": "computed_optional",
"description": "The target vpc id."
}
}
]
}
},
{
"name": "vpc_route_entries",
"schema": {
"attributes": [
{
"name": "route_entries",
"list_nested": {
"computed_optional_required": "computed_optional",
"nested_object": {
"attributes": [
{
"name": "description",
"string": {
"computed_optional_required": "computed_optional",
"description": "An optional description for the route.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthAtMost(250)"
}
}
]
}
},
{
"name": "destination",
"string": {
"computed_optional_required": "required",
"description": "The destination address for this route entry. This may be in CIDR format.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthAtLeast(1)"
}
}
]
}
},
{
"name": "router",
"string": {
"computed_optional_required": "required",
"description": "The server that will receive traffic sent to the destination property in this VPC.",
"validators": [
{
"custom": {
"imports": [
{
"path": "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
}
],
"schema_definition": "stringvalidator.LengthAtLeast(1)"
}
}
]
}
}
]
},
"description": "Submit null to leave unaltered, submit an empty list to clear all route entries. It is not possible to PATCH individual route entries, to alter a route entry submit the entire list of route entries you wish to save."
}
},
{
"name": "vpc_id",
"int64": {
"computed_optional_required": "computed_optional",
"description": "The target vpc id."
}
}
]
}
}
],
"version": "0.1"
+59 -36
View File
@@ -31,7 +31,8 @@ resources:
backups:
type: bool
description: >
If true this will enable two daily backups for the server. Options.daily_backups will override this value if provided. Setting this to false has no effect.
If true this will enable two daily backups for the server. `options.daily_backups` will override this
value if provided. Setting this to false has no effect.
optional: true
default:
static: false
@@ -39,7 +40,7 @@ resources:
image:
type: string
description: The slug of the selected operating system.
example: "ubuntu-24.04"
example: 'ubuntu-24.04'
password:
type: string
description: >
@@ -53,11 +54,25 @@ resources:
- ipv6
- licenses
- links
- options
- server
- ssh_keys
- vpc_id
- port_blocking
- options
server_firewall_rules:
create:
path: /servers/{server_id}/actions#ChangeAdvancedFirewallRules
method: POST
read:
path: /servers/{server_id}/advanced_firewall_rules
method: GET
update:
path: /servers/{server_id}/actions#ChangeAdvancedFirewallRules
method: POST
delete:
path: /servers/{server_id}/actions#ChangeAdvancedFirewallRules
method: POST
schema:
ignores:
- type
- action
ssh_key:
create:
path: /account/keys
@@ -74,34 +89,42 @@ resources:
key_id: id
ignores:
- ssh_key
#
# Currently don't need to generate data sources as they use the same schema as resources
vpc:
create:
path: /vpcs
method: POST
read:
path: /vpcs/{vpc_id}
method: GET
update:
path: /vpcs/{vpc_id}
method: PATCH
delete:
path: /vpcs/{vpc_id}
method: DELETE
schema:
attributes:
aliases:
vpc_id: id
ignores:
- route_entries
- vpc
vpc_route_entries:
create:
path: /vpcs/{vpc_id}
method: PATCH
read:
path: /vpcs/{vpc_id}
method: GET
update:
path: /vpcs/{vpc_id}
method: PATCH
delete:
path: /vpcs/{vpc_id}
method: DELETE
schema:
ignores:
- name
- vpc
# data_sources:
# server:
# read:
# path: /servers/{server_id}
# method: GET
# schema:
# attributes:
# aliases:
# server_id: id
# ignores:
# - ipv6
# - licenses
# - links
# - options
# - server
# - ssh_keys
# - vpc_id
# - user_data
# - port_blocking
# ssh_key:
# read:
# path: /account/keys/{key_id}
# method: GET
# schema:
# attributes:
# aliases:
# key_id: id
# ignores:
# - ssh_key
# Currently, don't need to generate data sources as they use the same schema as resources