Skip to content

Commit

Permalink
provider: Implement definednet_host resource
Browse files Browse the repository at this point in the history
The resource provides a mechanism for managing hosts on Defined.net control plane.
  • Loading branch information
janartodesk committed Oct 22, 2024
1 parent 6ecc942 commit a2b7cdd
Show file tree
Hide file tree
Showing 13 changed files with 692 additions and 3 deletions.
15 changes: 15 additions & 0 deletions examples/resources/definednet_host/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
variable "definednet_token" {
description = "Defined.net HTTP API token"
sensitive = true
}

provider "definednet" {
token = var.definednet_token
}

resource "definednet_host" "example" {
name = "example.defined.test"
network_id = "network-7P81MCS2TVAY9XJWQTNJ3PWYPD"
role_id = "role-WSG78880Z655TQJVQFL5CZ405B"
tags = ["service:app"]
}
33 changes: 30 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,69 @@ go 1.22.7
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/hashicorp/terraform-plugin-framework v1.12.0
github.com/hashicorp/terraform-plugin-go v0.24.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.10.0
github.com/onsi/ginkgo/v2 v2.20.2
github.com/onsi/gomega v1.34.2
github.com/samber/lo v1.47.0
)

require (
github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/terraform-plugin-go v0.24.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/hc-install v0.8.0 // indirect
github.com/hashicorp/hcl/v2 v2.21.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/zclconf/go-cty v1.15.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.24.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
google.golang.org/grpc v1.66.2 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
130 changes: 130 additions & 0 deletions go.sum

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions internal/host/docs/resource.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
`definednet_host` enables managing Nebula overlay network hosts on Defined.net.

The Defined.net API token must be configured with the following scope:

- `hosts:create`
- `hosts:delete`
- `hosts:enroll`
- `hosts:list`
- `hosts:read`
- `hosts:update`
255 changes: 255 additions & 0 deletions internal/host/resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package host

import (
"context"
_ "embed"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/sendsmaily/terraform-provider-definednet/internal/definednet"
)

// NewResource creates a Defined.net Nebula host resource.
func NewResource() resource.Resource {
return &Resource{}
}

// Resource is Defined.net Nebula host resource.
type Resource struct {
client definednet.Client
}

var _ resource.Resource = (*Resource)(nil)
var _ resource.ResourceWithConfigure = (*Resource)(nil)
var _ resource.ResourceWithImportState = (*Resource)(nil)

// Configure configures the resource.
func (r *Resource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(definednet.Client)
if !ok {
resp.Diagnostics.AddError("Invalid Configuration", "The provider specifies an invalid client type")
return
}

r.client = client
}

// Metadata returns the resource's metadata.
func (r *Resource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = fmt.Sprintf("%s_host", req.ProviderTypeName)
}

// Schema returns the resource's configuration schema.
func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = Schema
}

// Create creates Nebula hosts on Defined.net control plane.
func (r *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var state State

resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

var tags []string
resp.Diagnostics.Append(state.Tags.ElementsAs(ctx, &tags, false)...)
if resp.Diagnostics.HasError() {
return
}

host, err := definednet.CreateHost(ctx, r.client, definednet.CreateHostRequest{
NetworkID: state.NetworkID.ValueString(),
RoleID: state.RoleID.ValueString(),
Name: state.Name.ValueString(),
StaticAddresses: []string{},
ListenPort: 0,
IsLighthouse: false,
IsRelay: false,
Tags: tags,
})

if err != nil {
resp.Diagnostics.AddError("Request Failure", err.Error())
return
}

state.ID = types.StringValue(host.ID)
state.IPAddress = types.StringValue(host.IPAddress)

tflog.Trace(ctx, "created Defined.net host", map[string]any{
"id": state.ID.String(),
"network_id": state.NetworkID.String(),
"role_id": state.RoleID.String(),
"name": state.Name.String(),
"tags": state.Tags.String(),
})

code, err := definednet.CreateEnrollmentCode(ctx, r.client, definednet.CreateEnrollmentCodeRequest{
ID: state.ID.ValueString(),
})

if err != nil {
resp.Diagnostics.AddError("Request Failure", err.Error())
return
}

state.EnrollmentCode = types.StringValue(code.Code)

tflog.Trace(ctx, "created Defined.net enrollment code", map[string]any{
"id": state.ID.String(),
})

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
}

