hhctl CLI and Terraform provider for Hammerhead

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 17:02:15 -06:00
commit bf340cdab4
19 files changed
+2442

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/datasource"
dsschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"git.veillax.com/Veillax/hhctl/client"
)
var (
_ resource.Resource = &edgeResource{}
_ resource.ResourceWithConfigure = &edgeResource{}
_ resource.ResourceWithImportState = &edgeResource{}
_ datasource.DataSource = &edgeData{}
_ datasource.DataSourceWithConfigure = &edgeData{}
)
func NewEdge() resource.Resource { return &edgeResource{} }
type edgeResource struct{ c *client.Client }
type edgeModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
TunnelAddr types.String `tfsdk:"tunnel_addr"`
OrgIDs types.Set `tfsdk:"org_ids"`
Token types.String `tfsdk:"token"`
IPs types.List `tfsdk:"ips"`
}
func (r *edgeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_edge"
}
func (r *edgeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "An edge: a public Blacktip node. Needs a server API key.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}},
"name": schema.StringAttribute{Required: true},
"tunnel_addr": schema.StringAttribute{Required: true, Description: "The edge's public WireGuard endpoint, host:port (UDP)."},
"org_ids": schema.SetAttribute{Optional: true, Computed: true, ElementType: types.StringType, Default: setdefault.StaticValue(setOf(nil)),
Description: "Organizations allowed to use the edge; empty means all."},
"token": schema.StringAttribute{Computed: true, Sensitive: true, PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
Description: "The token to give the Blacktip program."},
"ips": schema.ListAttribute{Computed: true, ElementType: types.StringType, Description: "Addresses the edge reported it can bind."},
},
}
}
func (r *edgeResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
r.c = clientFrom(req.ProviderData)
}
func ipsList(ctx context.Context, ips []string) types.List {
if ips == nil {
ips = []string{}
}
l, _ := types.ListValueFrom(ctx, types.StringType, ips)
return l
}
func (r *edgeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan edgeModel
if resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...); resp.Diagnostics.HasError() {
return
}
e, err := r.c.CreateEdge(client.Edge{Name: plan.Name.ValueString(), TunnelAddr: plan.TunnelAddr.ValueString(), OrgIDs: stringsOf(ctx, plan.OrgIDs)})
if err != nil {
resp.Diagnostics.AddError("Creating edge", err.Error())
return
}
plan.ID, plan.Token, plan.IPs = types.StringValue(e.ID), types.StringValue(e.Token), ipsList(ctx, e.IPs)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *edgeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state edgeModel
if resp.Diagnostics.Append(req.State.Get(ctx, &state)...); resp.Diagnostics.HasError() {
return
}
e, err := r.c.GetEdge(state.ID.ValueString())
if client.IsNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError("Reading edge", err.Error())
return
}
state.Name, state.TunnelAddr, state.OrgIDs, state.IPs = types.StringValue(e.Name), types.StringValue(e.TunnelAddr), setOf(e.OrgIDs), ipsList(ctx, e.IPs)
if e.Token != "" {
state.Token = types.StringValue(e.Token)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *edgeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan, state edgeModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
e, err := r.c.UpdateEdge(client.Edge{ID: state.ID.ValueString(), Name: plan.Name.ValueString(), TunnelAddr: plan.TunnelAddr.ValueString(), OrgIDs: stringsOf(ctx, plan.OrgIDs)})
if err != nil {
resp.Diagnostics.AddError("Updating edge", err.Error())
return
}
plan.ID, plan.Token, plan.IPs = state.ID, state.Token, ipsList(ctx, e.IPs)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
func (r *edgeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state edgeModel
if resp.Diagnostics.Append(req.State.Get(ctx, &state)...); resp.Diagnostics.HasError() {
return
}
if err := r.c.DeleteEdge(state.ID.ValueString()); err != nil && !client.IsNotFound(err) {
resp.Diagnostics.AddError("Deleting edge", err.Error())
}
}
func (r *edgeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
importByID(ctx, req, resp)
}
// ---- data source: look an edge up by name, so ports can refer to one created elsewhere ----
func NewEdgeData() datasource.DataSource { return &edgeData{} }
type edgeData struct{ c *client.Client }
type edgeDataModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
IPs types.List `tfsdk:"ips"`
Online types.Bool `tfsdk:"online"`
}
func (d *edgeData) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_edge"
}
func (d *edgeData) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = dsschema.Schema{
Description: "An existing edge, found by name. Works with an organization key.",
Attributes: map[string]dsschema.Attribute{
"id": dsschema.StringAttribute{Computed: true},
"name": dsschema.StringAttribute{Required: true},
"ips": dsschema.ListAttribute{Computed: true, ElementType: types.StringType},
"online": dsschema.BoolAttribute{Computed: true},
},
}
}
func (d *edgeData) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
d.c = clientFrom(req.ProviderData)
}
func (d *edgeData) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var m edgeDataModel
if resp.Diagnostics.Append(req.Config.Get(ctx, &m)...); resp.Diagnostics.HasError() {
return
}
edges, err := d.c.Edges()
if err != nil {
resp.Diagnostics.AddError("Listing edges", err.Error())
return
}
for _, e := range edges {
if e.Name == m.Name.ValueString() {
m.ID, m.IPs, m.Online = types.StringValue(e.ID), ipsList(ctx, e.IPs), types.BoolValue(e.Online)
resp.Diagnostics.Append(resp.State.Set(ctx, &m)...)
return
}
}
resp.Diagnostics.AddError("Edge not found", "No edge named "+m.Name.ValueString()+" is visible to this key.")
}