Files
Oscar Hermoso 9a58a8112c 👍 Support vpc_ipv4_address, bump BinaryLane OpenAPI schema to v0.34.0 (#114)
* 👍 Bump BinaryLane OpenAPI schema to v0.34.0

* 💄 Support change of IPv4 addresses
2026-02-08 16:58:30 +08:00

340 lines
10 KiB
Go

package provider
import (
"context"
"fmt"
"net/http"
"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/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &sshKeyResource{}
_ resource.ResourceWithConfigure = &sshKeyResource{}
_ resource.ResourceWithImportState = &sshKeyResource{}
)
func NewSshKeyResource() resource.Resource {
return &sshKeyResource{}
}
type sshKeyResource struct {
bc *BinarylaneClient
}
type sshKeyModel struct {
resources.SshKeyModel
Fingerprint types.String `tfsdk:"fingerprint"`
PublicKey TrimmedStringValue `tfsdk:"public_key"`
}
func (d *sshKeyResource) 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 *sshKeyResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_ssh_key"
}
func (r *sshKeyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = resources.SshKeyResourceSchema(ctx)
// Overrides
idDescription := "The numeric ID of the SSH key."
resp.Schema.Attributes["id"] = schema.Int64Attribute{
Required: false,
Optional: false,
Computed: true,
Description: idDescription,
MarkdownDescription: idDescription,
PlanModifiers: []planmodifier.Int64{
int64planmodifier.UseStateForUnknown(),
},
}
defaultDescription := "If `true`, this SSH key will be included on all new server installations (if the operating " +
"system supports SSH key injection). Default is `false`."
resp.Schema.Attributes["default"] = schema.BoolAttribute{
Required: false,
Optional: true,
Computed: true,
Description: defaultDescription,
MarkdownDescription: defaultDescription,
Default: booldefault.StaticBool(false), // Add default to backups
}
// Additional attributes
publicKeyDescription := "The public key in OpenSSH \"authorized_keys\" format. This should be a valid public key, " +
"such as one generated by `ssh-keygen`."
resp.Schema.Attributes["public_key"] = schema.StringAttribute{
Required: true,
Optional: false,
Computed: false,
Description: publicKeyDescription,
MarkdownDescription: publicKeyDescription,
PlanModifiers: []planmodifier.String{
RequiresReplaceForTrimmedStringModifier{},
},
CustomType: TrimmedStringType{}, // Ignore leading/trailing whitespace
}
fingerprintDescription := "The fingerprint of the SSH key."
resp.Schema.Attributes["fingerprint"] = schema.StringAttribute{
Description: fingerprintDescription,
MarkdownDescription: fingerprintDescription,
Optional: false,
Required: false,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
}
}
func (r *sshKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data sshKeyModel
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
sshResp, err := r.bc.client.PostAccountKeysWithResponse(ctx, binarylane.SshKeyRequest{
Name: data.Name.ValueString(),
Default: data.Default.ValueBoolPointer(),
PublicKey: data.PublicKey.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating SSH Key: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if sshResp.StatusCode() == http.StatusNotFound {
tflog.Warn(ctx, fmt.Sprintf("SSH Key not found, removing from state: name=%s", data.Name.String()))
resp.State.RemoveResource(ctx)
return
}
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code creating SSH key",
fmt.Sprintf("Received %s creating SSH key: name=%s. Details: %s", sshResp.Status(), data.Name.ValueString(), sshResp.Body),
)
return
}
// Set data values
data.Id = types.Int64Value(sshResp.JSON200.SshKey.Id)
data.Fingerprint = types.StringValue(sshResp.JSON200.SshKey.Fingerprint)
if resp.Diagnostics.HasError() {
return
}
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *sshKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data sshKeyModel
// 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
sshResp, err := r.bc.client.GetAccountKeysKeyIdWithResponse(ctx, int(data.Id.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error creating SSH Key: name=%s", data.Name.ValueString()),
err.Error(),
)
return
}
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting SSH key",
fmt.Sprintf("Received %s getting SSH key: name=%s. Details: %s", sshResp.Status(), data.Name.ValueString(), sshResp.Body),
)
return
}
// Set data values
data.Id = types.Int64Value(sshResp.JSON200.SshKey.Id)
data.Default = types.BoolValue(sshResp.JSON200.SshKey.Default)
data.Name = types.StringValue(*sshResp.JSON200.SshKey.Name)
data.PublicKey = TrimmedStringValue{
StringValue: types.StringValue(sshResp.JSON200.SshKey.PublicKey),
}
data.Fingerprint = types.StringValue(sshResp.JSON200.SshKey.Fingerprint)
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *sshKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state sshKeyModel
// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// Update API call logic
if !state.Name.Equal(plan.Name) || !state.Default.Equal(plan.Default) {
sshResp, err := r.bc.client.PutAccountKeysKeyIdWithResponse(ctx, int(state.Id.ValueInt64()), binarylane.UpdateSshKeyRequest{
Name: plan.Name.ValueString(),
Default: plan.Default.ValueBoolPointer(),
})
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error updating SSH Key: name=%s", plan.Name.ValueString()),
err.Error(),
)
return
}
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code updating SSH Key",
fmt.Sprintf("Received %s updating new SSH Key: name=%s. Details: %s", sshResp.Status(), plan.Name.ValueString(), sshResp.Body),
)
return
}
state.Name = plan.Name
state.Default = plan.Default
}
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *sshKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state sshKeyModel
// Read Terraform prior state into the model
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Delete API call logic
sshResp, err := r.bc.client.DeleteAccountKeysKeyIdWithResponse(ctx, int(state.Id.ValueInt64()))
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Error deleting SSH Key: name=%s", state.Name.ValueString()),
err.Error(),
)
return
}
if sshResp.StatusCode() != http.StatusNoContent {
resp.Diagnostics.AddError(
"Unexpected HTTP status code deleting SSH Key",
fmt.Sprintf("Received %s deleting SSH Key: name=%s. Details: %s", sshResp.Status(), state.Name.ValueString(), sshResp.Body),
)
return
}
}
func (r *sshKeyResource) ImportState(
ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse,
) {
// Import by ID
id, err := strconv.ParseInt(req.ID, 10, 32)
if err == nil {
diags := resp.State.SetAttribute(ctx, path.Root("id"), int32(id))
resp.Diagnostics.Append(diags...)
return
}
// Import by name
var page int32 = 1
perPage := int32(200)
var sshKey binarylane.SshKey
var nextPage bool = true
for nextPage { // Need to paginate because the API does not support filtering by fingerprint
params := binarylane.GetAccountKeysParams{
Page: &page,
PerPage: &perPage,
}
sshResp, err := r.bc.client.GetAccountKeysWithResponse(ctx, &params)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Error getting SSH keys for import: fingerprint=%s", req.ID), err.Error())
return
}
if sshResp.StatusCode() != http.StatusOK {
resp.Diagnostics.AddError(
"Unexpected HTTP status code getting SSH key for import",
fmt.Sprintf("Received %s getting SSH key for import: fingerprint=%s. Details: %s", sshResp.Status(), req.ID,
sshResp.Body))
return
}
sshKeys := sshResp.JSON200.SshKeys
for _, key := range sshKeys {
if key.Fingerprint == req.ID {
sshKey = key
nextPage = false
break
}
}
if sshResp.JSON200.Links == nil || sshResp.JSON200.Links.Pages.Next == nil {
nextPage = false
break
}
page++
}
if sshKey.Id == 0 {
resp.Diagnostics.AddError(
"Could not find SSH key by fingerprint",
fmt.Sprintf("Error finding SSH key: fingerprint=%s", req.ID),
)
return
}
diags := resp.State.SetAttribute(ctx, path.Root("id"), sshKey.Id)
resp.Diagnostics.Append(diags...)
}