diff --git a/go.mod b/go.mod index 8c1937f4..396dbc7c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ce94ca37..20b064c7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/resources/common/error.go b/internal/resources/common/error.go new file mode 100644 index 00000000..966ee41b --- /dev/null +++ b/internal/resources/common/error.go @@ -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 +} diff --git a/internal/resources/common/state.go b/internal/resources/common/state.go index 97f3f44d..3b29fe66 100644 --- a/internal/resources/common/state.go +++ b/internal/resources/common/state.go @@ -1,6 +1,8 @@ package common import ( + "fmt" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -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() +} diff --git a/internal/resources/deviceandappmanagement/beta/assignmentFilter/object.go b/internal/resources/deviceandappmanagement/beta/assignmentFilter/construct.go similarity index 100% rename from internal/resources/deviceandappmanagement/beta/assignmentFilter/object.go rename to internal/resources/deviceandappmanagement/beta/assignmentFilter/construct.go diff --git a/internal/resources/deviceandappmanagement/beta/assignmentFilter/crud.go b/internal/resources/deviceandappmanagement/beta/assignmentFilter/crud.go new file mode 100644 index 00000000..97f199f7 --- /dev/null +++ b/internal/resources/deviceandappmanagement/beta/assignmentFilter/crud.go @@ -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) +} diff --git a/internal/resources/deviceandappmanagement/beta/assignmentFilter/resource.go b/internal/resources/deviceandappmanagement/beta/assignmentFilter/resource.go index a71cd612..ce4bf0c9 100644 --- a/internal/resources/deviceandappmanagement/beta/assignmentFilter/resource.go +++ b/internal/resources/deviceandappmanagement/beta/assignmentFilter/resource.go @@ -4,10 +4,11 @@ package graphBetaAssignmentFilter import ( "context" "fmt" - "time" + "strings" "github.com/deploymenttheory/terraform-provider-microsoft365/internal/client" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -30,6 +31,7 @@ type AssignmentFilterResource struct { client *msgraphbetasdk.GraphServiceClient ProviderTypeName string TypeName string + isCreate bool } type AssignmentFilterResourceModel struct { @@ -104,10 +106,14 @@ func (r *AssignmentFilterResource) Schema(ctx context.Context, req resource.Sche Description: "The optional description of the assignment filter.", }, "platform": schema.StringAttribute{ - Required: true, - Description: fmt.Sprintf("The Intune device management type (platform) for the assignment filter. Supported types: %v", getAllPlatformStrings()), + Required: true, + Description: fmt.Sprintf( + "The Intune device management type (platform) for the assignment filter. "+ + "Must be one of the following values: %s. "+ + "This specifies the OS platform type for which the assignment filter will be applied.", + strings.Join(validPlatformTypes, ", ")), Validators: []validator.String{ - platformValidator{}, + stringvalidator.OneOf(validPlatformTypes...), }, }, "rule": schema.StringAttribute{ @@ -116,12 +122,11 @@ func (r *AssignmentFilterResource) Schema(ctx context.Context, req resource.Sche }, "assignment_filter_management_type": schema.StringAttribute{ Optional: true, - Description: fmt.Sprintf("Indicates filter is applied to either 'devices' or 'apps' management type. Possible values are: %v. Default filter will be applied to 'devices'.", getAllManagementTypeStrings()), + Description: fmt.Sprintf("Indicates filter is applied to either 'devices' or 'apps' management type. Possible values are: %s. Default filter will be applied to 'devices'.", strings.Join(validAssignmentFilterManagementTypes, ", ")), Validators: []validator.String{ - assignmentFilterManagementTypeValidator{}, + stringvalidator.OneOf(validAssignmentFilterManagementTypes...), }, }, - "created_date_time": schema.StringAttribute{ Computed: true, Description: "The creation time of the assignment filter.", @@ -153,10 +158,11 @@ func (r *AssignmentFilterResource) Schema(ctx context.Context, req resource.Sche Description: "The group ID associated with the payload.", }, "assignment_filter_type": schema.StringAttribute{ - Required: true, - Description: fmt.Sprintf("The assignment filter type. Supported types: %v", getAllAssignmentFilterTypes()), + Required: true, + Description: fmt.Sprintf("The assignment filter type. Supported types: %s", + strings.Join(getValidAssignmentFilterTypes(), ", ")), Validators: []validator.String{ - assignmentFilterTypeValidator{}, + stringvalidator.OneOf(getValidAssignmentFilterTypes()...), }, }, }, @@ -171,164 +177,3 @@ func (r *AssignmentFilterResource) Schema(ctx context.Context, req resource.Sche }, } } - -// 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 - } - - if r.client == nil { - resp.Diagnostics.AddError( - "Client is not initialized", - "Cannot create assignment filter because the client is not initialized.", - ) - 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()) - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) - - 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 { - resp.Diagnostics.AddError( - "Error reading assignment filter", - fmt.Sprintf("Could not read assignment filter: %s", 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 { - resp.Diagnostics.AddError( - "Error updating assignment filter", - fmt.Sprintf("Could not update assignment filter: %s", 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) -} diff --git a/internal/resources/deviceandappmanagement/beta/assignmentFilter/state.go b/internal/resources/deviceandappmanagement/beta/assignmentFilter/state.go index b7d2f51b..d3cd8b63 100644 --- a/internal/resources/deviceandappmanagement/beta/assignmentFilter/state.go +++ b/internal/resources/deviceandappmanagement/beta/assignmentFilter/state.go @@ -1,6 +1,8 @@ package graphBetaAssignmentFilter import ( + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/helpers" + "github.com/deploymenttheory/terraform-provider-microsoft365/internal/resources/common" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/microsoftgraph/msgraph-beta-sdk-go/models" @@ -8,67 +10,97 @@ import ( // mapRemoteStateToTerraform func mapRemoteStateToTerraform(data *AssignmentFilterResourceModel, remoteResource models.DeviceAndAppManagementAssignmentFilterable) { - data.DisplayName = types.StringValue(*remoteResource.GetDisplayName()) - data.Description = types.StringValue(*remoteResource.GetDescription()) - data.Platform = types.StringValue(remoteResource.GetPlatform().String()) - data.Rule = types.StringValue(*remoteResource.GetRule()) - data.AssignmentFilterManagementType = types.StringValue(remoteResource.GetAssignmentFilterManagementType().String()) - data.CreatedDateTime = types.StringValue(remoteResource.GetCreatedDateTime().String()) - data.LastModifiedDateTime = types.StringValue(remoteResource.GetLastModifiedDateTime().String()) + if displayName := remoteResource.GetDisplayName(); displayName != nil { + data.DisplayName = types.StringValue(*displayName) + } else { + data.DisplayName = types.StringNull() + } + + if description := remoteResource.GetDescription(); description != nil { + data.Description = types.StringValue(*description) + } else { + data.Description = types.StringNull() + } + + if platform := remoteResource.GetPlatform(); platform != nil { + data.Platform = types.StringValue(platform.String()) + } else { + data.Platform = types.StringNull() + } + + if rule := remoteResource.GetRule(); rule != nil { + data.Rule = types.StringValue(*rule) + } else { + data.Rule = types.StringNull() + } + + if managementType := remoteResource.GetAssignmentFilterManagementType(); managementType != nil { + data.AssignmentFilterManagementType = types.StringValue(managementType.String()) + } else { + data.AssignmentFilterManagementType = types.StringNull() + } + + if createdDateTime := remoteResource.GetCreatedDateTime(); createdDateTime != nil { + data.CreatedDateTime = types.StringValue(createdDateTime.Format(helpers.TimeFormatRFC3339)) + } else { + data.CreatedDateTime = types.StringNull() + } + + if lastModifiedDateTime := remoteResource.GetLastModifiedDateTime(); lastModifiedDateTime != nil { + data.LastModifiedDateTime = types.StringValue(lastModifiedDateTime.Format(helpers.TimeFormatRFC3339)) + } else { + data.LastModifiedDateTime = types.StringNull() + } // Set RoleScopeTags - roleScopeTags := remoteResource.GetRoleScopeTags() - if roleScopeTags != nil { + if roleScopeTags := remoteResource.GetRoleScopeTags(); roleScopeTags != nil { tagList := make([]attr.Value, len(roleScopeTags)) for i, tag := range roleScopeTags { tagList[i] = types.StringValue(tag) } - roleScopeTagsList := types.ListValueMust(types.StringType, tagList) - data.RoleScopeTags = roleScopeTagsList + data.RoleScopeTags = types.ListValueMust(types.StringType, tagList) } else { - roleScopeTagsList := types.ListValueMust(types.StringType, []attr.Value{}) - data.RoleScopeTags = roleScopeTagsList + data.RoleScopeTags = types.ListValueMust(types.StringType, []attr.Value{}) } // Set Payloads - payloads := remoteResource.GetPayloads() - if payloads != nil { - payloadList := make([]attr.Value, len(payloads)) - for i, payload := range payloads { - payloadType := payload.GetPayloadType().String() - assignmentFilterType := payload.GetAssignmentFilterType().String() + if payloads := remoteResource.GetPayloads(); payloads != nil { + payloadList := make([]attr.Value, 0, len(payloads)) + for _, payload := range payloads { payloadMap := map[string]attr.Value{ - "payload_id": types.StringValue(*payload.GetPayloadId()), - "payload_type": types.StringValue(payloadType), - "group_id": types.StringValue(*payload.GetGroupId()), - "assignment_remoteResource_type": types.StringValue(assignmentFilterType), + "payload_id": types.StringValue(common.SafeDeref(payload.GetPayloadId())), + "payload_type": types.StringValue(common.SafeEnumString(payload.GetPayloadType())), + "group_id": types.StringValue(common.SafeDeref(payload.GetGroupId())), + "assignment_filter_type": types.StringValue(common.SafeEnumString(payload.GetAssignmentFilterType())), } - payloadList[i] = types.ObjectValueMust(map[string]attr.Type{ - "payload_id": types.StringType, - "payload_type": types.StringType, - "group_id": types.StringType, - "assignment_remoteResource_type": types.StringType, + payloadObj, diags := types.ObjectValue(map[string]attr.Type{ + "payload_id": types.StringType, + "payload_type": types.StringType, + "group_id": types.StringType, + "assignment_filter_type": types.StringType, }, payloadMap) + if diags.HasError() { + // Handle or log the error + continue + } + payloadList = append(payloadList, payloadObj) } - payloadsList := types.ListValueMust(types.ObjectType{ + data.Payloads, _ = types.ListValue(types.ObjectType{ AttrTypes: map[string]attr.Type{ - "payload_id": types.StringType, - "payload_type": types.StringType, - "group_id": types.StringType, - "assignment_remoteResource_type": types.StringType, + "payload_id": types.StringType, + "payload_type": types.StringType, + "group_id": types.StringType, + "assignment_filter_type": types.StringType, }, }, payloadList) - data.Payloads = payloadsList } else { - payloadsList := types.ListValueMust(types.ObjectType{ + data.Payloads = types.ListNull(types.ObjectType{ AttrTypes: map[string]attr.Type{ - "payload_id": types.StringType, - "payload_type": types.StringType, - "group_id": types.StringType, - "assignment_remoteResource_type": types.StringType, + "payload_id": types.StringType, + "payload_type": types.StringType, + "group_id": types.StringType, + "assignment_filter_type": types.StringType, }, - }, []attr.Value{}) - data.Payloads = payloadsList + }) } - } diff --git a/internal/resources/deviceandappmanagement/beta/assignmentFilter/validators.go b/internal/resources/deviceandappmanagement/beta/assignmentFilter/validators.go index af9d4cf0..021eb2a5 100644 --- a/internal/resources/deviceandappmanagement/beta/assignmentFilter/validators.go +++ b/internal/resources/deviceandappmanagement/beta/assignmentFilter/validators.go @@ -1,158 +1,43 @@ package graphBetaAssignmentFilter -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/microsoftgraph/msgraph-beta-sdk-go/models" -) - -// platformValidator is the custom validator type -type platformValidator struct{} - -// ValidateString performs the validation. -func (v platformValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { - return - } - - _, err := models.ParseDevicePlatformType(req.ConfigValue.ValueString()) - if err != nil { - resp.Diagnostics.AddError( - "Invalid Device Platform Type", - fmt.Sprintf("The platform type '%s' is not valid. Supported types: %v", req.ConfigValue.ValueString(), getAllPlatformStrings()), - ) - } -} - -// Description describes the validation in plain text. -func (v platformValidator) Description(ctx context.Context) string { - return "must be a valid device platform type" -} - -// MarkdownDescription describes the validation in Markdown. -func (v platformValidator) MarkdownDescription(ctx context.Context) string { - return "must be a valid device platform type" -} - -// getAllPlatformStrings returns all the valid platform strings -func getAllPlatformStrings() []string { - platformTypes := []models.DevicePlatformType{ - models.ANDROID_DEVICEPLATFORMTYPE, - models.ANDROIDFORWORK_DEVICEPLATFORMTYPE, - models.IOS_DEVICEPLATFORMTYPE, - models.MACOS_DEVICEPLATFORMTYPE, - models.WINDOWSPHONE81_DEVICEPLATFORMTYPE, - models.WINDOWS81ANDLATER_DEVICEPLATFORMTYPE, - models.WINDOWS10ANDLATER_DEVICEPLATFORMTYPE, - models.ANDROIDWORKPROFILE_DEVICEPLATFORMTYPE, - models.UNKNOWN_DEVICEPLATFORMTYPE, - models.ANDROIDAOSP_DEVICEPLATFORMTYPE, - models.ANDROIDMOBILEAPPLICATIONMANAGEMENT_DEVICEPLATFORMTYPE, - models.IOSMOBILEAPPLICATIONMANAGEMENT_DEVICEPLATFORMTYPE, - models.UNKNOWNFUTUREVALUE_DEVICEPLATFORMTYPE, - models.WINDOWSMOBILEAPPLICATIONMANAGEMENT_DEVICEPLATFORMTYPE, - } - - var platformStrings []string - for _, platform := range platformTypes { - platformStrings = append(platformStrings, platform.String()) - } - return platformStrings -} - -// assignmentFilterManagementTypeValidator is the custom validator type -type assignmentFilterManagementTypeValidator struct{} - -// Description describes the validation in plain text. -func (v assignmentFilterManagementTypeValidator) Description(ctx context.Context) string { - return "must be a valid assignment filter management type" -} - -// MarkdownDescription describes the validation in Markdown. -func (v assignmentFilterManagementTypeValidator) MarkdownDescription(ctx context.Context) string { - return "must be a valid assignment filter management type" -} - -// ValidateString performs the validation. -func (v assignmentFilterManagementTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { - return - } - - validTypes := getAllManagementTypeStrings() - value := req.ConfigValue.ValueString() - for _, validType := range validTypes { - if value == validType { - return - } - } - - resp.Diagnostics.AddError( - "Invalid Assignment Filter Management Type", - fmt.Sprintf("The management type '%s' is not valid. Supported types: %v", value, validTypes), - ) -} - -// getAllManagementTypeStrings returns all the valid management type strings -func getAllManagementTypeStrings() []string { - return []string{"devices", "apps", "unknownFutureValue"} -} - -// assignmentFilterTypeValidator is the custom validator type -type assignmentFilterTypeValidator struct{} - -// ValidateString performs the validation. -func (v assignmentFilterTypeValidator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { - if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { - return - } - - validTypes := getAllAssignmentFilterTypes() - value := req.ConfigValue.ValueString() - for _, validType := range validTypes { - if value == validType { - return - } - } - - resp.Diagnostics.AddError( - "Invalid Assignment Filter Type", - fmt.Sprintf("The assignment filter type '%s' is not valid. Supported types: %v", value, validTypes), - ) -} - -// Description describes the validation in plain text. -func (v assignmentFilterTypeValidator) Description(ctx context.Context) string { - return "must be a valid assignment filter type" -} - -// MarkdownDescription describes the validation in Markdown. -func (v assignmentFilterTypeValidator) MarkdownDescription(ctx context.Context) string { - return "must be a valid assignment filter type" -} - -// getAllAssignmentFilterTypes returns all the valid assignment filter type strings -func getAllAssignmentFilterTypes() []string { - types := []models.AssociatedAssignmentPayloadType{ - models.UNKNOWN_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.DEVICECONFIGURATIONANDCOMPLIANCE_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.APPLICATION_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.ANDROIDENTERPRISEAPP_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.ENROLLMENTCONFIGURATION_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.GROUPPOLICYCONFIGURATION_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.ZEROTOUCHDEPLOYMENTDEVICECONFIGPROFILE_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.ANDROIDENTERPRISECONFIGURATION_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.DEVICEFIRMWARECONFIGURATIONINTERFACEPOLICY_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.RESOURCEACCESSPOLICY_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.WIN32APP_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - models.DEVICEMANAGMENTCONFIGURATIONANDCOMPLIANCEPOLICY_ASSOCIATEDASSIGNMENTPAYLOADTYPE, - } - - var typeStrings []string - for _, t := range types { - typeStrings = append(typeStrings, t.String()) +/* platform type validator */ + +var validPlatformTypes = []string{ + "android", + "androidForWork", + "iOS", + "macOS", + "windowsPhone81", + "windows81AndLater", + "windows10AndLater", + "androidWorkProfile", + "unknown", + "androidAOSP", + "androidMobileApplicationManagement", + "iOSMobileApplicationManagement", + "windowsMobileApplicationManagement", +} + +var validAssignmentFilterManagementTypes = []string{ + "devices", + "apps", + "unknownFutureValue", +} + +func getValidAssignmentFilterTypes() []string { + // This reflects the order in the SDK's String() method + return []string{ + "unknown", + "deviceConfigurationAndCompliance", + "application", + "androidEnterpriseApp", + "enrollmentConfiguration", + "groupPolicyConfiguration", + "zeroTouchDeploymentDeviceConfigProfile", + "androidEnterpriseConfiguration", + "deviceFirmwareConfigurationInterfacePolicy", + "resourceAccessPolicy", + "win32app", + "deviceManagmentConfigurationAndCompliancePolicy", } - return typeStrings }