Skip to content

Commit

Permalink
feat: Recreate manually deleted resources (#131)
Browse files Browse the repository at this point in the history
  • Loading branch information
biblicalph authored Feb 21, 2024
1 parent 31aa741 commit dbd96b1
Show file tree
Hide file tree
Showing 59 changed files with 1,567 additions and 43 deletions.
10 changes: 2 additions & 8 deletions internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,8 @@ func readBody(resp *http.Response, err error) error {
return err
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)

if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {
return fmt.Errorf("%s: %s", resp.Status, string(body))
return newAPIError(resp)
}

return err
Expand All @@ -125,11 +122,8 @@ func readJson(result any, resp *http.Response, err error) error {
return err
}

defer resp.Body.Close()

if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusNoContent {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("%s: %s", resp.Status, string(body))
return newAPIError(resp)
}

return json.NewDecoder(resp.Body).Decode(result)
Expand Down
40 changes: 40 additions & 0 deletions internal/client/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package client

import (
"fmt"
"io"
"net/http"
)

type apiResponseError struct {
body string
status uint16
}

func newAPIError(resp *http.Response) apiResponseError {
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)

status := uint16(resp.StatusCode)
body := ""
if err != nil {
body = fmt.Sprintf("failed to read response body. reason: %s", err.Error())
} else {
body = string(respBody)
}

return apiResponseError{
body: body,
status: status,
}
}

// implement errors.Error interface
func (e apiResponseError) Error() string {
return fmt.Sprintf("%d: %s", e.status, e.body)
}

func IsNotFoundError(target error) bool {
err, ok := target.(apiResponseError)
return ok && err.status == http.StatusNotFound
}
9 changes: 8 additions & 1 deletion internal/provider/destination_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"os"
"reflect"
"strings"

"github.com/hashicorp/terraform-plugin-framework/path"

"github.com/hashicorp/terraform-plugin-framework/resource/schema"

"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/mezmo/terraform-provider-mezmo/internal/client"
. "github.com/mezmo/terraform-provider-mezmo/internal/client"
. "github.com/mezmo/terraform-provider-mezmo/internal/provider/models/destinations"
. "github.com/mezmo/terraform-provider-mezmo/internal/provider/models/modelutils"
Expand Down Expand Up @@ -169,6 +171,11 @@ func (r *DestinationResource[T]) Read(ctx context.Context, req resource.ReadRequ
}

component, err := r.client.Destination(r.getPipelineIdFunc(&state).ValueString(), r.getIdFunc(&state).ValueString())
// force re-creation of manually deleted resources
if client.IsNotFoundError(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError(
"Error reading destination",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,32 @@ func TestAzureBlobStorageDestinationResource(t *testing.T) {
}),
),
},

// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_azure_blob_storage_destination" "new_destination" {
pipeline_id = mezmo_pipeline.test_parent2.id
title = "new title"
inputs = []
connection_string = "abc://defg.com"
container_name = "my_container"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_azure_blob_storage_destination.new_destination", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_azure_blob_storage_destination.new_destination", "title", "new title"),
// delete the resource
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_azure_blob_storage_destination.new_destination",
),
),
// verify resource will be re-created after refresh
ExpectNonEmptyPlan: true,
},
// Delete testing automatically occurs in TestCase
},
})
Expand Down
23 changes: 23 additions & 0 deletions internal/provider/models/destinations/test/blackhole_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ func TestBlackholeDestinationResource(t *testing.T) {
}),
),
},
// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_blackhole_destination" "destination_2" {
pipeline_id = mezmo_pipeline.test_parent2.id
title = "new title"
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_blackhole_destination.destination_2", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_blackhole_destination.destination_2", "title", "new title"),
// delete the resource
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_blackhole_destination.destination_2",
),
),
// verify resource will be re-created after refresh
ExpectNonEmptyPlan: true,
},
// Delete testing automatically occurs in TestCase
},
})
Expand Down
29 changes: 29 additions & 0 deletions internal/provider/models/destinations/test/datadog_logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,35 @@ func TestDatadogLogsDestinationResource(t *testing.T) {
}),
),
},
// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_http_source" "my_source2" {
pipeline_id = mezmo_pipeline.test_parent2.id
}
resource "mezmo_datadog_logs_destination" "test_destination" {
pipeline_id = mezmo_pipeline.test_parent2.id
title = "new title"
site = "us3"
api_key = "<secret-api-key>"
compression = "gzip"
inputs = [mezmo_http_source.my_source2.id]
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_datadog_logs_destination.test_destination", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_datadog_logs_destination.test_destination", "title", "new title"),
// verify resource will be re-created after refresh
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_datadog_logs_destination.test_destination",
),
),
ExpectNonEmptyPlan: true,
},
},
})
}
29 changes: 29 additions & 0 deletions internal/provider/models/destinations/test/datadog_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,35 @@ func TestDatadogMetricsDestinationResource(t *testing.T) {
}),
),
},
// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_http_source" "my_source2" {
pipeline_id = mezmo_pipeline.test_parent2.id
}
resource "mezmo_datadog_metrics_destination" "test_destination" {
pipeline_id = mezmo_pipeline.test_parent2.id
title = "new title"
site = "us3"
api_key = "<secret-api-key>"
ack_enabled = true
inputs = [mezmo_http_source.my_source2.id]
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_datadog_metrics_destination.test_destination", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_datadog_metrics_destination.test_destination", "title", "new title"),
// verify resource will be re-created after refresh
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_datadog_metrics_destination.test_destination",
),
),
ExpectNonEmptyPlan: true,
},
},
})
}
32 changes: 32 additions & 0 deletions internal/provider/models/destinations/test/elasticsearch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,38 @@ func TestElasticSearchDestinationResource(t *testing.T) {
}),
),
},
// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_http_source" "my_source2" {
pipeline_id = mezmo_pipeline.test_parent2.id
}
resource "mezmo_elasticsearch_destination" "test_destination" {
pipeline_id = mezmo_pipeline.test_parent2.id
endpoints = ["https://google.com"]
title = "new title"
auth = {
strategy = "basic"
user = "user1"
password = "pass1"
}
inputs = [mezmo_http_source.my_source2.id]
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_elasticsearch_destination.test_destination", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_elasticsearch_destination.test_destination", "title", "new title"),
// verify resource will be re-created after refresh
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_elasticsearch_destination.test_destination",
),
),
ExpectNonEmptyPlan: true,
},

// Delete testing automatically occurs in TestCase
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,40 @@ func TestGcpCloudStorageSinkResource(t *testing.T) {
}),
),
},
// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_http_source" "my_source2" {
pipeline_id = mezmo_pipeline.test_parent2.id
}
resource "mezmo_gcp_cloud_storage_destination" "test_destination" {
pipeline_id = mezmo_pipeline.test_parent2.id
title = "new title"
encoding = "json"
compression = "gzip"
bucket = "test_bucket"
bucket_prefix = "bucket_prefix"
auth = {
type = "api_key"
value = "key"
}
inputs = [mezmo_http_source.my_source2.id]
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_gcp_cloud_storage_destination.test_destination", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_gcp_cloud_storage_destination.test_destination", "title", "new title"),
// verify resource will be re-created after refresh
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_gcp_cloud_storage_destination.test_destination",
),
),
ExpectNonEmptyPlan: true,
},
},
})
}
28 changes: 28 additions & 0 deletions internal/provider/models/destinations/test/honeycomb_logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,34 @@ func TestHoneycombLogsDestinationResource(t *testing.T) {
}),
),
},
// confirm manually deleted resources are recreated
{
Config: GetProviderConfig() + `
resource "mezmo_pipeline" "test_parent2" {
title = "pipeline"
}
resource "mezmo_http_source" "my_source2" {
pipeline_id = mezmo_pipeline.test_parent2.id
}
resource "mezmo_honeycomb_logs_destination" "test_destination" {
pipeline_id = mezmo_pipeline.test_parent2.id
title = "new title"
dataset = "ds3"
api_key = "key2"
inputs = [mezmo_http_source.my_source2.id]
}`,
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestMatchResourceAttr(
"mezmo_honeycomb_logs_destination.test_destination", "id", regexp.MustCompile(`[\w-]{36}`)),
resource.TestCheckResourceAttr("mezmo_honeycomb_logs_destination.test_destination", "title", "new title"),
// verify resource will be re-created after refresh
TestDeletePipelineNodeManually(
"mezmo_pipeline.test_parent2",
"mezmo_honeycomb_logs_destination.test_destination",
),
),
ExpectNonEmptyPlan: true,
},

// Delete testing automatically occurs in TestCase
},
Expand Down
Loading

0 comments on commit dbd96b1

Please sign in to comment.