Skip to content

Commit

Permalink
Implement Zone resource (#184)
Browse files Browse the repository at this point in the history
We are adding a zone resource in favor of the `dnsimple_zone` data source, so we can add support for the zone active state management. With the new resource, developers can deactivate or activate zones, essentially managing whether a zone is publicly resolving or not. More on the API endpoints used [here](https://developer.dnsimple.com/v2/zones/#activateZoneService).
  • Loading branch information
DXTimer authored Jan 17, 2024
1 parent 2d68da9 commit 4079e6d
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 0 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## main

FEATURES:

- **New Resource:** `dnsimple_zone` (dnsimple/terraform-provider-dnsimple#184)

NOTES:

- The `dnsimple_zone` data source is now deprecated and will be removed in a future release. Please migrate to the `dnsimple_zone` resource.

## 1.3.1

BUG FIXES:
Expand Down
2 changes: 2 additions & 0 deletions docs/data-sources/zone.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ page_title: "DNSimple: dnsimple_zone"

Get information about a DNSimple zone.

!> Data source is getting deprecated in favor of [`dnsimple\_zone`](../resources/zone.md) resource.

# Example Usage

Get zone:
Expand Down
60 changes: 60 additions & 0 deletions docs/resources/zone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
page_title: "DNSimple: dnsimple_zone"
---

# dnsimple\_zone

Provides a DNSimple zone resource.

-> Currently the resource creation acts as an import, so the zone must already exist in DNSimple. The only attribute that will be modified during resource creation is the `active` state of the zone. This is because our API does not allow for the creation of zones. Creation of zones happens through the purchase or creation of domains. We expect this behavior to change in the future.

## Example Usage

```hcl
# Create a zone
resource "dnsimple_zone" "foobar" {
name = "${var.dnsimple.zone}"
}
```

## Argument Reference

The following argument(s) are supported:

* `name` - (Required) The zone name

# Attributes Reference

- `id` - The ID of this resource.
- `account_id` - The account ID for the zone.
- `reverse` - Whether the zone is a reverse zone.
- `secondary` - Whether the zone is a secondary zone.
- `active` - Whether the zone is active.
- `last_transferred_at` - The last time the zone was transferred only applicable for **secondary** zones.

## Import

DNSimple zones can be imported using their numeric record ID or the zone name.

```bash
terraform import dnsimple_zone.resource_name foo.com
```

The zone ID can be found within [DNSimple Zones API](https://developer.dnsimple.com/v2/zones/#getZone). Check out [Authentication](https://developer.dnsimple.com/v2/#authentication) in API Overview for available options.

```bash
curl -H 'Authorization: Bearer <ACCESS_TOKEN>' https://api.dnsimple.com/v2/1234/zones/example.com | jq
{
"data": {
"id": 1,
"account_id": 1234,
"name": "example.com",
"reverse": false,
"secondary": false,
"last_transferred_at": null,
"active": true,
"created_at": "2023-04-18T04:58:01Z",
"updated_at": "2024-01-16T15:53:18Z"
}
}
```
1 change: 1 addition & 0 deletions internal/framework/datasources/zone_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (d *ZoneDataSource) Schema(ctx context.Context, req datasource.SchemaReques
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "DNSimple zone data source",
DeprecationMessage: "This data source is deprecated. Please use the dnsimple_zone resource instead.",

Attributes: map[string]schema.Attribute{
"id": common.IDInt64Attribute(),
Expand Down
1 change: 1 addition & 0 deletions internal/framework/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func (p *DnsimpleProvider) Resources(ctx context.Context) []func() resource.Reso
resources.NewEmailForwardResource,
resources.NewLetsEncryptCertificateResource,
resources.NewZoneRecordResource,
resources.NewZoneResource,
}
}

Expand Down
278 changes: 278 additions & 0 deletions internal/framework/resources/zone_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package resources

import (
"context"
"errors"
"fmt"

"github.com/dnsimple/dnsimple-go/dnsimple"
"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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/common"
"github.com/terraform-providers/terraform-provider-dnsimple/internal/framework/utils"
)

// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &ZoneResource{}
_ resource.ResourceWithConfigure = &ZoneResource{}
_ resource.ResourceWithImportState = &ZoneResource{}
)

func NewZoneResource() resource.Resource {
return &ZoneResource{}
}

// ZoneResource defines the resource implementation.
type ZoneResource struct {
config *common.DnsimpleProviderConfig
}

// ZoneResourceModel describes the resource data model.
type ZoneResourceModel struct {
Name types.String `tfsdk:"name"`
AccountId types.Int64 `tfsdk:"account_id"`
Reverse types.Bool `tfsdk:"reverse"`
Secondary types.Bool `tfsdk:"secondary"`
Active types.Bool `tfsdk:"active"`
LastTransferredAt types.String `tfsdk:"last_transferred_at"`
Id types.Int64 `tfsdk:"id"`
}

func (r *ZoneResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_zone"
}

func (r *ZoneResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "DNSimple zone resource",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"account_id": schema.Int64Attribute{
Computed: true,
},
"reverse": schema.BoolAttribute{
Computed: true,
},
"secondary": schema.BoolAttribute{
Computed: true,
},
"active": schema.BoolAttribute{
Optional: true,
Computed: true,
},
"last_transferred_at": schema.StringAttribute{
Computed: true,
},
"id": common.IDInt64Attribute(),
},
}
}

func (r *ZoneResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

config, ok := req.ProviderData.(*common.DnsimpleProviderConfig)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *provider.DnsimpleProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.config = config
}

func (r *ZoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data *ZoneResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

response, err := r.config.Client.Zones.GetZone(ctx, r.config.AccountID, data.Name.ValueString())

if err != nil {
var errorResponse *dnsimple.ErrorResponse
if errors.As(err, &errorResponse) {
resp.Diagnostics.Append(utils.AttributeErrorsToDiagnostics(errorResponse)...)
return
}

resp.Diagnostics.AddError(
"failed to retrieve DNSimple Zone",
err.Error(),
)
return
}

if !(data.Active.IsUnknown() || data.Active.IsNull()) && data.Active.ValueBool() != response.Data.Active {
zone, diags := r.setActiveState(ctx, data)

if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

r.updateModelFromAPIResponse(zone, data)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

return
}

r.updateModelFromAPIResponse(response.Data, data)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ZoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data *ZoneResourceModel

// Read Terraform prior state data into the model
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

response, err := r.config.Client.Zones.GetZone(ctx, r.config.AccountID, data.Name.ValueString())

if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("failed to read DNSimple Zone: %s", data.Name.ValueString()),
err.Error(),
)
return
}

r.updateModelFromAPIResponse(response.Data, data)

// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *ZoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var (
configData *ZoneResourceModel
planData *ZoneResourceModel
stateData *ZoneResourceModel
)

resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
if resp.Diagnostics.HasError() {
return
}

resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
if resp.Diagnostics.HasError() {
return
}

resp.Diagnostics.Append(req.Config.Get(ctx, &configData)...)
if resp.Diagnostics.HasError() {
return
}

if !(planData.Active.IsUnknown() || planData.Active.IsNull()) && planData.Active.ValueBool() != stateData.Active.ValueBool() {
zone, diags := r.setActiveState(ctx, planData)

if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}

r.updateModelFromAPIResponse(zone, planData)

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &planData)...)

return
}
}

func (r *ZoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data *ZoneResourceModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

tflog.Warn(ctx, fmt.Sprintf("Removing DNSimple Zone from Terraform state only: %s, %s", data.Name, data.Id))
}

func (r *ZoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
response, err := r.config.Client.Zones.GetZone(ctx, r.config.AccountID, req.ID)

if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("failed to find DNSimple Zone ID: %s", req.ID),
err.Error(),
)
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), response.Data.ID)...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), response.Data.Name)...)
}

func (r *ZoneResource) updateModelFromAPIResponse(zone *dnsimple.Zone, data *ZoneResourceModel) {
data.Id = types.Int64Value(zone.ID)
data.Name = types.StringValue(zone.Name)
data.AccountId = types.Int64Value(zone.AccountID)
data.Reverse = types.BoolValue(zone.Reverse)
data.Secondary = types.BoolValue(zone.Secondary)
data.Active = types.BoolValue(zone.Active)
data.LastTransferredAt = types.StringValue(zone.LastTransferredAt)
}

func (r *ZoneResource) setActiveState(ctx context.Context, data *ZoneResourceModel) (*dnsimple.Zone, diag.Diagnostics) {
diagnostics := diag.Diagnostics{}

tflog.Debug(ctx, fmt.Sprintf("setting active to %t", data.Active.ValueBool()))

if data.Active.ValueBool() {
zoneResponse, err := r.config.Client.Zones.ActivateZoneDns(ctx, r.config.AccountID, data.Name.ValueString())
if err != nil {
diagnostics.AddError(
fmt.Sprintf("failed to activate DNSimple Zone: %s, %d", data.Name.ValueString(), data.Id.ValueInt64()),
err.Error(),
)
}
return zoneResponse.Data, diagnostics
}

zoneResponse, err := r.config.Client.Zones.DeactivateZoneDns(ctx, r.config.AccountID, data.Name.ValueString())
if err != nil {
diagnostics.AddError(
fmt.Sprintf("failed to deactivate DNSimple Zone: %s, %d", data.Name.ValueString(), data.Id.ValueInt64()),
err.Error(),
)
}

return zoneResponse.Data, diagnostics
}
Loading

0 comments on commit 4079e6d

Please sign in to comment.