Skip to content

Commit

Permalink
Merge pull request #21 from deploymenttheory/feature-scaffolding
Browse files Browse the repository at this point in the history
Feature scaffolding - refactor the CRUD function patterns
  • Loading branch information
ShocOne authored Jul 25, 2024
2 parents c65d713 + 545bfd9 commit 03c9913
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 367 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ require (
github.com/hashicorp/hc-install v0.7.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.23.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwk
github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1 h1:gm5b1kHgFFhaKFhm4h2TgvMUlNzFAtUqlcOWnWPm+9E=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.4.1/go.mod h1:MsjL1sQ9L7wGwzJ5RjcI6FzEMdyoBnw+XK8ZnOvQOLY=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
Expand Down
64 changes: 64 additions & 0 deletions internal/resources/common/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package common

import (
"strings"

"github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
)

// IsNotFoundError checks if the given error is an OData error indicating that a resource was not found.
// The function first verifies if the error is not nil. Then, it attempts to cast the error to an ODataError
// type from the Microsoft Graph SDK. If the casting is successful, the function retrieves the main error
// details using the GetErrorEscaped method of the ODataError struct. It then checks if the error code or
// message contains indications of a "not found" error.
//
// Specifically, the function looks for the error codes "request_resourcenotfound" and "resourcenotfound"
// (case-insensitive), or a message containing the phrase "not found" (case-insensitive). If any of these
// conditions are met, the function returns true, indicating that the error is a "not found" error.
// Otherwise, it returns false.
//
// The ODataError struct is part of the Microsoft Graph SDK and includes various methods and properties
// to handle API errors. The main error details are encapsulated in a nested structure that provides
// additional context, such as error codes and descriptive messages.
//
// Usage:
//
// if common.IsNotFoundError(err) {
// // Handle the "not found" error case
// }
//
// Parameters:
//
// err - The error to check.
//
// Returns:
//
// bool - True if the error indicates that a resource was not found, otherwise false.
func IsNotFoundError(err error) bool {
if err == nil {
return false
}

odataErr, ok := err.(*odataerrors.ODataError)
if !ok {
return false
}

mainError := odataErr.GetErrorEscaped()
if mainError != nil {
if code := mainError.GetCode(); code != nil {
switch strings.ToLower(*code) {
case "request_resourcenotfound", "resourcenotfound":
return true
}
}

if message := mainError.GetMessage(); message != nil {
if strings.Contains(strings.ToLower(*message), "not found") {
return true
}
}
}

return false
}
23 changes: 23 additions & 0 deletions internal/resources/common/state.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package common

import (
"fmt"

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

"github.com/hashicorp/terraform-plugin-framework/attr"
Expand Down Expand Up @@ -31,3 +33,24 @@ func SetParsedValueFromAttributes[T any](attrs map[string]attr.Value, key string
}
return nil
}

// safeDeref safely dereferences a string pointer.
// It returns an empty string if the pointer is nil,
// otherwise it returns the dereferenced string value.
func SafeDeref(s *string) string {
if s == nil {
return ""
}
return *s
}

// safeEnumString safely converts an enum to its string representation.
// It returns an empty string if the enum is nil,
// otherwise it calls the String() method on the enum.
// This function expects the input to implement the fmt.Stringer interface.
func SafeEnumString(e fmt.Stringer) string {
if e == nil {
return ""
}
return e.String()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package graphBetaAssignmentFilter

import (
"context"
"fmt"
"time"

"github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

// Create handles the Create operation.
func (r *AssignmentFilterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data AssignmentFilterResourceModel

tflog.Debug(ctx, fmt.Sprintf("Starting creation of resource: %s_%s", r.ProviderTypeName, r.TypeName))

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

createTimeout, diags := data.Timeouts.Create(ctx, 30*time.Second)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, createTimeout)
defer cancel()

requestBody, err := constructResource(ctx, &data)
if err != nil {
resp.Diagnostics.AddError(
"Error constructing assignment filter",
fmt.Sprintf("Could not construct resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()),
)
return
}

assignmentFilter, err := r.client.DeviceManagement().AssignmentFilters().Post(ctx, requestBody, nil)
if err != nil {
resp.Diagnostics.AddError(
"Error creating assignment filter",
fmt.Sprintf("Could not create assignment filter: %s", err.Error()),
)
return
}

data.ID = types.StringValue(*assignmentFilter.GetId())

r.isCreate = true

readResp := resource.ReadResponse{
State: resp.State,
}
r.Read(ctx, resource.ReadRequest{State: resp.State}, &readResp)
resp.Diagnostics.Append(readResp.Diagnostics...)

r.isCreate = false

tflog.Debug(ctx, fmt.Sprintf("Finished creation of resource: %s_%s", r.ProviderTypeName, r.TypeName))
}

// Read handles the read operation and stating.
func (r *AssignmentFilterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data AssignmentFilterResourceModel

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

readTimeout, diags := data.Timeouts.Read(ctx, 30*time.Second)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, readTimeout)
defer cancel()

remoteResource, err := r.client.DeviceManagement().AssignmentFilters().ByDeviceAndAppManagementAssignmentFilterId(data.ID.ValueString()).Get(ctx, nil)
if err != nil {
if common.IsNotFoundError(err) && !r.isCreate {
resp.Diagnostics.AddWarning(
"Resource Not Found",
fmt.Sprintf("The resource: %s_%s with ID %s was not found and will be removed from the state.", r.ProviderTypeName, r.TypeName, data.ID.ValueString()),
)
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Error reading assignment filter",
fmt.Sprintf("Could not read resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()),
)
return
}

mapRemoteStateToTerraform(&data, remoteResource)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// Update handles the Update operation.
func (r *AssignmentFilterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data AssignmentFilterResourceModel

tflog.Debug(ctx, fmt.Sprintf("Starting Update of resource: %s_%s", r.ProviderTypeName, r.TypeName))

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

updateTimeout, diags := data.Timeouts.Update(ctx, 30*time.Second)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, updateTimeout)
defer cancel()

requestBody, err := constructResource(ctx, &data)

if err != nil {
resp.Diagnostics.AddError(
"Error constructing assignment filter",
fmt.Sprintf("Could not construct resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()),
)
return
}

_, err = r.client.DeviceManagement().AssignmentFilters().ByDeviceAndAppManagementAssignmentFilterId(data.ID.ValueString()).Patch(ctx, requestBody, nil)
if err != nil {
if common.IsNotFoundError(err) && !r.isCreate {
resp.Diagnostics.AddWarning(
"Resource Not Found",
fmt.Sprintf("The resource: %s_%s with ID %s was not found and will be removed from the state.", r.ProviderTypeName, r.TypeName, data.ID.ValueString()),
)
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(
"Error reading assignment filter",
fmt.Sprintf("Could not update resource: %s_%s: %s", r.ProviderTypeName, r.TypeName, err.Error()),
)
return
}

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

tflog.Debug(ctx, fmt.Sprintf("Finished Update of resource: %s_%s", r.ProviderTypeName, r.TypeName))
}

// Delete handles the Delete operation.
func (r *AssignmentFilterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data AssignmentFilterResourceModel

tflog.Debug(ctx, fmt.Sprintf("Starting deletion of resource: %s_%s", r.ProviderTypeName, r.TypeName))

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

deleteTimeout, diags := data.Timeouts.Delete(ctx, 30*time.Second)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
ctx, cancel := context.WithTimeout(ctx, deleteTimeout)
defer cancel()

err := r.client.DeviceManagement().AssignmentFilters().ByDeviceAndAppManagementAssignmentFilterId(data.ID.ValueString()).Delete(ctx, nil)
if err != nil {
resp.Diagnostics.AddError(fmt.Sprintf("Client error when deleting %s_%s", r.ProviderTypeName, r.TypeName), err.Error())
return
}

tflog.Debug(ctx, fmt.Sprintf("Completed deletion of resource: %s_%s", r.ProviderTypeName, r.TypeName))

resp.State.RemoveResource(ctx)
}
Loading

0 comments on commit 03c9913

Please sign in to comment.