Skip to content

Commit 87bb59b

Browse files
mawasilegithub-actions[bot]
andauthored
powerplatform environment billing policy id and environment group id should accept empty guid as an empty values instead of "" (#604)
* Feat: Add Bing search configuration to environment schema and DTO * Feat: Enhance environment API to support cross-region data movement and update schema * Feat: Update environment API to support copilot policies and add new schema attributes for Bing search and cross-region data movement * Feat: Add support for Bing Chat and update environment AI features in the API * Feat: Update environment API to include copilot policies and enhance billing policy handling in tests * Feat: Add tests for generative AI features in non-US and US regions in environment resource * Feat: Refactor environment update logic to improve readability and maintainability * Feat: Add allow_bing_search and allow_moving_data_across_regions attributes to powerplatform_environment * Feat: Update documentation to include allow_bing_search and allow_moving_data_across_regions attributes for powerplatform_environment * Fix formatting and update API request to include copilotPolicies in managed environment tests * Validate location constraints for AI generative features in powerplatform_environment resource * Add release cycle field to SourceModel and improve error messages for location validation * Refactor AI generative features validation logic and improve error handling for location constraints * enhance validation: add length requirement for environment group ID * fixing environemnt_group_id * refactor: update billing policy ID handling to use constants for empty values * docs: update descriptions for billing_policy_id and environment_group_id to specify empty GUID usage * fixing TestAccEnvironmentsResource_Create_Environment_And_Add_Env_Group * fix TestAccTestEnvironmentSettingsResource_Validate_Read * fix TestAccManagedEnvironmentsResource_Validate_No_Dataverse * trigger acc tests * run_tests pipeline: clean environment that starts with "Test" and not only with "TestAcc" * fix: update retry duration and handle environment deletion errors --------- Co-authored-by: github-actions[bot] <tfmod442916@users.noreply.github.com>
1 parent 370b9fb commit 87bb59b

File tree

12 files changed

+88
-62
lines changed

12 files changed

+88
-62
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: changed
2+
body: 'powerplatform_environment: attributes `environment_group_id` and `billing_policy_id` uses empty guid `00000000-0000-0000-0000-000000000000` as no value, instead of ""'
3+
time: 2025-02-27T16:14:15.118950739Z
4+
custom:
5+
Issue: "604"

.github/workflows/run_tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,9 @@ jobs:
130130
run: |
131131
dotnet tool install --global Microsoft.PowerApps.CLI.Tool
132132
pac auth create --githubFederated --tenant ${{ secrets.ACCEPTANCE_TESTS_ENV_TENANT_ID }} --applicationId ${{ secrets.ACCEPTANCE_TESTS_ENV_CLIENT_ID }}
133-
$environmentsList = (pac admin list --name "TestAcc" --json | ConvertFrom-Json)
133+
$environmentsList = (pac admin list --name "Test" --json | ConvertFrom-Json)
134134
$environmentsList | ForEach-Object -Parallel {
135-
if ($_.DisplayName.StartsWith("TestAcc")){
135+
if ($_.DisplayName.StartsWith("Test")){
136136
Write-Output $_.DisplayName
137137
pac admin delete -env $_.EnvironmentID
138138
}

docs/resources/environment.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ resource "powerplatform_environment" "development" {
6262
- `allow_bing_search` (Boolean) Allow Bing search in the environment
6363
- `allow_moving_data_across_regions` (Boolean) Allow moving data across regions
6464
- `azure_region` (String) Azure region of the environment (westeurope, eastus etc.). Can be queried using the `powerplatform_locations` data source. This property should only be set if absolutely necessary like when trying to create an environment in the same Azure region as Azure resources or Fabric capacity. Changing this property after environment creation will result in a destroy and recreation of the environment (you can use the [`prevent_destroy` lifecycle metatdata](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#prevent_destroy) as an added safeguard to prevent accidental deletion of environments).
65-
- `billing_policy_id` (String) Billing policy id (guid) for pay-as-you-go environments using Azure subscription billing
65+
- `billing_policy_id` (String) Billing policy id (guid) for pay-as-you-go environments using Azure subscription billing. To remove the environment from the billing policy, set this attribute to `00000000-0000-0000-0000-000000000000`
6666
- `cadence` (String) Cadence of updates for the environment (Frequent, Moderate). For more information check [here](https://learn.microsoft.com/en-us/power-platform/admin/create-environment#setting-an-environment-refresh-cadence).
6767
- `dataverse` (Attributes) Dataverse environment details (see [below for nested schema](#nestedatt--dataverse))
6868
- `description` (String) Description of the environment
69-
- `environment_group_id` (String) Environment group id (guid) that the environment belongs to. See [Environment groups](https://learn.microsoft.com/en-us/power-platform/admin/environment-groups) for more information.
69+
- `environment_group_id` (String) Environment group id (guid) that the environment belongs to. See [Environment groups](https://learn.microsoft.com/en-us/power-platform/admin/environment-groups) for more information. To remove the environment from the environment group, set this attribute to `00000000-0000-0000-0000-000000000000`
7070
- `owner_id` (String) Entra ID user id (guid) of the environment owner when creating developer environment
7171
- `release_cycle` (String) Gives you the ability to create environments that are updated first. This allows you to experience and validate scenarios that are important to you before any updates reach your business-critical applications. See [more](https://learn.microsoft.com/en-us/power-platform/admin/early-release).
7272
- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts))

internal/api/client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,9 @@ func (client *Client) Execute(ctx context.Context, scopes []string, method, url
144144
}
145145
}
146146

147-
// RetryAfterDefault returns a random duration between 5 and 10 seconds.
147+
// RetryAfterDefault returns a random duration between 10 and 20 seconds.
148148
func DefaultRetryAfter() time.Duration {
149-
return time.Duration((rand.Intn(5) + 5)) * time.Second
149+
return time.Duration((rand.Intn(10) + 10)) * time.Second
150150
}
151151

152152
// SleepWithContext sleeps for the given duration or until the context is canceled.

internal/api/request.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"errors"
1111
"fmt"
1212
"io"
13-
"math/rand"
1413
"net/http"
1514
"runtime"
1615
"time"
@@ -92,7 +91,7 @@ func retryAfter(ctx context.Context, resp *http.Response) time.Duration {
9291
retryAfter, err := time.ParseDuration(retryHeader)
9392
if err != nil {
9493
// default retry after 5-10 seconds
95-
return time.Duration((rand.Intn(5) + 5)) * time.Second
94+
return DefaultRetryAfter()
9695
}
9796

9897
return retryAfter

internal/services/environment/api_environment.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,23 @@ func (client *Client) DeleteEnvironment(ctx context.Context, environmentId strin
290290
Message: "Deleted using Power Platform Terraform Provider",
291291
}
292292

293-
response, err := client.Api.Execute(ctx, nil, "DELETE", apiUrl.String(), nil, environmentDelete, []int{http.StatusAccepted}, nil)
294-
if err != nil {
295-
var httpError *customerrors.UnexpectedHttpStatusCodeError
296-
if errors.As(err, &httpError) {
297-
return fmt.Errorf("Unexpected HTTP Status %s; Body: %s", httpError.StatusText, httpError.Body)
293+
response, err := client.Api.Execute(ctx, nil, "DELETE", apiUrl.String(), nil, environmentDelete, []int{http.StatusNoContent, http.StatusAccepted, http.StatusConflict}, nil)
294+
295+
if response.HttpResponse.StatusCode == http.StatusConflict {
296+
// the is another operation in progress, let's wait for it to complete, and try again
297+
tflog.Debug(ctx, "Another operation is in progress, waiting for it to complete")
298+
err = client.Api.SleepWithContext(ctx, api.DefaultRetryAfter())
299+
if err != nil {
300+
return err
298301
}
299-
return err
302+
return client.DeleteEnvironment(ctx, environmentId)
303+
}
304+
305+
var httpError *customerrors.UnexpectedHttpStatusCodeError
306+
if errors.As(err, &httpError) {
307+
return fmt.Errorf("Unexpected HTTP Status %s; Body: %s", httpError.StatusText, httpError.Body)
300308
}
309+
301310
tflog.Debug(ctx, "Environment Deletion Operation HTTP Status: '"+response.HttpResponse.Status+"'")
302311

303312
tflog.Debug(ctx, "Waiting for environment deletion operation to complete")

internal/services/environment/datasource_environments_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ func TestUnitEnvironmentsDataSource_Validate_Read(t *testing.T) {
129129
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.environment_type", "Sandbox"),
130130
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.location", "europe"),
131131
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.azure_region", "westeurope"),
132-
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.billing_policy_id", ""),
133-
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.environment_group_id", ""),
132+
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.billing_policy_id", "00000000-0000-0000-0000-000000000000"),
133+
resource.TestCheckResourceAttr("data.powerplatform_environments.all", "environments.1.environment_group_id", "00000000-0000-0000-0000-000000000000"),
134134
resource.TestCheckNoResourceAttr("data.powerplatform_environments.all", "environments.1.dataverse.domain"),
135135
resource.TestCheckNoResourceAttr("data.powerplatform_environments.all", "environments.1.dataverse.language_code"),
136136
resource.TestCheckNoResourceAttr("data.powerplatform_environments.all", "environments.1.dataverse.organization_id"),

internal/services/environment/models.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/hashicorp/terraform-plugin-framework/types"
1414
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
1515
"github.com/microsoft/terraform-provider-power-platform/internal/config"
16+
"github.com/microsoft/terraform-provider-power-platform/internal/constants"
1617
"github.com/microsoft/terraform-provider-power-platform/internal/helpers"
1718
"github.com/microsoft/terraform-provider-power-platform/internal/services/licensing"
1819
)
@@ -110,7 +111,7 @@ func convertCreateEnvironmentDtoFromSourceModel(ctx context.Context, environment
110111
environmentDto.Properties.AzureRegion = environmentSource.AzureRegion.ValueString()
111112
}
112113

113-
if !environmentSource.BillingPolicyId.IsNull() && environmentSource.BillingPolicyId.ValueString() != "" {
114+
if !environmentSource.BillingPolicyId.IsNull() && environmentSource.BillingPolicyId.ValueString() != constants.ZERO_UUID {
114115
environmentDto.Properties.BillingPolicy = BillingPolicyDto{
115116
Id: environmentSource.BillingPolicyId.ValueString(),
116117
}
@@ -332,15 +333,15 @@ func convertEnvironmentGroupFromDto(environmentDto EnvironmentDto, model *Source
332333
if environmentDto.Properties.ParentEnvironmentGroup != nil {
333334
model.EnvironmentGroupId = types.StringValue(environmentDto.Properties.ParentEnvironmentGroup.Id)
334335
} else {
335-
model.EnvironmentGroupId = types.StringValue("")
336+
model.EnvironmentGroupId = types.StringValue(constants.ZERO_UUID)
336337
}
337338
}
338339

339340
func convertBillingPolicyModelFromDto(environmentDto EnvironmentDto, model *SourceModel) {
340-
if environmentDto.Properties.BillingPolicy != nil {
341+
if environmentDto.Properties.BillingPolicy != nil && environmentDto.Properties.BillingPolicy.Id != "" {
341342
model.BillingPolicyId = types.StringValue(environmentDto.Properties.BillingPolicy.Id)
342343
} else {
343-
model.BillingPolicyId = types.StringValue("")
344+
model.BillingPolicyId = types.StringValue(constants.ZERO_UUID)
344345
}
345346
}
346347

internal/services/environment/resource_environment.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,15 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
102102
},
103103
},
104104
"environment_group_id": schema.StringAttribute{
105-
MarkdownDescription: "Environment group id (guid) that the environment belongs to. See [Environment groups](https://learn.microsoft.com/en-us/power-platform/admin/environment-groups) for more information.",
105+
MarkdownDescription: "Environment group id (guid) that the environment belongs to. See [Environment groups](https://learn.microsoft.com/en-us/power-platform/admin/environment-groups) for more information. To remove the environment from the environment group, set this attribute to `00000000-0000-0000-0000-000000000000`",
106106
Computed: true,
107107
Optional: true,
108108
PlanModifiers: []planmodifier.String{
109109
stringplanmodifier.UseStateForUnknown(),
110110
},
111111
Validators: []validator.String{
112-
stringvalidator.RegexMatches(regexp.MustCompile(helpers.GuidOrEmptyValueRegex), "environment_group_id must be a valid environment group id guid"),
112+
stringvalidator.LengthAtLeast(1),
113+
stringvalidator.RegexMatches(regexp.MustCompile(helpers.GuidRegex), "environment_group_id must be a valid environment group id guid"),
113114
stringvalidator.AlsoRequires(path.Root("dataverse").Expression()),
114115
},
115116
},
@@ -200,12 +201,16 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
200201
},
201202
},
202203
"billing_policy_id": &schema.StringAttribute{
203-
MarkdownDescription: "Billing policy id (guid) for pay-as-you-go environments using Azure subscription billing",
204+
MarkdownDescription: "Billing policy id (guid) for pay-as-you-go environments using Azure subscription billing. To remove the environment from the billing policy, set this attribute to `00000000-0000-0000-0000-000000000000`",
204205
Optional: true,
205206
Computed: true,
206207
PlanModifiers: []planmodifier.String{
207208
stringplanmodifier.UseStateForUnknown(),
208209
},
210+
Validators: []validator.String{
211+
stringvalidator.LengthAtLeast(1),
212+
stringvalidator.RegexMatches(regexp.MustCompile(helpers.GuidRegex), "billing_policy_id must be a valid billing policy id guid"),
213+
},
209214
},
210215
"enterprise_policies": schema.SetNestedAttribute{
211216
MarkdownDescription: "Enterprise policies for the environment. See [Enterprise policies](https://learn.microsoft.com/en-us/power-platform/admin/enterprise-policies) for more details.",
@@ -285,6 +290,7 @@ func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp
285290
MarkdownDescription: "Security group id (guid). For an empty security group, set this property to `0000000-0000-0000-0000-000000000000`",
286291
Optional: true,
287292
Validators: []validator.String{
293+
stringvalidator.RegexMatches(regexp.MustCompile(helpers.GuidRegex), "security_group_id must be a valid security group guid id"),
288294
validators.MakeFieldRequiredWhenOtherFieldDoesNotHaveValue(path.Root("environment_type").Expression(), regexp.MustCompile(EnvironmentTypesExceptDeveloperRegex), "dataverse.security_group_id is required for all environment_type values except `Developer`"),
289295
},
290296
},
@@ -734,8 +740,8 @@ func (r *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp
734740
err := r.EnvironmentClient.DeleteEnvironment(ctx, state.Id.ValueString())
735741
if err != nil {
736742
resp.Diagnostics.AddError(fmt.Sprintf("Client error when deleting %s_%s", r.ProviderTypeName, r.TypeName), err.Error())
737-
return
738743
}
744+
resp.State.RemoveResource(ctx)
739745
}
740746

741747
func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
@@ -780,19 +786,16 @@ func (r *Resource) updateAllowBingSearch(ctx context.Context, plan *SourceModel)
780786
}
781787

782788
func updateEnvironmentGroupId(plan *SourceModel, environmentDto *EnvironmentDto) {
783-
if !plan.EnvironmentGroupId.IsNull() && plan.EnvironmentGroupId.ValueString() != "" {
784-
envGroupId := constants.ZERO_UUID
785-
if plan.EnvironmentGroupId.ValueString() != "" && plan.EnvironmentGroupId.ValueString() != constants.ZERO_UUID {
786-
envGroupId = plan.EnvironmentGroupId.ValueString()
787-
}
789+
// if there is no value in the plan, not even empty guid, then we do nothing, attribute stop bing tracked in state
790+
if !plan.EnvironmentGroupId.IsNull() {
788791
environmentDto.Properties.ParentEnvironmentGroup = &ParentEnvironmentGroupDto{
789-
Id: envGroupId,
792+
Id: plan.EnvironmentGroupId.ValueString(),
790793
}
791794
}
792795
}
793796

794797
func updateBillingPolicyId(plan *SourceModel, environmentDto *EnvironmentDto) {
795-
if !plan.BillingPolicyId.IsNull() && plan.BillingPolicyId.ValueString() != "" {
798+
if !plan.BillingPolicyId.IsNull() {
796799
environmentDto.Properties.BillingPolicy = &BillingPolicyDto{
797800
Id: plan.BillingPolicyId.ValueString(),
798801
}
@@ -814,7 +817,7 @@ func (r *Resource) updateDataverse(ctx context.Context, plan *SourceModel, state
814817
}
815818

816819
func (r *Resource) removeBillingPolicy(ctx context.Context, state *SourceModel) error {
817-
if !state.BillingPolicyId.IsNull() && !state.BillingPolicyId.IsUnknown() && state.BillingPolicyId.ValueString() != "" {
820+
if !state.BillingPolicyId.IsNull() && !state.BillingPolicyId.IsUnknown() && state.BillingPolicyId.ValueString() != constants.ZERO_UUID {
818821
tflog.Debug(ctx, fmt.Sprintf("Removing environment %s from billing policy %s", state.Id.ValueString(), state.BillingPolicyId.ValueString()))
819822
err := r.LicensingClient.RemoveEnvironmentsToBillingPolicy(ctx, state.BillingPolicyId.ValueString(), []string{state.Id.ValueString()})
820823
if err != nil {
@@ -825,7 +828,7 @@ func (r *Resource) removeBillingPolicy(ctx context.Context, state *SourceModel)
825828
}
826829

827830
func (r *Resource) addBillingPolicy(ctx context.Context, plan *SourceModel) error {
828-
if !plan.BillingPolicyId.IsNull() && !plan.BillingPolicyId.IsUnknown() && plan.BillingPolicyId.ValueString() != "" {
831+
if !plan.BillingPolicyId.IsNull() && !plan.BillingPolicyId.IsUnknown() && plan.BillingPolicyId.ValueString() != constants.ZERO_UUID {
829832
tflog.Debug(ctx, fmt.Sprintf("Adding environment %s to billing policy %s", plan.Id.ValueString(), plan.BillingPolicyId.ValueString()))
830833
err := r.LicensingClient.AddEnvironmentsToBillingPolicy(ctx, plan.BillingPolicyId.ValueString(), []string{plan.Id.ValueString()})
831834
if err != nil {

0 commit comments

Comments
 (0)