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

Implement Zone resource #184

Merged
merged 3 commits into from
Jan 17, 2024
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
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