From 71216601c67bdb4625f665ab2a19553b2b8f1b7b Mon Sep 17 00:00:00 2001 From: benvin Date: Sat, 25 Jul 2026 23:22:16 +1000 Subject: [PATCH] Add policy/blrule/conntrack provider resources Add tomswallapi_policy, tomswallapi_blrule, and tomswallapi_conntrack for the global-compiled long-tail tier, following the id-keyed resource pattern. Add an optionalList helper for the dport/sport list attributes. Register and document. --- README.md | 3 + internal/provider/helpers.go | 9 ++ internal/provider/provider.go | 3 + internal/provider/resource_blrule.go | 189 ++++++++++++++++++++++ internal/provider/resource_conntrack.go | 199 ++++++++++++++++++++++++ internal/provider/resource_policy.go | 165 ++++++++++++++++++++ 6 files changed, 568 insertions(+) create mode 100644 internal/provider/resource_blrule.go create mode 100644 internal/provider/resource_conntrack.go create mode 100644 internal/provider/resource_policy.go diff --git a/README.md b/README.md index aa485b5..b1fa990 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ provider "tomswallapi" { | `tomswallapi_snat` | id | masquerade/SNAT; `source` zone-or-CIDR, `egress` zone | | `tomswallapi_netmap` | id | net-to-net map, anchored `device:zone`\|`device:interface` | | `tomswallapi_nat` | id | 1:1 static NAT bound to a `device` | +| `tomswallapi_policy` | id | default zone-to-zone posture (`priority` ordered) | +| `tomswallapi_blrule` | id | blacklist/whitelist rule (pre-rules) | +| `tomswallapi_conntrack` | id | connection-tracking control (notrack/helper) | ## Example diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go index e17ba69..5bbeb51 100644 --- a/internal/provider/helpers.go +++ b/internal/provider/helpers.go @@ -45,6 +45,15 @@ func stringsToList(ctx context.Context, s []string, diags *diag.Diagnostics) typ return l } +// optionalList maps an API string slice back to state for an optional list +// attribute, preserving a null value the user left unset when the API returns none. +func optionalList(ctx context.Context, apiVals []string, prior types.List, diags *diag.Diagnostics) types.List { + if prior.IsNull() && len(apiVals) == 0 { + return types.ListNull(types.StringType) + } + return stringsToList(ctx, apiVals, diags) +} + // mapToStrings converts a Terraform map into a map[string]string. func mapToStrings(ctx context.Context, m types.Map, diags *diag.Diagnostics) map[string]string { if m.IsNull() || m.IsUnknown() { diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 58c8bb2..147bb69 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -78,6 +78,9 @@ func (p *tomswallProvider) Resources(_ context.Context) []func() resource.Resour NewSNATResource, NewNetmapResource, NewNATResource, + NewPolicyResource, + NewBlruleResource, + NewConntrackResource, } } diff --git a/internal/provider/resource_blrule.go b/internal/provider/resource_blrule.go new file mode 100644 index 0000000..42cb069 --- /dev/null +++ b/internal/provider/resource_blrule.go @@ -0,0 +1,189 @@ +package provider + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &blruleResource{} + _ resource.ResourceWithImportState = &blruleResource{} +) + +type blruleResource struct{ client *apiClient } + +type blruleModel struct { + ID types.Int64 `tfsdk:"id"` + Priority types.Int64 `tfsdk:"priority"` + Action types.String `tfsdk:"action"` + Source types.String `tfsdk:"source"` + Dest types.String `tfsdk:"dest"` + Proto types.String `tfsdk:"proto"` + DPort types.List `tfsdk:"dport"` + SPort types.List `tfsdk:"sport"` + Log types.String `tfsdk:"log"` + Comment types.String `tfsdk:"comment"` +} + +type blruleAPI struct { + ID int64 `json:"id,omitempty"` + Priority int `json:"priority"` + Action string `json:"action"` + Source string `json:"source,omitempty"` + Dest string `json:"dest,omitempty"` + Proto string `json:"proto,omitempty"` + DPort []string `json:"dport,omitempty"` + SPort []string `json:"sport,omitempty"` + Log string `json:"log,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewBlruleResource() resource.Resource { return &blruleResource{} } + +func (r *blruleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_blrule" +} + +func (r *blruleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A blacklist/whitelist rule, processed before normal rules.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "priority": schema.Int64Attribute{ + Description: "Evaluation priority; lower first.", + Optional: true, Computed: true, Default: int64default.StaticInt64(0), + }, + "action": schema.StringAttribute{Description: "drop, reject, whitelist, ...", Required: true}, + "source": schema.StringAttribute{Optional: true}, + "dest": schema.StringAttribute{Optional: true}, + "proto": schema.StringAttribute{Optional: true}, + "dport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "sport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "log": schema.StringAttribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *blruleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *blruleResource) body(ctx context.Context, plan blruleModel, diags *diag.Diagnostics) blruleAPI { + return blruleAPI{ + Priority: int(plan.Priority.ValueInt64()), + Action: plan.Action.ValueString(), + Source: plan.Source.ValueString(), + Dest: plan.Dest.ValueString(), + Proto: plan.Proto.ValueString(), + DPort: listToStrings(ctx, plan.DPort, diags), + SPort: listToStrings(ctx, plan.SPort, diags), + Log: plan.Log.ValueString(), + Comment: plan.Comment.ValueString(), + } +} + +func (r *blruleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan blruleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out blruleAPI + if err := r.client.post(ctx, "/api/v1/blrules", body, &out); err != nil { + resp.Diagnostics.AddError("create blrule failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *blruleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state blruleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out blruleAPI + if err := r.client.post(ctx, "/api/v1/blrules", body, &out); err != nil { + resp.Diagnostics.AddError("recreate blrule failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/blrules/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old blrule failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *blruleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state blruleModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out blruleAPI + if err := r.client.get(ctx, "/api/v1/blrules/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read blrule failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...) +} + +func (r *blruleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state blruleModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/blrules/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete blrule failed", err.Error()) + } +} + +func (r *blruleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "blrule id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *blruleResource) toModel(ctx context.Context, api blruleAPI, prior blruleModel, diags *diag.Diagnostics) blruleModel { + return blruleModel{ + ID: types.Int64Value(api.ID), + Priority: types.Int64Value(int64(api.Priority)), + Action: types.StringValue(api.Action), + Source: optionalString(api.Source, prior.Source), + Dest: optionalString(api.Dest, prior.Dest), + Proto: optionalString(api.Proto, prior.Proto), + DPort: optionalList(ctx, api.DPort, prior.DPort, diags), + SPort: optionalList(ctx, api.SPort, prior.SPort, diags), + Log: optionalString(api.Log, prior.Log), + Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_conntrack.go b/internal/provider/resource_conntrack.go new file mode 100644 index 0000000..c8199a1 --- /dev/null +++ b/internal/provider/resource_conntrack.go @@ -0,0 +1,199 @@ +package provider + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &conntrackResource{} + _ resource.ResourceWithImportState = &conntrackResource{} +) + +type conntrackResource struct{ client *apiClient } + +type conntrackModel struct { + ID types.Int64 `tfsdk:"id"` + Priority types.Int64 `tfsdk:"priority"` + Action types.String `tfsdk:"action"` + Source types.String `tfsdk:"source"` + Dest types.String `tfsdk:"dest"` + Proto types.String `tfsdk:"proto"` + DPort types.List `tfsdk:"dport"` + SPort types.List `tfsdk:"sport"` + Chain types.String `tfsdk:"chain"` + Helper types.String `tfsdk:"helper"` + User types.String `tfsdk:"user"` + Comment types.String `tfsdk:"comment"` +} + +type conntrackAPI struct { + ID int64 `json:"id,omitempty"` + Priority int `json:"priority"` + Action string `json:"action"` + Source string `json:"source,omitempty"` + Dest string `json:"dest,omitempty"` + Proto string `json:"proto,omitempty"` + DPort []string `json:"dport,omitempty"` + SPort []string `json:"sport,omitempty"` + Chain string `json:"chain,omitempty"` + Helper string `json:"helper,omitempty"` + User string `json:"user,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func NewConntrackResource() resource.Resource { return &conntrackResource{} } + +func (r *conntrackResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_conntrack" +} + +func (r *conntrackResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A connection-tracking control rule (e.g. notrack, or assign a conntrack helper).", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{Computed: true, PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}}, + "priority": schema.Int64Attribute{ + Description: "Evaluation priority; lower first.", + Optional: true, Computed: true, Default: int64default.StaticInt64(0), + }, + "action": schema.StringAttribute{Description: "notrack, helper, ...", Required: true}, + "source": schema.StringAttribute{Optional: true}, + "dest": schema.StringAttribute{Optional: true}, + "proto": schema.StringAttribute{Optional: true}, + "dport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "sport": schema.ListAttribute{Optional: true, ElementType: types.StringType}, + "chain": schema.StringAttribute{Optional: true}, + "helper": schema.StringAttribute{Description: "Conntrack helper (ftp, sip, tftp, ...).", Optional: true}, + "user": schema.StringAttribute{Optional: true}, + "comment": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *conntrackResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *conntrackResource) body(ctx context.Context, plan conntrackModel, diags *diag.Diagnostics) conntrackAPI { + return conntrackAPI{ + Priority: int(plan.Priority.ValueInt64()), + Action: plan.Action.ValueString(), + Source: plan.Source.ValueString(), + Dest: plan.Dest.ValueString(), + Proto: plan.Proto.ValueString(), + DPort: listToStrings(ctx, plan.DPort, diags), + SPort: listToStrings(ctx, plan.SPort, diags), + Chain: plan.Chain.ValueString(), + Helper: plan.Helper.ValueString(), + User: plan.User.ValueString(), + Comment: plan.Comment.ValueString(), + } +} + +func (r *conntrackResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan conntrackModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out conntrackAPI + if err := r.client.post(ctx, "/api/v1/conntrack", body, &out); err != nil { + resp.Diagnostics.AddError("create conntrack failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *conntrackResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state conntrackModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + body := r.body(ctx, plan, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + var out conntrackAPI + if err := r.client.post(ctx, "/api/v1/conntrack", body, &out); err != nil { + resp.Diagnostics.AddError("recreate conntrack failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/conntrack/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old conntrack failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, plan, &resp.Diagnostics))...) +} + +func (r *conntrackResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state conntrackModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out conntrackAPI + if err := r.client.get(ctx, "/api/v1/conntrack/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read conntrack failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(ctx, out, state, &resp.Diagnostics))...) +} + +func (r *conntrackResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state conntrackModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/conntrack/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete conntrack failed", err.Error()) + } +} + +func (r *conntrackResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "conntrack id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *conntrackResource) toModel(ctx context.Context, api conntrackAPI, prior conntrackModel, diags *diag.Diagnostics) conntrackModel { + return conntrackModel{ + ID: types.Int64Value(api.ID), + Priority: types.Int64Value(int64(api.Priority)), + Action: types.StringValue(api.Action), + Source: optionalString(api.Source, prior.Source), + Dest: optionalString(api.Dest, prior.Dest), + Proto: optionalString(api.Proto, prior.Proto), + DPort: optionalList(ctx, api.DPort, prior.DPort, diags), + SPort: optionalList(ctx, api.SPort, prior.SPort, diags), + Chain: optionalString(api.Chain, prior.Chain), + Helper: optionalString(api.Helper, prior.Helper), + User: optionalString(api.User, prior.User), + Comment: optionalString(api.Comment, prior.Comment), + } +} diff --git a/internal/provider/resource_policy.go b/internal/provider/resource_policy.go new file mode 100644 index 0000000..0e4a62b --- /dev/null +++ b/internal/provider/resource_policy.go @@ -0,0 +1,165 @@ +package provider + +import ( + "context" + "strconv" + + "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/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &policyResource{} + _ resource.ResourceWithImportState = &policyResource{} +) + +type policyResource struct{ client *apiClient } + +type policyModel struct { + ID types.Int64 `tfsdk:"id"` + Priority types.Int64 `tfsdk:"priority"` + Source types.String `tfsdk:"source"` + Dest types.String `tfsdk:"dest"` + Action types.String `tfsdk:"action"` + Log types.String `tfsdk:"log"` +} + +type policyAPI struct { + ID int64 `json:"id,omitempty"` + Priority int `json:"priority"` + Source string `json:"source"` + Dest string `json:"dest"` + Action string `json:"action"` + Log string `json:"log,omitempty"` +} + +func NewPolicyResource() resource.Resource { return &policyResource{} } + +func (r *policyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_policy" +} + +func (r *policyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "A default zone-to-zone policy (fleet posture). Lower priority is evaluated first.", + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + PlanModifiers: []planmodifier.Int64{int64planmodifier.UseStateForUnknown()}, + }, + "priority": schema.Int64Attribute{ + Description: "Evaluation priority; lower first.", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(0), + }, + "source": schema.StringAttribute{Description: "Source zone (or all).", Required: true}, + "dest": schema.StringAttribute{Description: "Dest zone (or all).", Required: true}, + "action": schema.StringAttribute{Description: "accept, drop, reject, continue, none, ...", Required: true}, + "log": schema.StringAttribute{Optional: true}, + }, + } +} + +func (r *policyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = configureClient(req, resp) +} + +func (r *policyResource) body(plan policyModel) policyAPI { + return policyAPI{ + Priority: int(plan.Priority.ValueInt64()), + Source: plan.Source.ValueString(), + Dest: plan.Dest.ValueString(), + Action: plan.Action.ValueString(), + Log: plan.Log.ValueString(), + } +} + +func (r *policyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan policyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + var out policyAPI + if err := r.client.post(ctx, "/api/v1/policies", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("create policy failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *policyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state policyModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out policyAPI + if err := r.client.post(ctx, "/api/v1/policies", r.body(plan), &out); err != nil { + resp.Diagnostics.AddError("recreate policy failed", err.Error()) + return + } + if id := state.ID.ValueInt64(); id != 0 { + if err := r.client.del(ctx, "/api/v1/policies/"+strconv.FormatInt(id, 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete old policy failed", err.Error()) + return + } + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, plan))...) +} + +func (r *policyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state policyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + var out policyAPI + if err := r.client.get(ctx, "/api/v1/policies/"+strconv.FormatInt(state.ID.ValueInt64(), 10), &out); err != nil { + if isNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + resp.Diagnostics.AddError("read policy failed", err.Error()) + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, r.toModel(out, state))...) +} + +func (r *policyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state policyModel + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + if err := r.client.del(ctx, "/api/v1/policies/"+strconv.FormatInt(state.ID.ValueInt64(), 10)); err != nil && !isNotFound(err) { + resp.Diagnostics.AddError("delete policy failed", err.Error()) + } +} + +func (r *policyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + id, err := strconv.ParseInt(req.ID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("invalid import ID", "policy id must be an integer") + return + } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) +} + +func (r *policyResource) toModel(api policyAPI, prior policyModel) policyModel { + return policyModel{ + ID: types.Int64Value(api.ID), + Priority: types.Int64Value(int64(api.Priority)), + Source: types.StringValue(api.Source), + Dest: types.StringValue(api.Dest), + Action: types.StringValue(api.Action), + Log: optionalString(api.Log, prior.Log), + } +}