-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add management of DNS rewrite rules (#26)
- Loading branch information
Showing
14 changed files
with
528 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package adguard | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/gmichels/adguard-client-go" | ||
"github.com/hashicorp/terraform-plugin-framework/datasource" | ||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
) | ||
|
||
// ensure the implementation satisfies the expected interfaces | ||
var ( | ||
_ datasource.DataSource = &rewriteDataSource{} | ||
_ datasource.DataSourceWithConfigure = &rewriteDataSource{} | ||
) | ||
|
||
// rewriteDataSource is the data source implementation | ||
type rewriteDataSource struct { | ||
adg *adguard.ADG | ||
} | ||
|
||
// rewriteDataModel maps rewrite schema data | ||
type rewriteDataModel struct { | ||
ID types.String `tfsdk:"id"` | ||
Domain types.String `tfsdk:"domain"` | ||
Answer types.String `tfsdk:"answer"` | ||
} | ||
|
||
// NewRewriteDataSource is a helper function to simplify the provider implementation | ||
func NewRewriteDataSource() datasource.DataSource { | ||
return &rewriteDataSource{} | ||
} | ||
|
||
// Metadata returns the data source type name | ||
func (d *rewriteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_rewrite" | ||
} | ||
|
||
// Schema defines the schema for the data source | ||
func (d *rewriteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
Attributes: map[string]schema.Attribute{ | ||
"id": schema.StringAttribute{ | ||
Description: "Identifier attribute", | ||
Computed: true, | ||
}, | ||
"domain": schema.StringAttribute{ | ||
Description: "Domain name", | ||
Required: true, | ||
}, | ||
"answer": schema.StringAttribute{ | ||
Description: "Value of A, AAAA or CNAME DNS record", | ||
Computed: true, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
// Read refreshes the Terraform state with the latest data | ||
func (d *rewriteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { | ||
// read Terraform configuration data into the model | ||
var state rewriteDataModel | ||
diags := req.Config.Get(ctx, &state) | ||
resp.Diagnostics.Append(diags...) | ||
|
||
// retrieve rewrite info | ||
rewrite, err := d.adg.GetRewrite(state.Domain.ValueString()) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Unable to Read AdGuard Home Rewrite Rule", | ||
err.Error(), | ||
) | ||
return | ||
} | ||
if rewrite == nil { | ||
resp.Diagnostics.AddError( | ||
"Unable to Locate AdGuard Home Rewrite Rule", | ||
"No rewrite rule with name `"+state.Domain.ValueString()+"` exists in AdGuard Home.", | ||
) | ||
return | ||
} | ||
|
||
// map response body to model | ||
state.Domain = types.StringValue(rewrite.Domain) | ||
state.Answer = types.StringValue(rewrite.Answer) | ||
|
||
// set ID placeholder for testing | ||
state.ID = types.StringValue("placeholder") | ||
|
||
// set state | ||
diags = resp.State.Set(ctx, &state) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} | ||
|
||
// Configure adds the provider configured rewrite to the data source | ||
func (d *rewriteDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { | ||
if req.ProviderData == nil { | ||
return | ||
} | ||
|
||
d.adg = req.ProviderData.(*adguard.ADG) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package adguard | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" | ||
) | ||
|
||
func TestAccRewriteDataSource(t *testing.T) { | ||
resource.Test(t, resource.TestCase{ | ||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, | ||
Steps: []resource.TestStep{ | ||
// Read testing | ||
{ | ||
Config: providerConfig + `data "adguard_rewrite" "test" { domain = "example.org" }`, | ||
Check: resource.ComposeAggregateTestCheckFunc( | ||
resource.TestCheckResourceAttr("data.adguard_rewrite.test", "domain", "example.org"), | ||
resource.TestCheckResourceAttr("data.adguard_rewrite.test", "answer", "1.2.3.4"), | ||
|
||
// Verify placeholder id attribute | ||
resource.TestCheckResourceAttr("data.adguard_rewrite.test", "id", "placeholder"), | ||
), | ||
}, | ||
}, | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,230 @@ | ||
package adguard | ||
|
||
import ( | ||
"context" | ||
"regexp" | ||
"time" | ||
|
||
"github.com/gmichels/adguard-client-go" | ||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" | ||
"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/planmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" | ||
"github.com/hashicorp/terraform-plugin-framework/schema/validator" | ||
"github.com/hashicorp/terraform-plugin-framework/types" | ||
) | ||
|
||
// ensure the implementation satisfies the expected interfaces | ||
var ( | ||
_ resource.Resource = &rewriteResource{} | ||
_ resource.ResourceWithConfigure = &rewriteResource{} | ||
_ resource.ResourceWithImportState = &rewriteResource{} | ||
) | ||
|
||
// rewriteResource is the resource implementation | ||
type rewriteResource struct { | ||
adg *adguard.ADG | ||
} | ||
|
||
// rewriteResourceModel maps DNS rewrite rule schema data | ||
type rewriteResourceModel struct { | ||
ID types.String `tfsdk:"id"` | ||
LastUpdated types.String `tfsdk:"last_updated"` | ||
Domain types.String `tfsdk:"domain"` | ||
Answer types.String `tfsdk:"answer"` | ||
} | ||
|
||
// NewRewriteResource is a helper function to simplify the provider implementation | ||
func NewRewriteResource() resource.Resource { | ||
return &rewriteResource{} | ||
} | ||
|
||
// Metadata returns the resource type name | ||
func (r *rewriteResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { | ||
resp.TypeName = req.ProviderTypeName + "_rewrite" | ||
} | ||
|
||
// Schema defines the schema for the resource | ||
func (r *rewriteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { | ||
resp.Schema = schema.Schema{ | ||
Attributes: map[string]schema.Attribute{ | ||
"id": schema.StringAttribute{ | ||
Description: "Internal identifier for this rewrite", | ||
Computed: true, | ||
PlanModifiers: []planmodifier.String{ | ||
stringplanmodifier.UseStateForUnknown(), | ||
}, | ||
}, | ||
"last_updated": schema.StringAttribute{ | ||
Description: "Timestamp of the last Terraform update of the rewrite", | ||
Computed: true, | ||
}, | ||
"domain": schema.StringAttribute{ | ||
Description: "Domain name", | ||
Required: true, | ||
}, | ||
"answer": schema.StringAttribute{ | ||
Description: "Value of A, AAAA or CNAME DNS record", | ||
Required: true, | ||
Validators: []validator.String{ | ||
stringvalidator.RegexMatches( | ||
regexp.MustCompile(`^[a-z0-9/.:-]+$`), | ||
"must be an IP address/CIDR, MAC address, or only contain numbers, lowercase letters, and hyphens", | ||
), | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
// Configure adds the provider configured DNS rewrite rule to the resource | ||
func (r *rewriteResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { | ||
if req.ProviderData == nil { | ||
return | ||
} | ||
|
||
r.adg = req.ProviderData.(*adguard.ADG) | ||
} | ||
|
||
// Create creates the resource and sets the initial Terraform state | ||
func (r *rewriteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { | ||
// retrieve values from plan | ||
var plan rewriteResourceModel | ||
diags := req.Plan.Get(ctx, &plan) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// instantiate empty DNS rewrite rule for storing plan data | ||
var rewrite adguard.RewriteEntry | ||
|
||
// populate DNS rewrite rule from plan | ||
rewrite.Domain = plan.Domain.ValueString() | ||
rewrite.Answer = plan.Answer.ValueString() | ||
|
||
// create new DNS rewrite rule using plan | ||
newRewrite, err := r.adg.CreateRewrite(rewrite) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error Creating DNS Rewrite Rule", | ||
"Could not create DNS rewrite rule, unexpected error: "+err.Error(), | ||
) | ||
return | ||
} | ||
|
||
// response sent by AdGuard Home is the same as the sent payload, | ||
// just add missing attributes for state | ||
plan.ID = types.StringValue(newRewrite.Domain) | ||
// add the last updated attribute | ||
plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) | ||
|
||
// set state to fully populated data | ||
diags = resp.State.Set(ctx, plan) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} | ||
|
||
// Read refreshes the Terraform state with the latest data | ||
func (r *rewriteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { | ||
// get current state | ||
var state rewriteResourceModel | ||
diags := req.State.Get(ctx, &state) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// get refreshed DNS rewrite rule value from AdGuard Home | ||
rewrite, err := r.adg.GetRewrite(state.ID.ValueString()) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error Reading AdGuard Home DNS Rewrite Rule", | ||
"Could not read AdGuard Home DNS rewrite rule with ID "+state.ID.ValueString()+": "+err.Error(), | ||
) | ||
return | ||
} else if rewrite == nil { | ||
resp.Diagnostics.AddError( | ||
"Error Reading AdGuard Home DNS Rewrite Rule", | ||
"No such AdGuard Home DNS rewrite rule with ID "+state.ID.ValueString(), | ||
) | ||
return | ||
} | ||
|
||
// overwrite DNS rewrite rule with refreshed state | ||
state.Domain = types.StringValue(rewrite.Domain) | ||
state.Answer = types.StringValue(rewrite.Answer) | ||
|
||
// set refreshed state | ||
diags = resp.State.Set(ctx, &state) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} | ||
|
||
// Update updates the resource and sets the updated Terraform state on success | ||
func (r *rewriteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { | ||
// retrieve values from plan | ||
var plan rewriteResourceModel | ||
diags := req.Plan.Get(ctx, &plan) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// generate API request body from plan | ||
var updateRewrite adguard.RewriteEntry | ||
updateRewrite.Domain = plan.Domain.ValueString() | ||
updateRewrite.Answer = plan.Answer.ValueString() | ||
|
||
// update existing DNS rewrite rule | ||
_, err := r.adg.UpdateRewrite(updateRewrite) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error Updating AdGuard Home DNS Rewrite Rule", | ||
"Could not update DNS rewrite rule, unexpected error: "+err.Error(), | ||
) | ||
return | ||
} | ||
|
||
// update resource state with updated items and timestamp | ||
plan.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) | ||
|
||
// update state | ||
diags = resp.State.Set(ctx, plan) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
} | ||
|
||
// Delete deletes the resource and removes the Terraform state on success | ||
func (r *rewriteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { | ||
// retrieve values from state | ||
var state rewriteResourceModel | ||
diags := req.State.Get(ctx, &state) | ||
resp.Diagnostics.Append(diags...) | ||
if resp.Diagnostics.HasError() { | ||
return | ||
} | ||
|
||
// delete existing DNS rewrite rule | ||
err := r.adg.DeleteRewrite(state.ID.ValueString()) | ||
if err != nil { | ||
resp.Diagnostics.AddError( | ||
"Error Deleting AdGuard Home DNS Rewrite Rule", | ||
"Could not delete DNS rewrite rule, unexpected error: "+err.Error(), | ||
) | ||
return | ||
} | ||
} | ||
|
||
func (r *rewriteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { | ||
// retrieve import ID and save to id attribute | ||
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) | ||
} |
Oops, something went wrong.