Skip to content

Commit

Permalink
Add in-place update to HCP Log Streaming Destination (#802)
Browse files Browse the repository at this point in the history
* add in-place update functionality to log streaming destination resource

* add changelog
  • Loading branch information
leahrob authored Apr 4, 2024
1 parent 125f2b2 commit ff7a0bc
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 65 deletions.
3 changes: 3 additions & 0 deletions .changelog/802.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:feature
Add in-place update functionality to `hcp_log_streaming_destination` resource.
```
23 changes: 23 additions & 0 deletions internal/clients/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,26 @@ func DeleteLogStreamingDestination(ctx context.Context, client *Client, loc *sha

return nil
}

func UpdateLogStreamingDestination(ctx context.Context, client *Client, loc *sharedmodels.HashicorpCloudLocationLocation, updatePaths []string, destination *models.LogService20210330Destination) error {
updateParams := log_service.NewLogServiceUpdateStreamingDestinationParams()
updateParams.Context = ctx
updateParams.DestinationResourceID = destination.Resource.ID
updateParams.DestinationResourceLocationOrganizationID = loc.OrganizationID
updateParams.DestinationResourceLocationProjectID = loc.ProjectID

updateBody := &models.LogService20210330UpdateStreamingDestinationRequest{
Destination: destination,
Mask: &models.ProtobufFieldMask{
Paths: updatePaths,
},
}

updateParams.Body = updateBody
_, err := client.LogService.LogServiceUpdateStreamingDestination(updateParams, nil)
if err != nil {
return err
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ func (r *resourceHCPLogStreamingDestination) Schema(_ context.Context, _ resourc
stringvalidator.LengthBetween(1, 30),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
"streaming_destination_id": schema.StringAttribute{
Description: "The ID of the HCP Log Streaming Destination",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
stringplanmodifier.UseStateForUnknown(),
},
},
"splunk_cloud": schema.SingleNestedAttribute{
Expand All @@ -77,7 +77,7 @@ func (r *resourceHCPLogStreamingDestination) Schema(_ context.Context, _ resourc
},
},
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
objectplanmodifier.UseStateForUnknown(),
},
Optional: true,
Validators: []validator.Object{
Expand Down Expand Up @@ -109,7 +109,7 @@ func (r *resourceHCPLogStreamingDestination) Schema(_ context.Context, _ resourc
},
},
PlanModifiers: []planmodifier.Object{
objectplanmodifier.RequiresReplace(),
objectplanmodifier.UseStateForUnknown(),
},
Optional: true,
Validators: []validator.Object{
Expand Down Expand Up @@ -307,7 +307,77 @@ func (r *resourceHCPLogStreamingDestination) Read(ctx context.Context, req resou
}

func (r *resourceHCPLogStreamingDestination) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// In-place update is not supported
var plan, state HCPLogStreamingDestination
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)

resp.Diagnostics.Append(plan.extract(ctx)...)
resp.Diagnostics.Append(state.extract(ctx)...)

loc := &sharedmodels.HashicorpCloudLocationLocation{
OrganizationID: r.client.Config.OrganizationID,
ProjectID: r.client.Config.ProjectID,
}

if resp.Diagnostics.HasError() {
return
}

var fieldMaskPaths []string
destination := &models.LogService20210330Destination{}

if !plan.Name.Equal(state.Name) {
fieldMaskPaths = append(fieldMaskPaths, "name")
destination.Name = plan.Name.ValueString()
}

// if tf plan is for cloudwatch
if !plan.CloudWatch.IsNull() {
// check if the saved tf state is also cloudwatch and see if there has been any drift
if !state.CloudWatch.IsNull() && plan.CloudWatch.Equal(state.CloudWatch) {
// do nothing ... state has not changed
} else {
// if there is a diff between plan and state we need to call log service to update destination
fieldMaskPaths = append(fieldMaskPaths, "provider")
destination.CloudwatchlogsProvider = &models.LogService20210330CloudwatchLogsProvider{
ExternalID: plan.cloudwatch.ExternalID.ValueString(),
Region: plan.cloudwatch.Region.ValueString(),
RoleArn: plan.cloudwatch.RoleArn.ValueString(),
LogGroupName: plan.cloudwatch.LogGroupName.ValueString(),
}
}
}

// if tf plan is for splunk
if !plan.SplunkCloud.IsNull() {
if !state.SplunkCloud.IsNull() && plan.SplunkCloud.Equal(state.SplunkCloud) {
// do nothing ... state has not changed
} else {
// if there is a diff between plan and state we need to call log service to update destination
fieldMaskPaths = append(fieldMaskPaths, "provider")
destination.SplunkCloudProvider = &models.LogService20210330SplunkCloudProvider{
HecEndpoint: plan.splunkCloud.HecEndpoint.ValueString(),
Token: plan.splunkCloud.Token.ValueString(),
}
}
}

if len(fieldMaskPaths) > 0 {
destination.Resource = &models.LocationLink{
ID: state.StreamingDestinationID.ValueString(),
Location: &models.CloudlocationLocation{
OrganizationID: loc.OrganizationID,
ProjectID: loc.ProjectID,
},
}
err := clients.UpdateLogStreamingDestination(ctx, r.client, loc, fieldMaskPaths, destination)
if err != nil {
resp.Diagnostics.AddError("Error updating log streaming destination", err.Error())
return
}

resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}
}
func (r *resourceHCPLogStreamingDestination) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state HCPLogStreamingDestination
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
"fmt"
"testing"

"github.com/hashicorp/hcp-sdk-go/clients/cloud-log-service/preview/2021-03-30/models"
sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/terraform"

"github.com/hashicorp/terraform-provider-hcp/internal/clients"
Expand All @@ -21,8 +21,6 @@ func TestAccHCPLogStreamingDestinationSplunk(t *testing.T) {
resourceName := "hcp_log_streaming_destination.test_splunk_cloud"
spName := "splunk-resource-name-1"
spNameUpdated := "splunk-resource-name-2"
var sp models.LogService20210330Destination
var sp2 models.LogService20210330Destination

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
Expand All @@ -39,7 +37,7 @@ func TestAccHCPLogStreamingDestinationSplunk(t *testing.T) {
{
Config: testAccSplunkConfig(spName),
Check: resource.ComposeTestCheckFunc(
testAccHCPLogStreamingDestinationExists(t, resourceName, &sp),
testAccHCPLogStreamingDestinationExists(t, resourceName),
resource.TestCheckResourceAttr(resourceName, "name", spName),
resource.TestCheckResourceAttrSet(resourceName, "splunk_cloud.endpoint"),
resource.TestCheckResourceAttrSet(resourceName, "splunk_cloud.token"),
Expand All @@ -48,35 +46,54 @@ func TestAccHCPLogStreamingDestinationSplunk(t *testing.T) {
),
},
{
// Update the name
Config: testAccSplunkConfig(spNameUpdated),
// Update the name and token and expect in-place update
Config: testAccSplunkConfigUpdated(spNameUpdated),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate),
},
},
Check: resource.ComposeTestCheckFunc(
testAccHCPLogStreamingDestinationExists(t, resourceName, &sp2),
testAccHCPLogStreamingDestinationExists(t, resourceName),
resource.TestCheckResourceAttr(resourceName, "name", spNameUpdated),
resource.TestCheckResourceAttrSet(resourceName, "splunk_cloud.endpoint"),
resource.TestCheckResourceAttrSet(resourceName, "splunk_cloud.token"),
resource.TestCheckResourceAttr(resourceName, "splunk_cloud.endpoint", "https://http-inputs-hcptest.splunkcloud.com/services/collector/event"),
resource.TestCheckResourceAttr(resourceName, "splunk_cloud.token", "splunk-authentication-token"),
func(_ *terraform.State) error {
if sp.Resource.ID == sp2.Resource.ID {
return fmt.Errorf("resource_ids match, indicating resource wasn't recreated")
}
return nil
},
resource.TestCheckResourceAttr(resourceName, "splunk_cloud.token", "splunk-authentication-token234"),
),
},
},
})
}

func testAccSplunkConfig(name string) string {
return fmt.Sprintf(`
resource "hcp_log_streaming_destination" "test_splunk_cloud" {
name = "%[1]s"
splunk_cloud = {
endpoint = "https://http-inputs-hcptest.splunkcloud.com/services/collector/event"
token = "splunk-authentication-token"
}
}
`, name)
}

func testAccSplunkConfigUpdated(name string) string {
return fmt.Sprintf(`
resource "hcp_log_streaming_destination" "test_splunk_cloud" {
name = "%[1]s"
splunk_cloud = {
endpoint = "https://http-inputs-hcptest.splunkcloud.com/services/collector/event"
token = "splunk-authentication-token234"
}
}
`, name)
}

func TestAccHCPLogStreamingDestinationCloudWatch(t *testing.T) {
resourceName := "hcp_log_streaming_destination.test_cloudwatch"
cwName := "cloudwatch-resource-name-1"
cwNameUpdated := "cloudwatch-resource-name-2"
cwNameLogGroup := "cloudwatch-resource-name-3"
var cw models.LogService20210330Destination
var cw2 models.LogService20210330Destination
var cw3 models.LogService20210330Destination

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
Expand All @@ -93,7 +110,7 @@ func TestAccHCPLogStreamingDestinationCloudWatch(t *testing.T) {
{
Config: testAccCloudWatchLogsConfig(cwName),
Check: resource.ComposeTestCheckFunc(
testAccHCPLogStreamingDestinationExists(t, resourceName, &cw),
testAccHCPLogStreamingDestinationExists(t, resourceName),
resource.TestCheckResourceAttr(resourceName, "name", cwName),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.region"),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.role_arn"),
Expand All @@ -104,37 +121,22 @@ func TestAccHCPLogStreamingDestinationCloudWatch(t *testing.T) {
),
},
{
// Update the name
Config: testAccCloudWatchLogsConfig(cwNameUpdated),
Check: resource.ComposeTestCheckFunc(
testAccHCPLogStreamingDestinationExists(t, resourceName, &cw2),
resource.TestCheckResourceAttr(resourceName, "name", cwNameUpdated),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.region"),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.role_arn"),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.external_id"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.region", "us-west-2"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.external_id", "superSecretExternalID"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.role_arn", "arn:aws:iam::000000000000:role/cloud_watch_role"),
func(_ *terraform.State) error {
if cw.Resource.ID == cw2.Resource.ID {
return fmt.Errorf("resource_ids match, indicating resource wasn't recreated")
}
return nil
// Update the name, log group name and externalID and expect in-place update
Config: testAccCloudWatchLogsConfigUpdated(cwNameUpdated),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate),
},
),
},
{
// test with a log group name
Config: testAccCloudWatchLogsConfigLogGroupName(cwNameLogGroup),
},
Check: resource.ComposeTestCheckFunc(
testAccHCPLogStreamingDestinationExists(t, resourceName, &cw3),
resource.TestCheckResourceAttr(resourceName, "name", cwNameLogGroup),
testAccHCPLogStreamingDestinationExists(t, resourceName),
resource.TestCheckResourceAttr(resourceName, "name", cwNameUpdated),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.region"),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.role_arn"),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.external_id"),
resource.TestCheckResourceAttrSet(resourceName, "cloudwatch.log_group_name"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.region", "us-west-2"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.external_id", "superSecretExternalID"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.external_id", "superSecretExternalID789"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.role_arn", "arn:aws:iam::000000000000:role/cloud_watch_role"),
resource.TestCheckResourceAttr(resourceName, "cloudwatch.log_group_name", "a-log-group-name"),
),
Expand All @@ -143,18 +145,6 @@ func TestAccHCPLogStreamingDestinationCloudWatch(t *testing.T) {
})
}

func testAccSplunkConfig(name string) string {
return fmt.Sprintf(`
resource "hcp_log_streaming_destination" "test_splunk_cloud" {
name = "%[1]s"
splunk_cloud = {
endpoint = "https://http-inputs-hcptest.splunkcloud.com/services/collector/event"
token = "splunk-authentication-token"
}
}
`, name)
}

func testAccCloudWatchLogsConfig(name string) string {
return fmt.Sprintf(`
resource "hcp_log_streaming_destination" "test_cloudwatch" {
Expand All @@ -168,21 +158,21 @@ func testAccCloudWatchLogsConfig(name string) string {
`, name)
}

func testAccCloudWatchLogsConfigLogGroupName(name string) string {
func testAccCloudWatchLogsConfigUpdated(name string) string {
return fmt.Sprintf(`
resource "hcp_log_streaming_destination" "test_cloudwatch" {
name = "%[1]s"
cloudwatch = {
region = "us-west-2"
role_arn = "arn:aws:iam::000000000000:role/cloud_watch_role"
external_id = "superSecretExternalID"
external_id = "superSecretExternalID789"
log_group_name = "a-log-group-name"
}
}
`, name)
}

func testAccHCPLogStreamingDestinationExists(t *testing.T, name string, destination *models.LogService20210330Destination) resource.TestCheckFunc {
func testAccHCPLogStreamingDestinationExists(t *testing.T, name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
Expand All @@ -209,8 +199,6 @@ func testAccHCPLogStreamingDestinationExists(t *testing.T, name string, destinat
return fmt.Errorf("log Streaming Destination (%s) not found", streamingDestinationID)
}

// assign the response to the pointer
*destination = *res
return nil
}
}
Expand Down

0 comments on commit ff7a0bc

Please sign in to comment.