Compare commits

...

5 Commits

Author SHA1 Message Date
Oscar Hermoso 7a978731bb 🥉 k3s example working (#14)
* WIP commit - helm example

* 🥉 k3s working!
2024-09-07 17:00:24 +08:00
Oscar Hermoso 54dd00fa72 📰 Create LICENSE 2024-09-01 08:17:56 +08:00
Anurag e93659373d 🕸 Add server import by hostname & fix missing port_blocking (#12)
* Add server import by hostname

* Fix missing port blocking in server create

* hostname -> name
2024-08-31 15:27:03 +08:00
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
29 changed files with 530 additions and 228 deletions
+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
+7 -1
View File
@@ -35,7 +35,13 @@ terraform apply
```sh
TF_ACC=1 go test -v ./...
TF_DEBUG=1 BINARYLANE_API_TOKEN=********* TF_ACC=1 go test -v ./...
```
### Update modules
```sh
go get -u && go mod tidy && go generate
```
### Adding resources/data sources to the provider
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Oscar Hermoso
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+31 -9
View File
@@ -1,6 +1,6 @@
# terraform-provider-binarylane
See the examples in the [examples directory](./examples/basic/main.tf).
See the examples in the [examples directory](./examples/basic/README.md).
```terraform
resource "binarylane_server" "example" {
@@ -10,15 +10,33 @@ resource "binarylane_server" "example" {
}
```
## WIP
## Progress
If somehow you use this in production I would be pretty impressed.
Planned features:
- [x] Create/delete a server when runing locally
- [x] Publish
- [x] [Terraform Registry](https://registry.terraform.io/providers/oscarhermoso/binarylane/latest/docs/resources/server)
- [x] OpenTofu
- [ ] Deploy the rest of the owl
- [x] Servers
- [x] Firewall Rules
- [x] Cloud Init
- [ ] Updating server properties
- [ ] Backups
- [ ] Software
- [ ] IPV6
- [ ] Alerts
- [x] SSH Keys
- [x] Virtual Private Cloud
- [ ] Images
- [ ] Load Balancers
- [ ] DNS
- [x] Docs
- [x] Generated docs
- [ ] Review & fill out remaining descriptions
- [ ] Include examples in docs
- [x] Examples
- [x] Basic
- [x] Cloud Init
- [ ] NixOS
- [ ] Virtual Private Cloud
- [ ] Kubernetes
## Parameters
@@ -78,7 +96,10 @@ jq '[ .regions[] | .slug ] | sort' tmp/regions.json
]
```
Sizes (not all sizes are available in all regions):
### Sizes
<details>
<summary>VM size</summary>
```sh
curl -X GET "https://api.binarylane.com.au/v2/sizes" \
@@ -86,6 +107,7 @@ curl -X GET "https://api.binarylane.com.au/v2/sizes" \
jq '[ .sizes[] | .slug ] | sort' tmp/sizes.json
```
</details>
```json
[
+1
View File
@@ -25,6 +25,7 @@ TODO
- `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.
+1
View File
@@ -28,6 +28,7 @@ Provides a Binary Lane Server resource. This can be used to create and delete se
- `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.
+2 -2
View File
@@ -11,10 +11,10 @@ provider "binarylane" {
}
resource "binarylane_server" "example" {
name = "tf-example"
name = "tf-example-basic"
region = "per" # or "syd", "mel", "bne", "sin"
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
}
+3
View File
@@ -35,3 +35,6 @@ override.tf.json
# Ignore CLI configuration files
.terraformrc
terraform.rc
# .kube/config contains credentials
.kube
+1
View File
@@ -5,3 +5,4 @@ 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
- https://github.com/schnerring/schnerring.github.io/blob/07d06fb40e3d01f2483a739f516ac4d711a5742c/content/blog/use-terraform-to-deploy-an-azure-kubernetes-service-aks-cluster-traefik-2-cert-manager-and-lets-encrypt-certificates/index.md
+3
View File
@@ -10,4 +10,7 @@ curl -sfL https://get.k3s.io | sh -s - agent \
unset K3S_TOKEN
unset K3S_URL
# Disable firewall recommended
ufw disable
echo "K3s Node Join Completed"
+4 -1
View File
@@ -10,7 +10,7 @@ curl -sfL https://get.k3s.io | sh -s - server \
# --disable-cloud-controller \
# --disable servicelb \
# --disable local-storage \
# --disable traefik \
# --disable traefik
unset INSTALL_K3S_NAME
unset K3S_TOKEN
@@ -19,4 +19,7 @@ unset K3S_KUBECONFIG_MODE
echo "Installing Helm"
curl -sfL https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
# Disable firewall recommended
ufw disable
echo "K3s Setup Completed"
+66 -9
View File
@@ -12,6 +12,13 @@ resource "random_password" "cluster_token" {
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" {
@@ -42,9 +49,35 @@ resource "binarylane_server" "server" {
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
}
data "external" "kubeconfig" {
program = [
"/usr/bin/ssh",
"-o UserKnownHostsFile=/dev/null",
"-o StrictHostKeyChecking=no",
"root@${binarylane_server.server.0.permalink}",
"echo '{\"kubeconfig\":\"'$(cat /etc/rancher/k3s/k3s.yaml | base64)'\"}'",
]
}
resource "local_sensitive_file" "kubeconfig" {
depends_on = [binarylane_server.server]
content = replace(
base64decode(
replace(data.external.kubeconfig.result.kubeconfig, " ", "")
),
"server: https://127.0.0.1:6443",
"server: https://${binarylane_server.server.0.permalink}:6443",
)
filename = "${path.module}/.kube/config"
file_permission = "0600"
}
# k3s Agents
@@ -70,9 +103,11 @@ resource "binarylane_server" "agent" {
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
@@ -88,20 +123,26 @@ resource "binarylane_vpc_route_entries" "example" {
]
}
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 flatten([binarylane_server.server, binarylane_server.agent]) : server.id => server }
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 = flatten([for _, a in binarylane_server.agent : a.private_ipv4_addresses])
destination_addresses = flatten([for _, s in binarylane_server.server : s.private_ipv4_addresses])
destination_ports = ["6443"]
action = "accept"
},
# TODO: Due to a bug, this only works if you uncomment AFTER creating servers
# {
# description = "K3s supervisor and Kubernetes API Server"
# protocol = "tcp"
# source_addresses = ["0.0.0.0/0"]
# destination_addresses = local.server_ips
# destination_ports = ["6443"]
# action = "accept"
# },
{
description = "Flannel VXLAN"
protocol = "udp"
@@ -118,5 +159,21 @@ resource "binarylane_server_firewall_rules" "example" {
destination_ports = ["10250"]
action = "accept"
},
{
description = "SSH"
protocol = "udp"
source_addresses = ["0.0.0.0/0"]
destination_addresses = [binarylane_vpc.example.ip_range]
destination_ports = ["22"]
action = "accept"
},
{
description = "HTTP"
protocol = "tcp"
source_addresses = ["0.0.0.0/0"]
destination_addresses = [binarylane_vpc.example.ip_range]
destination_ports = ["80"]
action = "accept"
},
]
}
+64
View File
@@ -0,0 +1,64 @@
resource "kubectl_manifest" "hello" {
yaml_body = <<YAML
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nginx-hello
labels:
app: nginx-hello
spec:
selector:
matchLabels:
app: nginx-hello
template:
metadata:
labels:
app: nginx-hello
spec:
containers:
- image: nginxdemos/hello
name: nginx-hello
ports:
- name: http
containerPort: 8080
protocol: TCP
YAML
}
resource "kubectl_manifest" "hello_service" {
yaml_body = <<YAML
apiVersion: v1
kind: Service
metadata:
name: nginx-hello
labels:
app: nginx-hello
spec:
type: ClusterIP
selector:
app: nginx-hello
ports:
- port: 80
YAML
}
resource "kubectl_manifest" "hello_ingress" {
yaml_body = <<YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-hello
spec:
rules:
- host: "*.bnr.la"
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx-hello
port:
number: 80
YAML
}
+18
View File
@@ -3,7 +3,25 @@ terraform {
binarylane = {
source = "oscarhermoso/binarylane"
}
kubectl = {
source = "alekc/kubectl"
version = ">= 2.0.0"
}
}
}
provider "binarylane" {}
provider "helm" {
kubernetes {
host = "${binarylane_server.server.0.permalink}:6443"
config_path = local_sensitive_file.kubeconfig.filename
insecure = true # TODO
}
}
provider "kubectl" {
host = "${binarylane_server.server.0.permalink}:6443"
config_path = local_sensitive_file.kubeconfig.filename
insecure = true # TODO
}
-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 -11
View File
@@ -8,20 +8,33 @@ terraform {
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"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
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]
}
+4 -4
View File
@@ -9,13 +9,13 @@ terraform {
provider "binarylane" {}
resource "binarylane_vpc" "example" {
name = "tf-example"
name = "tf-example-vpc"
ip_range = "10.240.0.0/16"
}
# Web & NAT server
resource "binarylane_server" "web" {
name = "tf-vpc-example-web"
name = "tf-example-vpc-web"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
@@ -25,7 +25,7 @@ resource "binarylane_server" "web" {
# Database server
resource "binarylane_server" "db" {
name = "tf-vpc-example-db"
name = "tf-example-vpc-db"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
@@ -35,7 +35,7 @@ resource "binarylane_server" "db" {
# VPN server
resource "binarylane_server" "vpn" {
name = "tf-vpc-example-vpn"
name = "tf-example-vpc-vpn"
region = "per"
image = "ubuntu-24.04"
size = "std-min"
+4
View File
@@ -11,6 +11,7 @@ cat <<<$(jq '.paths |= with_entries(.key |= sub("/v2/"; "/"))' $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
@@ -20,6 +21,9 @@ cat <<<$(jq '.paths["/account/keys/{key_id}"].delete.parameters[0].schema |= del
# Remove the "/paths/{image_id}" path because its duplicated by "/images/{image_id_or_slug}"
cat <<<$(jq 'del(.paths."/images/{image_id}")' $OPENAPI_FILE) >$OPENAPI_FILE
# 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
+3 -10
View File
@@ -10100,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": {
+2 -75
View File
@@ -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"`
@@ -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
+56 -27
View File
@@ -8,6 +8,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/datasource"
d_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
@@ -25,6 +26,13 @@ type serverDataSource struct {
bc *BinarylaneClient
}
type serverDataModel struct {
resources.ServerModel
PublicIpv4Addresses types.List `tfsdk:"public_ipv4_addresses"`
PrivateIPv4Addresses types.List `tfsdk:"private_ipv4_addresses"`
Permalink types.String `tfsdk:"permalink"`
}
func (d *serverDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
@@ -63,63 +71,84 @@ func (d *serverDataSource) Schema(ctx context.Context, req datasource.SchemaRequ
MarkdownDescription: id.GetMarkdownDescription(),
Required: true, // ID is required to find the server
}
// Additional attributes
resp.Schema.Attributes["permalink"] = &schema.StringAttribute{
Description: "TODO",
MarkdownDescription: "TODO",
Optional: true,
}
publicIpv4AddressesDescription := "The public IPv4 addresses assigned to the server."
resp.Schema.Attributes["public_ipv4_addresses"] = &schema.ListAttribute{
Description: publicIpv4AddressesDescription,
MarkdownDescription: publicIpv4AddressesDescription,
ElementType: types.StringType,
Optional: true,
}
privateIpv4AddressesDescription := "The private IPv4 addresses assigned to the server."
resp.Schema.Attributes["private_ipv4_addresses"] = &schema.ListAttribute{
Description: privateIpv4AddressesDescription,
MarkdownDescription: privateIpv4AddressesDescription,
ElementType: types.StringType,
Optional: true,
}
}
func (d *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data resources.ServerModel
var data serverDataModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
diags := req.Config.Get(ctx, &data)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// TODO: Use go routines here to make requests concurrently
// Read server
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
}
// TODO: Fetch user data
// Read user data
// userDataResp, err := d.bc.client.GetServersServerIdUserDataWithResponse(ctx, data.Id.ValueInt64())
// if err != nil {
// resp.Diagnostics.AddError(
// fmt.Sprintf("Error reading server user data: name=%s", data.Id.String()),
// err.Error(),
// )
// return
// }
// if userDataResp.StatusCode() != http.StatusOK {
// resp.Diagnostics.AddError(
// fmt.Sprintf("Unexpected HTTP status code reading server user data: name=%s", data.Id.String()),
// fmt.Sprintf("Unexpected HTTP status code reading server user data: %s", serverResp.Body),
// )
// return
// }
// Set data values
data.Id = types.Int64Value(*serverResp.JSON200.Server.Id)
data.Name = types.StringValue(*serverResp.JSON200.Server.Name)
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.UserData = types.StringValue(*userDataResp.JSON200.UserData)
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
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)...)
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
@@ -78,7 +78,6 @@ func (r *serverFirewallRulesResource) Create(ctx context.Context, req resource.C
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
+132 -37
View File
@@ -4,15 +4,22 @@ import (
"context"
"fmt"
"net/http"
"slices"
"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"
@@ -22,9 +29,9 @@ import (
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &serverResource{}
_ resource.ResourceWithConfigure = &serverResource{}
// _ resource.ResourceWithImportState = &serverResource{}
_ resource.Resource = &serverResource{}
_ resource.ResourceWithConfigure = &serverResource{}
_ resource.ResourceWithImportState = &serverResource{}
)
// Helper function to simplify the provider implementation.
@@ -38,10 +45,11 @@ type serverResource struct {
type serverModel struct {
resources.ServerModel
WaitForCreateSeconds 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"`
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"`
Permalink types.String `tfsdk:"permalink"`
}
func (d *serverResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
@@ -79,6 +87,9 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
}
pw := resp.Schema.Attributes["password"]
@@ -127,6 +138,19 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
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)),
},
}
// Additional attributes
waitDescription := "The number of seconds to wait for the server to be created, after which, a timeout error will " +
"be reported. If `wait_seconds` is left empty or set to 0, Terraform will succeed without waiting for the " +
@@ -135,8 +159,7 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
Description: waitDescription,
MarkdownDescription: waitDescription,
Optional: true,
Computed: true,
Default: int32default.StaticInt32(0),
Computed: false,
}
publicIpv4CountDescription := "The number of public IPv4 addresses to assign to the server. If this is not provided, the " +
@@ -146,6 +169,7 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
MarkdownDescription: publicIpv4CountDescription,
Optional: true,
Computed: true,
Default: int32default.StaticInt32(1),
Validators: []validator.Int32{
int32validator.AtLeast(0),
},
@@ -160,6 +184,9 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
}
privateIpv4AddressesDescription := "The private IPv4 addresses assigned to the server."
@@ -171,6 +198,18 @@ func (r *serverResource) Schema(ctx context.Context, _ resource.SchemaRequest, r
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
}
resp.Schema.Attributes["permalink"] = &schema.StringAttribute{
Description: "TODO",
MarkdownDescription: "TODO",
// read only
Optional: false,
Required: false,
Computed: true,
}
}
@@ -187,13 +226,22 @@ 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(),
Name: data.Name.ValueStringPointer(),
Image: data.Image.ValueString(),
Region: data.Region.ValueString(),
Size: data.Size.ValueString(),
UserData: data.UserData.ValueStringPointer(),
VpcId: data.VpcId.ValueInt64Pointer(),
PortBlocking: data.PortBlocking.ValueBoolPointer(),
SshKeys: &sshKeys,
Options: &binarylane.SizeOptionsRequest{
Ipv4Addresses: data.PublicIpv4Count.ValueInt32Pointer(),
},
@@ -227,20 +275,20 @@ 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)
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
publicV4AddressCount := 0
var publicIpv4Addresses []string
var privateIpv4Addresses []string
for _, v4address := range serverResp.JSON200.Server.Networks.V4 {
if v4address.Type == "public" {
publicV4AddressCount++
publicIpv4Addresses = append(publicIpv4Addresses, v4address.IpAddress)
} else {
privateIpv4Addresses = append(privateIpv4Addresses, v4address.IpAddress)
}
}
data.PublicIpv4Count = types.Int32Value(int32(publicV4AddressCount))
data.PublicIpv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, publicIpv4Addresses)
resp.Diagnostics.Append(diags...)
data.PrivateIPv4Addresses, diags = types.ListValueFrom(ctx, types.StringType, privateIpv4Addresses)
@@ -249,7 +297,7 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest,
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
if data.WaitForCreateSeconds <= 0 {
if data.WaitForCreateSeconds.ValueInt32() <= 0 {
return
}
@@ -268,7 +316,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...")
@@ -301,7 +349,8 @@ 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
}
@@ -320,8 +369,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: id=%s, name=%s. Details: %s", serverResp.Status(), data.Name.ValueString(), 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
}
@@ -330,6 +380,25 @@ 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)
data.Permalink = types.StringValue(*serverResp.JSON200.Server.Permalink)
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
@@ -391,19 +460,45 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}
// func (r *serverResource) ImportState(
// ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
// ) {
// id, err := strconv.ParseInt(req.ID, 10, 32)
func (r *serverResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
id, err := strconv.ParseInt(req.ID, 10, 32)
if err == nil {
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
} else {
name := req.ID
params := binarylane.GetServersParams{
Hostname: &name,
}
// if err != nil {
// resp.Diagnostics.AddError(
// "Error importing server",
// "Could not import server, unexpected error (ID should be an integer): "+err.Error(),
// )
// return
// }
serverResp, err := r.bc.client.GetServersWithResponse(ctx, &params)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Error getting server: hostname=%s", name), err.Error())
return
}
// diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
// resp.Diagnostics.Append(diags...)
// }
if serverResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting server",
fmt.Sprintf("Received %s reading server: hostname=%s. Details: %s", serverResp.Status(), name,
serverResp.Body))
return
}
servers := *serverResp.JSON200.Servers
idx := slices.IndexFunc(servers, func(s binarylane.Server) bool { return *s.Name == name })
if idx == -1 {
resp.Diagnostics.AddError(
"Could not find server by hostname",
fmt.Sprintf("Error finding server: hostname=%s", name),
)
return
}
server := servers[idx]
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(*server.Id))
resp.Diagnostics.Append(diags...)
}
}
+36 -2
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{
@@ -25,6 +29,18 @@ resource "binarylane_vpc" "test" {
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-test-server-resource"
region = "per"
@@ -37,6 +53,7 @@ resource "binarylane_server" "test" {
#cloud-config
echo "Hello World" > /var/tmp/output.txt
EOT
ssh_keys = [binarylane_ssh_key.test.id]
}
data "binarylane_server" "test" {
@@ -61,15 +78,32 @@ 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"),
resource.TestCheckResourceAttrSet("binarylane_server.test", "permalink"),
// Verify data source values
resource.TestCheckResourceAttrSet("data.binarylane_server.test", "id"),
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"),
resource.TestCheckResourceAttrPair("data.binarylane_server.test", "permalink", "binarylane_server.test", "permalink"),
),
},
{
ResourceName: "binarylane_server.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "ssh_keys", "user_data", "wait_for_create", "public_ipv4_count"},
},
{
ResourceName: "binarylane_server.test",
ImportState: true,
ImportStateId: "tf-test-server-resource",
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"password", "ssh_keys", "user_data", "wait_for_create", "public_ipv4_count"},
},
},
})
}
+8 -7
View File
@@ -7,6 +7,7 @@ import (
"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/resource/schema/booldefault"
@@ -17,9 +18,9 @@ import (
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &sshKeyResource{}
_ resource.ResourceWithConfigure = &sshKeyResource{}
// _ resource.ResourceWithImportState = &serverResource{}
_ resource.Resource = &sshKeyResource{}
_ resource.ResourceWithConfigure = &sshKeyResource{}
_ resource.ResourceWithImportState = &serverResource{}
)
func NewSshKeyResource() resource.Resource {
@@ -217,7 +218,7 @@ func (r *sshKeyResource) Delete(ctx context.Context, req resource.DeleteRequest,
}
}
// func (r *sshKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// // Retrieve import ID and save to id attribute
// resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
// }
func (r *sshKeyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
// Retrieve import ID and save to id attribute
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
+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)
}
@@ -65,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,
@@ -93,6 +100,7 @@ type ServerModel struct {
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"`
}
+10
View File
@@ -80,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": {
-2
View File
@@ -27,7 +27,6 @@ resources:
attributes:
aliases:
server_id: id
ipv4_addresses: options.ipv4_addresses
overrides:
backups:
type: bool
@@ -56,7 +55,6 @@ resources:
- licenses
- links
- server
- ssh_keys
- options
server_firewall_rules:
create: