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.") }