// Delete deletes Nebula hosts from Defined.net control plane.
func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state State

resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

if err := definednet.DeleteHost(ctx, r.client, definednet.DeleteHostRequest{
ID: state.ID.ValueString(),
}); err != nil {
resp.Diagnostics.AddError("Request Failure", err.Error())
}
}

// Read reads Nebula hosts from Defined.net control plane.
func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state State

resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

host, err := definednet.GetHost(ctx, r.client, definednet.GetHostRequest{
ID: state.ID.ValueString(),
})

if err != nil {
resp.Diagnostics.AddError("Request Failure", err.Error())
return
}

state.Name = types.StringValue(host.Name)
state.NetworkID = types.StringValue(host.NetworkID)
state.RoleID = types.StringValue(host.RoleID)

tags, diags := types.ListValueFrom(ctx, types.StringType, host.Tags)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

state.Tags = tags

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)

tflog.Trace(ctx, "refreshed Defined.net host", map[string]any{
"id": state.ID.String(),
"network_id": state.NetworkID.String(),
"role_id": state.RoleID.String(),
"name": state.Name.String(),
"tags": state.Tags.String(),
})
}

// Update updates Nebula hosts on Defined.net control plane.
func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var current, next State

resp.Diagnostics.Append(req.State.Get(ctx, &current)...)
if resp.Diagnostics.HasError() {
return
}

resp.Diagnostics.Append(req.Plan.Get(ctx, &next)...)
if resp.Diagnostics.HasError() {
return
}

var tags []string
resp.Diagnostics.Append(next.Tags.ElementsAs(ctx, &tags, false)...)
if resp.Diagnostics.HasError() {
return
}

host, err := definednet.UpdateHost(ctx, r.client, definednet.UpdateHostRequest{
ID: current.ID.ValueString(),
RoleID: next.RoleID.ValueString(),
Name: next.Name.ValueString(),
StaticAddresses: []string{},
ListenPort: 0,
Tags: tags,
})

if err != nil {
resp.Diagnostics.AddError("Request Failure", err.Error())
return
}

next.ID = types.StringValue(host.ID)
next.IPAddress = types.StringValue(host.IPAddress)
next.EnrollmentCode = current.EnrollmentCode

resp.Diagnostics.Append(resp.State.Set(ctx, next)...)

tflog.Trace(ctx, "updated Defined.net host", map[string]any{
"id": next.ID.String(),
"network_id": next.NetworkID.String(),
"role_id": next.RoleID.String(),
"name": next.Name.String(),
"tags": next.Tags.String(),
})
}

// ImportState imports Nebula hosts from Defined.net control plane.
func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
host, err := definednet.GetHost(ctx, r.client, definednet.GetHostRequest{
ID: req.ID,
})

if err != nil {
resp.Diagnostics.AddError("Request Failure", err.Error())
return
}

tags, diags := types.ListValueFrom(ctx, types.StringType, host.Tags)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

state := State{
ID: types.StringValue(host.ID),
Name: types.StringValue(host.Name),
NetworkID: types.StringValue(host.NetworkID),
RoleID: types.StringValue(host.RoleID),
IPAddress: types.StringValue(host.IPAddress),
EnrollmentCode: types.StringNull(),
Tags: tags,
}

resp.Diagnostics.Append(resp.State.Set(ctx, state)...)

tflog.Trace(ctx, "imported Defined.net host", map[string]any{
"id": state.ID.String(),
"network_id": state.NetworkID.String(),
"role_id": state.RoleID.String(),
"name": state.Name.String(),
"tags": state.Tags.String(),
})
}
Loading

0 comments on commit a2b7cdd

Please sign in to comment.