Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class support for unmanaged Dataverse solutions to the solution service, enabling CRUD management as a stable Terraform resource plus lookup via a dedicated data source, along with tests, examples, docs, and changelog entry.
Changes:
- Introduces
powerplatform_unmanaged_solutionresource with CRUD + import support. - Adds
powerplatform_unmanaged_solutiondata source for lookup byenvironment_id+uniquename. - Extends solution API/DTOs and adds unit/acceptance tests, examples, generated docs, and changelog entry.
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/services/solution/api_solution.go | Adds create/update/poll logic for unmanaged solutions and parses OData-EntityId for IDs. |
| internal/services/solution/dto.go | Extends SolutionDto (description/publisher) and adds create/update DTOs for unmanaged solution CRUD. |
| internal/services/solution/models.go | Adds models for unmanaged solution resource and data source. |
| internal/services/solution/resource_unmanaged_solution.go | Implements unmanaged solution Terraform resource (schema + CRUD + import). |
| internal/services/solution/resource_unmanaged_solution_test.go | Unit + acceptance coverage for unmanaged solution resource (create/update/import + edge cases). |
| internal/services/solution/datasource_unmanaged_solution.go | Implements unmanaged solution lookup data source. |
| internal/services/solution/datasource_unmanaged_solution_test.go | Unit coverage for unmanaged solution data source (read/no-dataverse/not-found). |
| internal/services/solution/tests/resource/Validate_Unmanaged_Create/get_environment_00000000-0000-0000-0000-000000000001.json | Test fixture for Dataverse-enabled environment lookup (create path). |
| internal/services/solution/tests/resource/Validate_Unmanaged_Create/get_solution.json | Test fixture for created solution GET response. |
| internal/services/solution/tests/resource/Validate_Unmanaged_No_Dataverse/get_environment_00000000-0000-0000-0000-000000000001.json | Test fixture for environment without Dataverse. |
| internal/services/solution/tests/resource/Validate_Unmanaged_Update/get_environment_00000000-0000-0000-0000-000000000001.json | Test fixture for Dataverse-enabled environment lookup (update path). |
| internal/services/solution/tests/resource/Validate_Unmanaged_Update/get_solution_before.json | Test fixture for solution state before update. |
| internal/services/solution/tests/resource/Validate_Unmanaged_Update/get_solution_after.json | Test fixture for solution state after update. |
| internal/provider/provider.go | Registers new unmanaged solution resource and data source with the provider. |
| internal/provider/provider_test.go | Updates provider child resource/data source registry tests to include unmanaged solution types. |
| examples/resources/powerplatform_unmanaged_solution/resource.tf | Adds resource example usage (creates env + unmanaged solution). |
| examples/resources/powerplatform_unmanaged_solution/outputs.tf | Outputs example for unmanaged solution ID. |
| examples/resources/powerplatform_unmanaged_solution/import.sh | Import example for unmanaged solution composite ID format. |
| examples/data-sources/powerplatform_unmanaged_solution/data-source.tf | Adds unmanaged solution data source example snippet. |
| examples/data-sources/powerplatform_unmanaged_solution/outputs.tf | Outputs example for unmanaged solution data source. |
| docs/resources/unmanaged_solution.md | Generated docs for the unmanaged solution resource. |
| docs/data-sources/unmanaged_solution.md | Generated docs for the unmanaged solution data source. |
| .changes/unreleased/added-20260330-165500-unmanaged-solution.yaml | Adds changelog entry for the new resource. |
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT license. | ||
|
|
||
| package solution |
There was a problem hiding this comment.
Unit tests in this repo generally use the external test package pattern (package <service>_test) to exercise the provider via its public surface (e.g., resource_unmanaged_solution_test.go uses package solution_test). This file uses package solution, which couples the test to unexported internals; consider moving this test into package solution_test by testing behavior through the resource/data source, or relocating the helper to a testable public/utility location if direct testing is required.
| return | ||
| } | ||
| if !dvExists { | ||
| resp.Diagnostics.AddError(fmt.Sprintf("No Dataverse exists in environment '%s'", plan.EnvironmentId.ValueString()), "") |
There was a problem hiding this comment.
This diagnostic adds an error with an empty detail string. Terraform users will only see the summary, and troubleshooting context is lost. Please provide a meaningful detail (e.g., explain that the environment is not Dataverse-enabled / missing linkedEnvironmentMetadata.instanceUrl).
| resp.Diagnostics.AddError(fmt.Sprintf("No Dataverse exists in environment '%s'", plan.EnvironmentId.ValueString()), "") | |
| resp.Diagnostics.AddError( | |
| fmt.Sprintf("No Dataverse exists in environment '%s'", plan.EnvironmentId.ValueString()), | |
| fmt.Sprintf("Unmanaged solutions can only be created in Dataverse-enabled environments. The environment '%s' does not have Dataverse provisioned, so solution creation cannot continue. Verify that the target environment is Dataverse-enabled and provision Dataverse before retrying.", plan.EnvironmentId.ValueString()), | |
| ) |
| } | ||
|
|
||
| if !dvExists { | ||
| resp.Diagnostics.AddError(fmt.Sprintf("No Dataverse exists in environment '%s'", state.EnvironmentId.ValueString()), "") |
There was a problem hiding this comment.
This diagnostic adds an error with an empty detail string, which makes troubleshooting harder for users. Please include actionable detail (e.g., that the environment does not have Dataverse enabled / linkedEnvironmentMetadata.instanceUrl is missing).
| resp.Diagnostics.AddError(fmt.Sprintf("No Dataverse exists in environment '%s'", state.EnvironmentId.ValueString()), "") | |
| resp.Diagnostics.AddError( | |
| fmt.Sprintf("No Dataverse exists in environment '%s'", state.EnvironmentId.ValueString()), | |
| fmt.Sprintf("The environment '%s' does not have Dataverse enabled or provisioned. Unmanaged solutions require a Dataverse environment. Verify that the environment ID is correct and that Dataverse has been created for this environment.", state.EnvironmentId.ValueString()), | |
| ) |
| } | ||
|
|
||
| func splitSolutionCompositeID(id string) []string { | ||
| return strings.Split(id, "_") |
There was a problem hiding this comment.
splitSolutionCompositeID uses strings.Split, which will split on every underscore. Import IDs are expected to have exactly two parts (environment_id and solution_id), so using a 2-way split (e.g., SplitN with 2) would make parsing more robust and avoid surprising failures if an underscore ever appears in an ID string.
| return strings.Split(id, "_") | |
| return strings.SplitN(id, "_", 2) |
| @@ -0,0 +1,5 @@ | |||
| kind: added | |||
| body: Added the `powerplatform_unmanaged_solution` resource for direct Dataverse solution CRUD without ZIP import lifecycle coupling. | |||
There was a problem hiding this comment.
As discussed, lets rename this resource and data source to powerplatform_solution.
The PR with rename of an exising solution into _import is here. I will get it merge into main asap.
| return client.GetSolutionById(ctx, environmentId, solutionId) | ||
| } | ||
|
|
||
| func (client *Client) WaitForUnmanagedSolution(ctx context.Context, environmentId, solutionId, uniqueName string) (*SolutionDto, error) { |
There was a problem hiding this comment.
this function is used only in this file?
| func (client *Client) WaitForUnmanagedSolution(ctx context.Context, environmentId, solutionId, uniqueName string) (*SolutionDto, error) { | |
| func (client *Client) waitForUnmanagedSolution(ctx context.Context, environmentId, solutionId, uniqueName string) (*SolutionDto, error) { |
| } | ||
| } | ||
|
|
||
| return client.WaitForUnmanagedSolution(ctx, environmentId, solutionID, uniqueName) |
There was a problem hiding this comment.
| return client.WaitForUnmanagedSolution(ctx, environmentId, solutionID, uniqueName) | |
| return client.waitForUnmanagedSolution(ctx, environmentId, solutionID, uniqueName) |
| @@ -0,0 +1,159 @@ | |||
| // Copyright (c) Microsoft Corporation. | |||
| // Licensed under the MIT license. | |||
|
|
|||
There was a problem hiding this comment.
do we need datasource since we already have powerplatform_solutions in the provider doing the same thing?
| defer exitContext() | ||
|
|
||
| resp.Schema = schema.Schema{ | ||
| MarkdownDescription: "Resource for managing unmanaged Dataverse solutions as first-class solution records without coupling lifecycle to solution ZIP import operations.", |
There was a problem hiding this comment.
| MarkdownDescription: "Resource for managing unmanaged Dataverse solutions as first-class solution records without coupling lifecycle to solution ZIP import operations.", | |
| MarkdownDescription: "Resource for managing Power Platform solutions as first-class solution records inside Dataverse", |
| @@ -0,0 +1,2 @@ | |||
| # Unmanaged solution resources can be imported using the provider id format {environment_id}_{solution_id} | |||
There was a problem hiding this comment.
to be consistent with the rest of the provider:
| # Unmanaged solution resources can be imported using the provider id format {environment_id}_{solution_id} | |
| # Unmanaged solution resources can be imported using the provider id format {environment_id}/{solution_id} |
| @@ -0,0 +1,2 @@ | |||
| # Unmanaged solution resources can be imported using the provider id format {environment_id}_{solution_id} | |||
| terraform import powerplatform_unmanaged_solution.solution 00000000-0000-0000-0000-000000000000_00000000-0000-0000-0000-000000000001 | |||
There was a problem hiding this comment.
to be consistent with the rest of the provider:
| terraform import powerplatform_unmanaged_solution.solution 00000000-0000-0000-0000-000000000000_00000000-0000-0000-0000-000000000001 | |
| terraform import powerplatform_unmanaged_solution.solution 00000000-0000-0000-0000-000000000000/00000000-0000-0000-0000-000000000001 |
| unmanagedPublisherID = "aa47dc6c-bf13-490b-a007-1da95a0d1e3f" | ||
| ) | ||
|
|
||
| func TestUnitUnmanagedSolutionResource_Validate_Create(t *testing.T) { |
There was a problem hiding this comment.
Each of this unit test should have Acceptance Test version too
|
|
||
| const ( | ||
| unmanagedSolutionEnvironmentID = "00000000-0000-0000-0000-000000000001" | ||
| unmanagedSolutionID = "86928ed8-df37-4ce2-add5-47030a833bff" |
There was a problem hiding this comment.
can solutionid and publisherId be something like 00000000-0000-0000-0000-00000000000X so its clear that its a mockup value?
|
|
||
| httpmock.RegisterResponder("GET", "https://00000000-0000-0000-0000-000000000001.crm4.dynamics.com/api/data/v9.2/solutions?%24expand=publisherid&%24filter=uniquename+eq+%27TerraformTestSolution%27", | ||
| func(req *http.Request) (*http.Response, error) { | ||
| readAttempts++ |
There was a problem hiding this comment.
if possible http mocked responders should work wihout any explicit logic. They should just respond with new .json files
Summary
powerplatform_unmanaged_solutionas a first-class Dataverse CRUD resourcepowerplatform_unmanaged_solutiondata source for lookup by environment and unique nameTesting
Closes #1117