Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add management of DNS rewrite rules #26

Merged
merged 5 commits into from
Mar 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions adguard/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ func (p *adguardProvider) DataSources(_ context.Context) []func() datasource.Dat
NewClientDataSource,
NewListFilterDataSource,
NewUserRulesDataSource,
NewRewriteDataSource,
}
}

Expand All @@ -294,5 +295,6 @@ func (p *adguardProvider) Resources(_ context.Context) []func() resource.Resourc
NewClientResource,
NewListFilterResource,
NewUserRulesResource,
NewRewriteResource,
}
}
106 changes: 106 additions & 0 deletions adguard/rewrite_data_source.go
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)
}
26 changes: 26 additions & 0 deletions adguard/rewrite_data_source_test.go
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"),
),
},
},
})
}
230 changes: 230 additions & 0 deletions adguard/rewrite_resource.go
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)
}
Loading