diff --git a/.changes/unreleased/added-20250307-024333.yaml b/.changes/unreleased/added-20250307-024333.yaml new file mode 100644 index 00000000..0f181e4a --- /dev/null +++ b/.changes/unreleased/added-20250307-024333.yaml @@ -0,0 +1,5 @@ +kind: added +body: Onboard new resource/data-source `fabric_gateway`. +time: 2025-03-07T02:43:33.229242334Z +custom: + Issue: "170" diff --git a/.gitignore b/.gitignore index 6dec83b0..fa5fb9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,6 @@ testresults.xml golangci-report.xml .wellknown.json .wellknown.*.json -!.wellknown.template.json changie.md .lycheecache *.env diff --git a/DEVELOPER.md b/DEVELOPER.md index bac7507c..90b5d954 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -298,6 +298,8 @@ FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID="" FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME="" FABRIC_TESTACC_WELLKNOWN_AZDO_ORGANIZATION_NAME="" FABRIC_TESTACC_WELLKNOWN_NAME_PREFIX="" +FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME="" +FABRIC_TESTACC_WELLKNOWN_AZURE_LOCATION"" # Optional FABRIC_TESTACC_WELLKNOWN_NAME_SUFFIX="" diff --git a/docs/data-sources/gateway.md b/docs/data-sources/gateway.md new file mode 100644 index 00000000..215d9639 --- /dev/null +++ b/docs/data-sources/gateway.md @@ -0,0 +1,67 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "fabric_gateway Data Source - terraform-provider-fabric" +subcategory: "" +description: |- + Get a Fabric Gateway. + Use this data source to get Gateway https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways. + -> This item supports Service Principal authentication. +--- + +# fabric_gateway (Data Source) + +Get a Fabric Gateway. + +Use this data source to get [Gateway](https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways). + +-> This item supports Service Principal authentication. + + +## Schema + +### Optional + +- `display_name` (String) The Gateway display name. +- `id` (String) The Gateway ID. +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `allow_cloud_connection_refresh` (Boolean) Allow cloud connection refresh. +- `allow_custom_connectors` (Boolean) Allow custom connectors. +- `capacity_id` (String) The Gateway capacity ID. +- `inactivity_minutes_before_sleep` (Number) The Gateway inactivity minutes before sleep. Possible values: `120`, `1440`, `150`, `240`, `30`, `360`, `480`, `60`, `720`, `90` +- `load_balancing_setting` (String) The load balancing setting. Possible values: `DistributeEvenly`, `Failover` +- `number_of_member_gateways` (Number) The number of member gateways. Possible values: 1 to 7. +- `public_key` (Attributes) The public key of the primary gateway member. Used to encrypt the credentials for creating and updating connections. (see [below for nested schema](#nestedatt--public_key)) +- `type` (String) The Gateway type. Possible values: `OnPremises`, `OnPremisesPersonal`, `VirtualNetwork` +- `version` (String) The Gateway version. +- `virtual_network_azure_resource` (Attributes) The Azure virtual network resource. (see [below for nested schema](#nestedatt--virtual_network_azure_resource)) + + + +### Nested Schema for `timeouts` + +Optional: + +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `public_key` + +Read-Only: + +- `exponent` (String) The exponent. +- `modulus` (String) The modulus. + + + +### Nested Schema for `virtual_network_azure_resource` + +Read-Only: + +- `resource_group_name` (String) The resource group name. +- `subnet_name` (String) The subnet name. +- `subscription_id` (String) The subscription ID. +- `virtual_network_name` (String) The virtual network name. diff --git a/docs/data-sources/gateway_role_assignments.md b/docs/data-sources/gateway_role_assignments.md new file mode 100644 index 00000000..83d79982 --- /dev/null +++ b/docs/data-sources/gateway_role_assignments.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "fabric_gateway_role_assignments Data Source - terraform-provider-fabric" +subcategory: "" +description: |- + List Fabric Gateway Role Assignments. + Use this data source to list [Gateway Role Assignments]. + -> This item supports Service Principal authentication. +--- + +# fabric_gateway_role_assignments (Data Source) + +List Fabric Gateway Role Assignments. + +Use this data source to list [Gateway Role Assignments]. + +-> This item supports Service Principal authentication. + + +## Schema + +### Required + +- `gateway_id` (String) The Gateway ID. + +### Optional + +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `values` (Attributes List) The list of Gateway Role Assignments. (see [below for nested schema](#nestedatt--values)) + + + +### Nested Schema for `timeouts` + +Optional: + +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `values` + +Read-Only: + +- `id` (String) The Principal ID. +- `role` (String) The gateway role of the principal. Possible values: `Admin`, `ConnectionCreator`, `ConnectionCreatorWithResharing`. +- `type` (String) The type of the principal. Possible values: `Group`, `ServicePrincipal`, `ServicePrincipalProfile`, `User`. diff --git a/docs/data-sources/gateways.md b/docs/data-sources/gateways.md new file mode 100644 index 00000000..9f909848 --- /dev/null +++ b/docs/data-sources/gateways.md @@ -0,0 +1,78 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "fabric_gateways Data Source - terraform-provider-fabric" +subcategory: "" +description: |- + List a Fabric Gateways. + Use this data source to list Gateways https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways. + -> This item supports Service Principal authentication. +--- + +# fabric_gateways (Data Source) + +List a Fabric Gateways. + +Use this data source to list [Gateways](https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways). + +-> This item supports Service Principal authentication. + + +## Schema + +### Optional + +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `values` (Attributes List) The list of Gateways. (see [below for nested schema](#nestedatt--values)) + + + +### Nested Schema for `timeouts` + +Optional: + +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `values` + +Optional: + +- `display_name` (String) The Gateway display name. +- `id` (String) The Gateway ID. + +Read-Only: + +- `allow_cloud_connection_refresh` (Boolean) Allow cloud connection refresh. +- `allow_custom_connectors` (Boolean) Allow custom connectors. +- `capacity_id` (String) The Gateway capacity ID. +- `inactivity_minutes_before_sleep` (Number) The Gateway inactivity minutes before sleep. Possible values: `120`, `1440`, `150`, `240`, `30`, `360`, `480`, `60`, `720`, `90` +- `load_balancing_setting` (String) The load balancing setting. Possible values: `DistributeEvenly`, `Failover` +- `number_of_member_gateways` (Number) The number of member gateways. Possible values: 1 to 7. +- `public_key` (Attributes) The public key of the primary gateway member. Used to encrypt the credentials for creating and updating connections. (see [below for nested schema](#nestedatt--values--public_key)) +- `type` (String) The Gateway type. Possible values: `OnPremises`, `OnPremisesPersonal`, `VirtualNetwork` +- `version` (String) The Gateway version. +- `virtual_network_azure_resource` (Attributes) The Azure virtual network resource. (see [below for nested schema](#nestedatt--values--virtual_network_azure_resource)) + + + +### Nested Schema for `values.public_key` + +Read-Only: + +- `exponent` (String) The exponent. +- `modulus` (String) The modulus. + + + +### Nested Schema for `values.virtual_network_azure_resource` + +Read-Only: + +- `resource_group_name` (String) The resource group name. +- `subnet_name` (String) The subnet name. +- `subscription_id` (String) The subscription ID. +- `virtual_network_name` (String) The virtual network name. diff --git a/docs/data-sources/workspace_role_assignments.md b/docs/data-sources/workspace_role_assignments.md index f85c6967..a4df7148 100644 --- a/docs/data-sources/workspace_role_assignments.md +++ b/docs/data-sources/workspace_role_assignments.md @@ -3,14 +3,14 @@ page_title: "fabric_workspace_role_assignments Data Source - terraform-provider-fabric" subcategory: "" description: |- - List a Fabric Workspace Role Assignments. + List Fabric Workspace Role Assignments. Use this data source to list Workspace Role Assignments https://learn.microsoft.com/power-bi/collaborate-share/service-roles-new-workspaces. -> This item supports Service Principal authentication. --- # fabric_workspace_role_assignments (Data Source) -List a Fabric Workspace Role Assignments. +List Fabric Workspace Role Assignments. Use this data source to list [Workspace Role Assignments](https://learn.microsoft.com/power-bi/collaborate-share/service-roles-new-workspaces). diff --git a/docs/resources/gateway.md b/docs/resources/gateway.md new file mode 100644 index 00000000..f1c876a0 --- /dev/null +++ b/docs/resources/gateway.md @@ -0,0 +1,59 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "fabric_gateway Resource - terraform-provider-fabric" +subcategory: "" +description: |- + This resource manages a Fabric Gateway. + See Gateways https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways for more information. + -> This item supports Service Principal authentication. +--- + +# fabric_gateway (Resource) + +This resource manages a Fabric Gateway. + +See [Gateways](https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways) for more information. + +-> This item supports Service Principal authentication. + + +## Schema + +### Required + +- `type` (String) The Gateway type. Accepted values: `OnPremises`, `OnPremisesPersonal`, `VirtualNetwork` + +### Optional + +- `capacity_id` (String) The Gateway capacity ID. +- `display_name` (String) The Gateway display name. +- `inactivity_minutes_before_sleep` (Number) The Gateway inactivity minutes before sleep. +- `number_of_member_gateways` (Number) The Gateway number of member gateways. +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) +- `virtual_network_azure_resource` (Attributes) The Gateway virtual network Azure resource. (see [below for nested schema](#nestedatt--virtual_network_azure_resource)) + +### Read-Only + +- `id` (String) The Gateway ID. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `virtual_network_azure_resource` + +Required: + +- `resource_group_name` (String) The resource group name. +- `subnet_name` (String) The subnet name. +- `subscription_id` (String) The subscription ID. +- `virtual_network_name` (String) The virtual network name. diff --git a/docs/resources/gateway_role_assignment.md b/docs/resources/gateway_role_assignment.md new file mode 100644 index 00000000..8457db24 --- /dev/null +++ b/docs/resources/gateway_role_assignment.md @@ -0,0 +1,43 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "fabric_gateway_role_assignment Resource - terraform-provider-fabric" +subcategory: "" +description: |- + Manage a Gateway Role Assignment. + -> This item supports Service Principal authentication. +--- + +# fabric_gateway_role_assignment (Resource) + +Manage a Gateway Role Assignment. + +-> This item supports Service Principal authentication. + + +## Schema + +### Required + +- `gateway_id` (String) The Gateway ID. +- `principal_id` (String) The Principal ID. +- `principal_type` (String) The type of the principal. Accepted values: `Group`, `ServicePrincipal`, `ServicePrincipalProfile`, `User`. +- `role` (String) The Gateway Role of the principal. Accepted values: `Admin`, `ConnectionCreator`, `ConnectionCreatorWithResharing`. + +### Optional + +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `id` (String) The Gateway Role Assignment ID. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 01b5f144..97496711 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -45,6 +45,7 @@ import ( "github.com/microsoft/terraform-provider-fabric/internal/services/environment" "github.com/microsoft/terraform-provider-fabric/internal/services/eventhouse" "github.com/microsoft/terraform-provider-fabric/internal/services/eventstream" + "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" "github.com/microsoft/terraform-provider-fabric/internal/services/graphqlapi" "github.com/microsoft/terraform-provider-fabric/internal/services/kqldashboard" "github.com/microsoft/terraform-provider-fabric/internal/services/kqldatabase" @@ -390,6 +391,8 @@ func (p *FabricProvider) Resources(ctx context.Context) []func() resource.Resour func() resource.Resource { return environment.NewResourceEnvironment(ctx) }, func() resource.Resource { return eventhouse.NewResourceEventhouse(ctx) }, eventstream.NewResourceEventstream, + gateway.NewResourceGateway, + gateway.NewResourceGatewayRoleAssignment, graphqlapi.NewResourceGraphQLApi, kqldashboard.NewResourceKQLDashboard, kqldatabase.NewResourceKQLDatabase, @@ -430,6 +433,9 @@ func (p *FabricProvider) DataSources(ctx context.Context) []func() datasource.Da func() datasource.DataSource { return eventhouse.NewDataSourceEventhouses(ctx) }, eventstream.NewDataSourceEventstream, eventstream.NewDataSourceEventstreams, + gateway.NewDataSourceGateway, + gateway.NewDataSourceGateways, + gateway.NewDataSourceGatewayRoleAssignments, graphqlapi.NewDataSourceGraphQLApi, graphqlapi.NewDataSourceGraphQLApis, kqldashboard.NewDataSourceKQLDashboard, diff --git a/internal/services/gateway/base.go b/internal/services/gateway/base.go new file mode 100644 index 00000000..73750c23 --- /dev/null +++ b/internal/services/gateway/base.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "github.com/microsoft/terraform-provider-fabric/internal/common" +) + +const ( + ItemName = "Gateway" + ItemTFName = "gateway" + ItemsName = "Gateways" + ItemsTFName = "gateways" + ItemDocsSPNSupport = common.DocsSPNSupported + ItemDocsURL = "https://learn.microsoft.com/power-bi/guidance/powerbi-implementation-planning-data-gateways" + GatewayRoleAssignmentName = "Gateway Role Assignment" + GatewayRoleAssignmentTFName = "gateway_role_assignment" + GatewayRoleAssignmentsName = "Gateway Role Assignments" + GatewayRoleAssignmentsTFName = "gateway_role_assignments" + ItemPreview = true +) + +var ( + PossibleInactivityMinutesBeforeSleepValues = []int32{30, 60, 90, 120, 150, 240, 360, 480, 720, 1440} //nolint:gochecknoglobals + MinNumberOfMemberGatewaysValues = int32(1) //nolint:gochecknoglobals + MaxNumberOfMemberGatewaysValues = int32(7) //nolint:gochecknoglobals +) diff --git a/internal/services/gateway/base_test.go b/internal/services/gateway/base_test.go new file mode 100644 index 00000000..5b135de1 --- /dev/null +++ b/internal/services/gateway/base_test.go @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" +) + +const ( + itemTFName = gateway.ItemTFName + itemsTFName = gateway.ItemsTFName + gatewayRoleAssignmentTFName = gateway.GatewayRoleAssignmentTFName + gatewayRoleAssignmentsTFName = gateway.GatewayRoleAssignmentsTFName +) diff --git a/internal/services/gateway/data_gateway.go b/internal/services/gateway/data_gateway.go new file mode 100644 index 00000000..d48b50a0 --- /dev/null +++ b/internal/services/gateway/data_gateway.go @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-log/tflog" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/pkg/utils" + pconfig "github.com/microsoft/terraform-provider-fabric/internal/provider/config" +) + +var ( + _ datasource.DataSourceWithConfigValidators = (*dataSourceGateway)(nil) + _ datasource.DataSourceWithConfigure = (*dataSourceGateway)(nil) +) + +type dataSourceGateway struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceGateway() datasource.DataSource { + return &dataSourceGateway{} +} + +func (d *dataSourceGateway) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + ItemTFName +} + +func (d *dataSourceGateway) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Get a Fabric " + ItemName + ".\n\n" + + "Use this data source to get [" + ItemName + "](" + ItemDocsURL + ").\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " ID.", + Optional: true, + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " display name.", + Optional: true, + Computed: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " type. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossibleGatewayTypeValues(), true, true), + Computed: true, + }, + "capacity_id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " capacity ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "inactivity_minutes_before_sleep": schema.Int32Attribute{ + MarkdownDescription: "The " + ItemName + " inactivity minutes before sleep. Possible values: " + utils.ConvertStringSlicesToString(PossibleInactivityMinutesBeforeSleepValues, true, true), + Computed: true, + }, + "number_of_member_gateways": schema.Int32Attribute{ + MarkdownDescription: "The number of member gateways. Possible values: " + strconv.Itoa(int(MinNumberOfMemberGatewaysValues)) + " to " + strconv.Itoa(int(MaxNumberOfMemberGatewaysValues)) + ".", + Computed: true, + }, + "virtual_network_azure_resource": schema.SingleNestedAttribute{ + MarkdownDescription: "The Azure virtual network resource.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[virtualNetworkAzureResourceModel](ctx), + Attributes: map[string]schema.Attribute{ + "resource_group_name": schema.StringAttribute{ + MarkdownDescription: "The resource group name.", + Computed: true, + }, + "subnet_name": schema.StringAttribute{ + MarkdownDescription: "The subnet name.", + Computed: true, + }, + "subscription_id": schema.StringAttribute{ + MarkdownDescription: "The subscription ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "virtual_network_name": schema.StringAttribute{ + MarkdownDescription: "The virtual network name.", + Computed: true, + }, + }, + }, + "allow_cloud_connection_refresh": schema.BoolAttribute{ + MarkdownDescription: "Allow cloud connection refresh.", + Computed: true, + }, + "allow_custom_connectors": schema.BoolAttribute{ + MarkdownDescription: "Allow custom connectors.", + Computed: true, + }, + "load_balancing_setting": schema.StringAttribute{ + MarkdownDescription: "The load balancing setting. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossibleLoadBalancingSettingValues(), true, true), + Computed: true, + }, + "public_key": schema.SingleNestedAttribute{ + MarkdownDescription: "The public key of the primary gateway member. Used to encrypt the credentials for creating and updating connections.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[publicKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "exponent": schema.StringAttribute{ + MarkdownDescription: "The exponent.", + Computed: true, + }, + "modulus": schema.StringAttribute{ + MarkdownDescription: "The modulus.", + Computed: true, + }, + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " version.", + Computed: true, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceGateway) ConfigValidators(_ context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.Conflicting( + path.MatchRoot("id"), + path.MatchRoot("display_name"), + ), + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("display_name"), + ), + } +} + +func (d *dataSourceGateway) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + pConfigData, ok := req.ProviderData.(*pconfig.ProviderData) + if !ok { + resp.Diagnostics.AddError( + common.ErrorDataSourceConfigType, + fmt.Sprintf(common.ErrorFabricClientType, req.ProviderData), + ) + + return + } + + d.pConfigData = pConfigData + d.client = fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient() +} + +func (d *dataSourceGateway) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "READ", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "READ", map[string]any{ + "config": req.Config, + }) + + var data dataSourceGatewayModel + + if resp.Diagnostics.Append(req.Config.Get(ctx, &data)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := data.Timeouts.Read(ctx, d.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + if data.ID.ValueString() != "" { + diags = d.getByID(ctx, &data) + } else { + diags = d.getByDisplayName(ctx, &data) + } + + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (d *dataSourceGateway) getByID(ctx context.Context, model *dataSourceGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, "GET "+ItemName+" BY ID", map[string]any{ + "id": model.ID.ValueString(), + }) + + respGet, err := d.client.GetGateway(ctx, model.ID.ValueString(), nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationRead, nil); diags.HasError() { + return diags + } + + if diags := model.set(ctx, respGet.GatewayClassification); diags.HasError() { + return diags + } + + return nil +} + +func (d *dataSourceGateway) getByDisplayName(ctx context.Context, model *dataSourceGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, "GET "+ItemName+" BY DISPLAY NAME", map[string]any{ + "display_name": model.DisplayName.ValueString(), + }) + + var diags diag.Diagnostics + + pager := d.client.NewListGatewaysPager(nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + return diags + } + + for _, entity := range page.Value { + var entityDisplayName string + + switch gateway := entity.(type) { + case *fabcore.VirtualNetworkGateway: + entityDisplayName = *(gateway.DisplayName) + case *fabcore.OnPremisesGateway: + entityDisplayName = *(gateway.DisplayName) + default: + continue + } + + if entityDisplayName == model.DisplayName.ValueString() { + model.ID = customtypes.NewUUIDPointerValue(entity.GetGateway().ID) + + return d.getByID(ctx, model) + } + } + } + + diags.AddError( + common.ErrorReadHeader, + "Unable to find Gateway with 'display_name': "+model.DisplayName.ValueString(), + ) + + return diags +} diff --git a/internal/services/gateway/data_gateway_role_assignments.go b/internal/services/gateway/data_gateway_role_assignments.go new file mode 100644 index 00000000..43f95ca4 --- /dev/null +++ b/internal/services/gateway/data_gateway_role_assignments.go @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/pkg/utils" + pconfig "github.com/microsoft/terraform-provider-fabric/internal/provider/config" +) + +var _ datasource.DataSourceWithConfigure = (*dataSourceGatewayRoleAssignments)(nil) + +type dataSourceGatewayRoleAssignments struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceGatewayRoleAssignments() datasource.DataSource { + return &dataSourceGatewayRoleAssignments{} +} + +func (d *dataSourceGatewayRoleAssignments) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + GatewayRoleAssignmentsTFName +} + +func (d *dataSourceGatewayRoleAssignments) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "List Fabric " + GatewayRoleAssignmentsName + ".\n\n" + + "Use this data source to list [" + GatewayRoleAssignmentsName + "].\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "gateway_id": schema.StringAttribute{ + MarkdownDescription: "The Gateway ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + }, + "values": schema.ListNestedAttribute{ + MarkdownDescription: "The list of " + GatewayRoleAssignmentsName + ".", + Computed: true, + CustomType: supertypes.NewListNestedObjectTypeOf[gatewayRoleAssignmentModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The Principal ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "role": schema.StringAttribute{ + MarkdownDescription: "The gateway role of the principal. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossibleGatewayRoleValues(), true, true) + ".", + Computed: true, + }, + // "display_name": schema.StringAttribute{ + // MarkdownDescription: "The principal's display name.", + // Computed: true, + // }, + "type": schema.StringAttribute{ + MarkdownDescription: "The type of the principal. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossiblePrincipalTypeValues(), true, true) + ".", + Computed: true, + }, + // "details": schema.SingleNestedAttribute{ + // MarkdownDescription: "The principal details.", + // Computed: true, + // CustomType: supertypes.NewSingleNestedObjectTypeOf[principalDetailsModel](ctx), + // Attributes: map[string]schema.Attribute{ + // "user_principal_name": schema.StringAttribute{ + // MarkdownDescription: "The user principal name.", + // Computed: true, + // }, + // "group_type": schema.StringAttribute{ + // MarkdownDescription: "The type of the group. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossibleGroupTypeValues(), true, true) + ".", + // Computed: true, + // }, + // "app_id": schema.StringAttribute{ + // MarkdownDescription: "The service principal's Microsoft Entra App ID.", + // Computed: true, + // CustomType: customtypes.UUIDType{}, + // }, + // "parent_principal_id": schema.StringAttribute{ + // MarkdownDescription: "The parent principal ID of Service Principal Profile.", + // Computed: true, + // CustomType: customtypes.UUIDType{}, + // }, + // }, + // }, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceGatewayRoleAssignments) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + pConfigData, ok := req.ProviderData.(*pconfig.ProviderData) + if !ok { + resp.Diagnostics.AddError( + common.ErrorDataSourceConfigType, + fmt.Sprintf(common.ErrorFabricClientType, req.ProviderData), + ) + + return + } + + d.pConfigData = pConfigData + d.client = fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient() +} + +func (d *dataSourceGatewayRoleAssignments) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "READ", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "READ", map[string]any{ + "config": req.Config, + }) + + var data dataSourceGatewayRoleAssignmentsModel + + if resp.Diagnostics.Append(req.Config.Get(ctx, &data)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := data.Timeouts.Read(ctx, d.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + if resp.Diagnostics.Append(d.list(ctx, &data)...); resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (d *dataSourceGatewayRoleAssignments) list(ctx context.Context, model *dataSourceGatewayRoleAssignmentsModel) diag.Diagnostics { + tflog.Trace(ctx, "getting "+GatewayRoleAssignmentsName) + + respList, err := d.client.ListGatewayRoleAssignments(ctx, model.GatewayID.ValueString(), nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + return diags + } + + return model.setValues(ctx, respList) +} diff --git a/internal/services/gateway/data_gateway_role_assignments_test.go b/internal/services/gateway/data_gateway_role_assignments_test.go new file mode 100644 index 00000000..9ed01688 --- /dev/null +++ b/internal/services/gateway/data_gateway_role_assignments_test.go @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "regexp" + "testing" + + at "github.com/dcarbone/terraform-plugin-framework-utils/v3/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp/fakes" +) + +var ( + testDataSourceGatewayRoleAssignments = testhelp.DataSourceFQN("fabric", gatewayRoleAssignmentsTFName, "test") + testDataSourceGatewayRoleAssignmentsHeader = at.DataSourceHeader(testhelp.TypeName("fabric", gatewayRoleAssignmentsTFName), "test") +) + +func TestUnit_GatewayRoleAssignmentsDataSource(t *testing.T) { + gatewayID := testhelp.RandomUUID() + gatewayRoleAssignments := NewRandomGatewayRoleAssignments() + fakes.FakeServer.ServerFactory.Core.GatewaysServer.NewListGatewayRoleAssignmentsPager = fakeGatewayRoleAssignments(gatewayRoleAssignments) + + entity := gatewayRoleAssignments.Value[1] + + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, nil, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - unexpected_attr + { + Config: at.CompileConfig( + testDataSourceGatewayRoleAssignmentsHeader, + map[string]any{ + "gateway_id": gatewayID, + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // read + { + Config: at.CompileConfig( + testDataSourceGatewayRoleAssignmentsHeader, + map[string]any{ + "gateway_id": gatewayID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceGatewayRoleAssignments, "gateway_id", gatewayID), + resource.TestCheckResourceAttrPtr(testDataSourceGatewayRoleAssignments, "values.1.id", entity.ID), + resource.TestCheckResourceAttrPtr(testDataSourceGatewayRoleAssignments, "values.1.role", (*string)(entity.Role)), + // resource.TestCheckResourceAttrPtr(testDataSourceGatewayRoleAssignments, "values.1.display_name", entity.Principal.DisplayName), + resource.TestCheckResourceAttrPtr(testDataSourceGatewayRoleAssignments, "values.1.type", (*string)(entity.Principal.Type)), + ), + }, + })) +} + +func TestAcc_GatewayRoleAssignmentsDataSource(t *testing.T) { + gateway := testhelp.WellKnown()["GatewayVirtualNetwork"].(map[string]any) + gatewayID := gateway["id"].(string) + + resource.ParallelTest(t, testhelp.NewTestAccCase(t, nil, nil, []resource.TestStep{ + // read + { + Config: at.CompileConfig( + testDataSourceGatewayRoleAssignmentsHeader, + map[string]any{ + "gateway_id": gatewayID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceGatewayRoleAssignments, "gateway_id", gatewayID), + resource.TestCheckResourceAttrSet(testDataSourceGatewayRoleAssignments, "values.0.id"), + ), + }, + }, + )) +} diff --git a/internal/services/gateway/data_gateway_test.go b/internal/services/gateway/data_gateway_test.go new file mode 100644 index 00000000..fcb83f3a --- /dev/null +++ b/internal/services/gateway/data_gateway_test.go @@ -0,0 +1,235 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "regexp" + "strconv" + "testing" + + at "github.com/dcarbone/terraform-plugin-framework-utils/v3/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp/fakes" +) + +var ( + testDataSourceItemFQN = testhelp.DataSourceFQN("fabric", itemTFName, "test") + testDataSourceItemHeader = at.DataSourceHeader(testhelp.TypeName("fabric", itemTFName), "test") +) + +func TestUnit_GatewayDataSource(t *testing.T) { + virtualNetworkGateway := fakes.NewRandomVirtualNetworkGateway() + onPremisesGateway := fakes.NewRandomOnPremisesGateway() + onPremisesGatewayPersonalGateway := fakes.NewRandomOnPremisesGatewayPersonal() + + fakes.FakeServer.Upsert(fakes.NewRandomGateway()) + fakes.FakeServer.Upsert(virtualNetworkGateway) + fakes.FakeServer.Upsert(onPremisesGateway) + fakes.FakeServer.Upsert(onPremisesGatewayPersonalGateway) + fakes.FakeServer.Upsert(fakes.NewRandomGateway()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, nil, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - no attributes + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{}, + ), + ExpectError: regexp.MustCompile(`Missing Attribute Configuration`), + }, + // error - id - invalid UUID + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": "invalid uuid", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // error - unexpected attribute + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": *virtualNetworkGateway.ID, + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // read by id - virtual network + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": *virtualNetworkGateway.ID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "id", virtualNetworkGateway.ID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "type", (*string)(virtualNetworkGateway.Type)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "display_name", virtualNetworkGateway.DisplayName), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "capacity_id", virtualNetworkGateway.CapacityID), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(int(*virtualNetworkGateway.InactivityMinutesBeforeSleep))), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*virtualNetworkGateway.NumberOfMemberGateways))), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.resource_group_name", virtualNetworkGateway.VirtualNetworkAzureResource.ResourceGroupName), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.subnet_name", virtualNetworkGateway.VirtualNetworkAzureResource.SubnetName), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.subscription_id", virtualNetworkGateway.VirtualNetworkAzureResource.SubscriptionID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.virtual_network_name", virtualNetworkGateway.VirtualNetworkAzureResource.VirtualNetworkName), + ), + }, + // read by name - virtual network + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "display_name": *virtualNetworkGateway.DisplayName, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "id", virtualNetworkGateway.ID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "type", (*string)(virtualNetworkGateway.Type)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "display_name", virtualNetworkGateway.DisplayName), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "capacity_id", virtualNetworkGateway.CapacityID), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(int(*virtualNetworkGateway.InactivityMinutesBeforeSleep))), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*virtualNetworkGateway.NumberOfMemberGateways))), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.resource_group_name", virtualNetworkGateway.VirtualNetworkAzureResource.ResourceGroupName), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.subnet_name", virtualNetworkGateway.VirtualNetworkAzureResource.SubnetName), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.subscription_id", virtualNetworkGateway.VirtualNetworkAzureResource.SubscriptionID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "virtual_network_azure_resource.virtual_network_name", virtualNetworkGateway.VirtualNetworkAzureResource.VirtualNetworkName), + ), + }, + // read by id - on premises + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": *onPremisesGateway.ID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "id", onPremisesGateway.ID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "type", (*string)(onPremisesGateway.Type)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "display_name", onPremisesGateway.DisplayName), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "allow_cloud_connection_refresh", strconv.FormatBool(*onPremisesGateway.AllowCloudConnectionRefresh)), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "allow_custom_connectors", strconv.FormatBool(*onPremisesGateway.AllowCustomConnectors)), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*onPremisesGateway.NumberOfMemberGateways))), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "load_balancing_setting", string(*onPremisesGateway.LoadBalancingSetting)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "version", onPremisesGateway.Version), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "public_key.exponent", onPremisesGateway.PublicKey.Exponent), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "public_key.modulus", onPremisesGateway.PublicKey.Modulus), + ), + }, + // read by name - on premises + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "display_name": *onPremisesGateway.DisplayName, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "id", onPremisesGateway.ID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "type", (*string)(onPremisesGateway.Type)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "display_name", onPremisesGateway.DisplayName), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "allow_cloud_connection_refresh", strconv.FormatBool(*onPremisesGateway.AllowCloudConnectionRefresh)), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "allow_custom_connectors", strconv.FormatBool(*onPremisesGateway.AllowCustomConnectors)), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*onPremisesGateway.NumberOfMemberGateways))), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "load_balancing_setting", string(*onPremisesGateway.LoadBalancingSetting)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "version", onPremisesGateway.Version), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "public_key.exponent", onPremisesGateway.PublicKey.Exponent), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "public_key.modulus", onPremisesGateway.PublicKey.Modulus), + ), + }, + // read by id - on premises personal + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": *onPremisesGatewayPersonalGateway.ID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "id", onPremisesGatewayPersonalGateway.ID), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "type", (*string)(onPremisesGatewayPersonalGateway.Type)), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "version", onPremisesGatewayPersonalGateway.Version), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "public_key.exponent", onPremisesGatewayPersonalGateway.PublicKey.Exponent), + resource.TestCheckResourceAttrPtr(testDataSourceItemFQN, "public_key.modulus", onPremisesGatewayPersonalGateway.PublicKey.Modulus), + ), + }, + // read by id - not found + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // read by name - not found + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "display_name": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + })) +} + +func TestAcc_GatewayDataSource(t *testing.T) { + entityVirtualNetwork := testhelp.WellKnown()["GatewayVirtualNetwork"].(map[string]any) + entityVirtualNetworkID := entityVirtualNetwork["id"].(string) + entityVirtualNetworkDisplayName := entityVirtualNetwork["displayName"].(string) + entityVirtualNetworkType := entityVirtualNetwork["type"].(string) + + resource.ParallelTest(t, testhelp.NewTestAccCase(t, nil, nil, []resource.TestStep{ + // read by id - not found + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // read by id - virtual network + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "id": entityVirtualNetworkID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceItemFQN, "id", entityVirtualNetworkID), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "display_name", entityVirtualNetworkDisplayName), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "type", entityVirtualNetworkType), + ), + }, + // read by name- virtual network + { + Config: at.CompileConfig( + testDataSourceItemHeader, + map[string]any{ + "display_name": entityVirtualNetworkDisplayName, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceItemFQN, "id", entityVirtualNetworkID), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "display_name", entityVirtualNetworkDisplayName), + resource.TestCheckResourceAttr(testDataSourceItemFQN, "type", entityVirtualNetworkType), + ), + }, + })) +} diff --git a/internal/services/gateway/data_gateways.go b/internal/services/gateway/data_gateways.go new file mode 100644 index 00000000..a56706a2 --- /dev/null +++ b/internal/services/gateway/data_gateways.go @@ -0,0 +1,208 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/pkg/utils" + pconfig "github.com/microsoft/terraform-provider-fabric/internal/provider/config" +) + +var _ datasource.DataSourceWithConfigure = (*dataSourceGateways)(nil) + +type dataSourceGateways struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceGateways() datasource.DataSource { + return &dataSourceGateways{} +} + +func (d *dataSourceGateways) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + ItemsTFName +} + +func (d *dataSourceGateways) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "List a Fabric " + ItemsName + ".\n\n" + + "Use this data source to list [" + ItemsName + "](" + ItemDocsURL + ").\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "values": schema.ListNestedAttribute{ + MarkdownDescription: "The list of " + ItemsName + ".", + Computed: true, + CustomType: supertypes.NewListNestedObjectTypeOf[baseDataSourceGatewayModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " ID.", + Optional: true, + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " display name.", + Optional: true, + Computed: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " type. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossibleGatewayTypeValues(), true, true), + Computed: true, + }, + "capacity_id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " capacity ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "inactivity_minutes_before_sleep": schema.Int32Attribute{ + MarkdownDescription: "The " + ItemName + " inactivity minutes before sleep. Possible values: " + utils.ConvertStringSlicesToString(PossibleInactivityMinutesBeforeSleepValues, true, true), + Computed: true, + }, + "number_of_member_gateways": schema.Int32Attribute{ + MarkdownDescription: "The number of member gateways. Possible values: " + strconv.Itoa(int(MinNumberOfMemberGatewaysValues)) + " to " + strconv.Itoa(int(MaxNumberOfMemberGatewaysValues)) + ".", + Computed: true, + }, + "virtual_network_azure_resource": schema.SingleNestedAttribute{ + MarkdownDescription: "The Azure virtual network resource.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[virtualNetworkAzureResourceModel](ctx), + Attributes: map[string]schema.Attribute{ + "resource_group_name": schema.StringAttribute{ + MarkdownDescription: "The resource group name.", + Computed: true, + }, + "subnet_name": schema.StringAttribute{ + MarkdownDescription: "The subnet name.", + Computed: true, + }, + "subscription_id": schema.StringAttribute{ + MarkdownDescription: "The subscription ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "virtual_network_name": schema.StringAttribute{ + MarkdownDescription: "The virtual network name.", + Computed: true, + }, + }, + }, + "allow_cloud_connection_refresh": schema.BoolAttribute{ + MarkdownDescription: "Allow cloud connection refresh.", + Computed: true, + }, + "allow_custom_connectors": schema.BoolAttribute{ + MarkdownDescription: "Allow custom connectors.", + Computed: true, + }, + "load_balancing_setting": schema.StringAttribute{ + MarkdownDescription: "The load balancing setting. Possible values: " + utils.ConvertStringSlicesToString(fabcore.PossibleLoadBalancingSettingValues(), true, true), + Computed: true, + }, + "public_key": schema.SingleNestedAttribute{ + MarkdownDescription: "The public key of the primary gateway member. Used to encrypt the credentials for creating and updating connections.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[publicKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "exponent": schema.StringAttribute{ + MarkdownDescription: "The exponent.", + Computed: true, + }, + "modulus": schema.StringAttribute{ + MarkdownDescription: "The modulus.", + Computed: true, + }, + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " version.", + Computed: true, + }, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceGateways) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + pConfigData, ok := req.ProviderData.(*pconfig.ProviderData) + if !ok { + resp.Diagnostics.AddError( + common.ErrorDataSourceConfigType, + fmt.Sprintf(common.ErrorFabricClientType, req.ProviderData), + ) + + return + } + + d.pConfigData = pConfigData + d.client = fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient() +} + +func (d *dataSourceGateways) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "READ", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "READ", map[string]any{ + "config": req.Config, + }) + + var data dataSourceGatewaysModel + + if resp.Diagnostics.Append(req.Config.Get(ctx, &data)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := data.Timeouts.Read(ctx, d.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + if resp.Diagnostics.Append(d.list(ctx, &data)...); resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (d *dataSourceGateways) list(ctx context.Context, model *dataSourceGatewaysModel) diag.Diagnostics { + tflog.Trace(ctx, "getting "+ItemsName) + + respList, err := d.client.ListGateways(ctx, nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + return diags + } + + return model.setValues(ctx, respList) +} diff --git a/internal/services/gateway/data_gateways_test.go b/internal/services/gateway/data_gateways_test.go new file mode 100644 index 00000000..705189ce --- /dev/null +++ b/internal/services/gateway/data_gateways_test.go @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "regexp" + "testing" + + at "github.com/dcarbone/terraform-plugin-framework-utils/v3/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp/fakes" +) + +var ( + testDataSourceItemsFQN = testhelp.DataSourceFQN("fabric", itemsTFName, "test") + testDataSourceItemsHeader = at.DataSourceHeader(testhelp.TypeName("fabric", itemsTFName), "test") +) + +func TestUnit_GatewaysDataSource(t *testing.T) { + virtualNetworkGateway := fakes.NewRandomVirtualNetworkGateway() + onPremisesGateway := fakes.NewRandomOnPremisesGateway() + onPremisesGatewayPersonalGateway := fakes.NewRandomOnPremisesGatewayPersonal() + + fakes.FakeServer.Upsert(fakes.NewRandomGateway()) + fakes.FakeServer.Upsert(virtualNetworkGateway) + fakes.FakeServer.Upsert(onPremisesGateway) + fakes.FakeServer.Upsert(onPremisesGatewayPersonalGateway) + fakes.FakeServer.Upsert(fakes.NewRandomGateway()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, nil, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - unexpected_attr + { + Config: at.CompileConfig( + testDataSourceItemsHeader, + map[string]any{ + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // read + { + Config: at.CompileConfig( + testDataSourceItemsHeader, + map[string]any{}, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(testDataSourceItemsFQN, "values.1.id"), + resource.TestCheckResourceAttrSet(testDataSourceItemsFQN, "values.2.id"), + resource.TestCheckResourceAttrSet(testDataSourceItemsFQN, "values.3.id"), + ), + }, + })) +} + +func TestAcc_GatewaysDataSource(t *testing.T) { + resource.ParallelTest(t, testhelp.NewTestAccCase(t, nil, nil, []resource.TestStep{ + // read + { + Config: at.CompileConfig( + testDataSourceItemsHeader, + map[string]any{}, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(testDataSourceItemsFQN, "values.0.id"), + ), + }, + }, + )) +} diff --git a/internal/services/gateway/fake_test.go b/internal/services/gateway/fake_test.go new file mode 100644 index 00000000..30705893 --- /dev/null +++ b/internal/services/gateway/fake_test.go @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "net/http" + + azfake "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake" + azto "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" +) + +func fakeGatewayRoleAssignments(exampleResp fabcore.GatewayRoleAssignments) func(gatewayID string, options *fabcore.GatewaysClientListGatewayRoleAssignmentsOptions) (resp azfake.PagerResponder[fabcore.GatewaysClientListGatewayRoleAssignmentsResponse]) { + return func(_ string, _ *fabcore.GatewaysClientListGatewayRoleAssignmentsOptions) (resp azfake.PagerResponder[fabcore.GatewaysClientListGatewayRoleAssignmentsResponse]) { + resp = azfake.PagerResponder[fabcore.GatewaysClientListGatewayRoleAssignmentsResponse]{} + resp.AddPage(http.StatusOK, fabcore.GatewaysClientListGatewayRoleAssignmentsResponse{GatewayRoleAssignments: exampleResp}, nil) + + return + } +} + +func NewRandomGatewayRoleAssignments() fabcore.GatewayRoleAssignments { + principal0ID := testhelp.RandomUUID() + principal1ID := testhelp.RandomUUID() + principal2ID := testhelp.RandomUUID() + + return fabcore.GatewayRoleAssignments{ + Value: []fabcore.GatewayRoleAssignment{ + { + ID: azto.Ptr(principal0ID), + Role: azto.Ptr(fabcore.GatewayRoleAdmin), + Principal: &fabcore.Principal{ + ID: azto.Ptr(principal0ID), + Type: azto.Ptr(fabcore.PrincipalTypeGroup), + DisplayName: azto.Ptr(testhelp.RandomName()), + GroupDetails: &fabcore.PrincipalGroupDetails{ + GroupType: azto.Ptr(fabcore.GroupTypeSecurityGroup), + }, + }, + }, + { + ID: azto.Ptr(principal1ID), + Role: azto.Ptr(fabcore.GatewayRoleConnectionCreator), + Principal: &fabcore.Principal{ + ID: azto.Ptr(principal1ID), + Type: azto.Ptr(fabcore.PrincipalTypeUser), + DisplayName: azto.Ptr(testhelp.RandomName()), + UserDetails: &fabcore.PrincipalUserDetails{ + UserPrincipalName: azto.Ptr(testhelp.RandomName()), + }, + }, + }, + { + ID: azto.Ptr(principal2ID), + Role: azto.Ptr(fabcore.GatewayRoleConnectionCreatorWithResharing), + Principal: &fabcore.Principal{ + ID: azto.Ptr(principal2ID), + Type: azto.Ptr(fabcore.PrincipalTypeServicePrincipal), + DisplayName: azto.Ptr(testhelp.RandomName()), + ServicePrincipalDetails: &fabcore.PrincipalServicePrincipalDetails{ + AADAppID: azto.Ptr(testhelp.RandomUUID()), + }, + }, + }, + }, + } +} diff --git a/internal/services/gateway/models.go b/internal/services/gateway/models.go new file mode 100644 index 00000000..2c405361 --- /dev/null +++ b/internal/services/gateway/models.go @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" +) + +type virtualNetworkAzureResourceModel struct { + ResourceGroupName types.String `tfsdk:"resource_group_name"` + SubnetName types.String `tfsdk:"subnet_name"` + SubscriptionID customtypes.UUID `tfsdk:"subscription_id"` + VirtualNetworkName types.String `tfsdk:"virtual_network_name"` // Rename to just 'name' or 'display_name'? +} + +func (to *virtualNetworkAzureResourceModel) set(from fabcore.VirtualNetworkAzureResource) { + to.ResourceGroupName = types.StringPointerValue(from.ResourceGroupName) + to.SubnetName = types.StringPointerValue(from.SubnetName) + to.SubscriptionID = customtypes.NewUUIDPointerValue(from.SubscriptionID) + to.VirtualNetworkName = types.StringPointerValue(from.VirtualNetworkName) +} diff --git a/internal/services/gateway/models_data_gateway.go b/internal/services/gateway/models_data_gateway.go new file mode 100644 index 00000000..e5eb9b4f --- /dev/null +++ b/internal/services/gateway/models_data_gateway.go @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" +) + +type dataSourceGatewayModel struct { + baseDataSourceGatewayModel + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type baseDataSourceGatewayModel struct { + ID customtypes.UUID `tfsdk:"id"` + Type types.String `tfsdk:"type"` + + DisplayName types.String `tfsdk:"display_name"` // VirtualNetwork & OnPremises + CapacityID customtypes.UUID `tfsdk:"capacity_id"` // VirtualNetwork + InactivityMinutesBeforeSleep types.Int32 `tfsdk:"inactivity_minutes_before_sleep"` // VirtualNetwork + NumberOfMemberGateways types.Int32 `tfsdk:"number_of_member_gateways"` // VirtualNetwork & OnPremises + VirtualNetworkAzureResource supertypes.SingleNestedObjectValueOf[virtualNetworkAzureResourceModel] `tfsdk:"virtual_network_azure_resource"` // VirtualNetwork + AllowCloudConnectionRefresh types.Bool `tfsdk:"allow_cloud_connection_refresh"` // OnPremises + AllowCustomConnectors types.Bool `tfsdk:"allow_custom_connectors"` // OnPremises + LoadBalancingSetting types.String `tfsdk:"load_balancing_setting"` // OnPremises + PublicKey supertypes.SingleNestedObjectValueOf[publicKeyModel] `tfsdk:"public_key"` // OnPremises & OnPremisesPersonal + Version types.String `tfsdk:"version"` // OnPremises & OnPremisesPersonal +} + +func (to *baseDataSourceGatewayModel) set(ctx context.Context, from fabcore.GatewayClassification) diag.Diagnostics { + var diags diag.Diagnostics + + virtualNetworkAzureResource := supertypes.NewSingleNestedObjectValueOfNull[virtualNetworkAzureResourceModel](ctx) + publicKey := supertypes.NewSingleNestedObjectValueOfNull[publicKeyModel](ctx) + + gw := from.GetGateway() + to.ID = customtypes.NewUUIDPointerValue(gw.ID) + to.Type = types.StringPointerValue((*string)(gw.Type)) + + switch entity := from.(type) { + case *fabcore.VirtualNetworkGateway: + to.DisplayName = types.StringPointerValue(entity.DisplayName) + to.CapacityID = customtypes.NewUUIDPointerValue(entity.CapacityID) + to.InactivityMinutesBeforeSleep = types.Int32PointerValue(entity.InactivityMinutesBeforeSleep) + to.NumberOfMemberGateways = types.Int32PointerValue(entity.NumberOfMemberGateways) + + if entity.VirtualNetworkAzureResource != nil { + virtualNetworkAzureResourceModel := &virtualNetworkAzureResourceModel{} + virtualNetworkAzureResourceModel.set(*entity.VirtualNetworkAzureResource) + + if diags := virtualNetworkAzureResource.Set(ctx, virtualNetworkAzureResourceModel); diags.HasError() { + return diags + } + } + + to.VirtualNetworkAzureResource = virtualNetworkAzureResource + + to.PublicKey = publicKey + + case *fabcore.OnPremisesGateway: + to.DisplayName = types.StringPointerValue(entity.DisplayName) + to.NumberOfMemberGateways = types.Int32PointerValue(entity.NumberOfMemberGateways) + to.AllowCloudConnectionRefresh = types.BoolPointerValue(entity.AllowCloudConnectionRefresh) + to.AllowCustomConnectors = types.BoolPointerValue(entity.AllowCustomConnectors) + to.LoadBalancingSetting = types.StringPointerValue((*string)(entity.LoadBalancingSetting)) + to.Version = types.StringPointerValue(entity.Version) + + if entity.PublicKey != nil { + publicKeyModel := &publicKeyModel{} + publicKeyModel.set(*entity.PublicKey) + + if diags := publicKey.Set(ctx, publicKeyModel); diags.HasError() { + return diags + } + } + + to.PublicKey = publicKey + + to.VirtualNetworkAzureResource = virtualNetworkAzureResource + + case *fabcore.OnPremisesGatewayPersonal: + to.Version = types.StringPointerValue(entity.Version) + + if entity.PublicKey != nil { + publicKeyModel := &publicKeyModel{} + publicKeyModel.set(*entity.PublicKey) + + if diags := publicKey.Set(ctx, publicKeyModel); diags.HasError() { + return diags + } + } + + to.PublicKey = publicKey + + to.VirtualNetworkAzureResource = virtualNetworkAzureResource + + default: + diags.AddError("Unsupported Gateway type", fmt.Sprintf("The Gateway type '%s' is not supported.", (string)(*gw.Type))) + + return diags + } + + return nil +} + +type publicKeyModel struct { + Exponent types.String `tfsdk:"exponent"` + Modulus types.String `tfsdk:"modulus"` +} + +func (to *publicKeyModel) set(from fabcore.PublicKey) { + to.Exponent = types.StringPointerValue(from.Exponent) + to.Modulus = types.StringPointerValue(from.Modulus) +} diff --git a/internal/services/gateway/models_data_gateway_role_assignment.go b/internal/services/gateway/models_data_gateway_role_assignment.go new file mode 100644 index 00000000..b717030b --- /dev/null +++ b/internal/services/gateway/models_data_gateway_role_assignment.go @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" +) + +type dataSourceGatewayRoleAssignmentsModel struct { + GatewayID customtypes.UUID `tfsdk:"gateway_id"` + Values supertypes.ListNestedObjectValueOf[gatewayRoleAssignmentModel] `tfsdk:"values"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (to *dataSourceGatewayRoleAssignmentsModel) setValues(ctx context.Context, from []fabcore.GatewayRoleAssignment) diag.Diagnostics { + slice := make([]*gatewayRoleAssignmentModel, 0, len(from)) + + for _, entity := range from { + var entityModel gatewayRoleAssignmentModel + + if diags := entityModel.set(entity); diags.HasError() { + return diags + } + + slice = append(slice, &entityModel) + } + + return to.Values.Set(ctx, slice) +} + +type gatewayRoleAssignmentModel struct { + ID customtypes.UUID `tfsdk:"id"` + Role types.String `tfsdk:"role"` + Type types.String `tfsdk:"type"` +} + +func (to *gatewayRoleAssignmentModel) set(from fabcore.GatewayRoleAssignment) diag.Diagnostics { + to.ID = customtypes.NewUUIDPointerValue(from.ID) + to.Role = types.StringPointerValue((*string)(from.Role)) + to.Type = types.StringPointerValue((*string)(from.Principal.Type)) + + return nil +} diff --git a/internal/services/gateway/models_data_gateways.go b/internal/services/gateway/models_data_gateways.go new file mode 100644 index 00000000..02189034 --- /dev/null +++ b/internal/services/gateway/models_data_gateways.go @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" +) + +type dataSourceGatewaysModel struct { + Values supertypes.ListNestedObjectValueOf[baseDataSourceGatewayModel] `tfsdk:"values"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (to *dataSourceGatewaysModel) setValues(ctx context.Context, from []fabcore.GatewayClassification) diag.Diagnostics { + slice := make([]*baseDataSourceGatewayModel, 0, len(from)) + + for _, entity := range from { + var entityModel baseDataSourceGatewayModel + if diags := entityModel.set(ctx, entity); diags.HasError() { + return diags + } + + slice = append(slice, &entityModel) + } + + return to.Values.Set(ctx, slice) +} diff --git a/internal/services/gateway/models_resource_gateway.go b/internal/services/gateway/models_resource_gateway.go new file mode 100644 index 00000000..9826ec2c --- /dev/null +++ b/internal/services/gateway/models_resource_gateway.go @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" +) + +type resourceGatewayModel struct { + baseResourceGatewayModel + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type baseResourceGatewayModel struct { + ID customtypes.UUID `tfsdk:"id"` + Type types.String `tfsdk:"type"` + + DisplayName types.String `tfsdk:"display_name"` // VirtualNetwork + CapacityID customtypes.UUID `tfsdk:"capacity_id"` // VirtualNetwork + InactivityMinutesBeforeSleep types.Int32 `tfsdk:"inactivity_minutes_before_sleep"` // VirtualNetwork + NumberOfMemberGateways types.Int32 `tfsdk:"number_of_member_gateways"` // VirtualNetwork + VirtualNetworkAzureResource supertypes.SingleNestedObjectValueOf[virtualNetworkAzureResourceModel] `tfsdk:"virtual_network_azure_resource"` // VirtualNetwork +} + +func (to *baseResourceGatewayModel) set(ctx context.Context, from fabcore.GatewayClassification) diag.Diagnostics { + var diags diag.Diagnostics + + switch gateway := from.(type) { + case *fabcore.VirtualNetworkGateway: + to.ID = customtypes.NewUUIDPointerValue(gateway.ID) + to.Type = types.StringPointerValue((*string)(gateway.Type)) + to.DisplayName = types.StringPointerValue(gateway.DisplayName) + to.CapacityID = customtypes.NewUUIDPointerValue(gateway.CapacityID) + to.InactivityMinutesBeforeSleep = types.Int32PointerValue(gateway.InactivityMinutesBeforeSleep) + to.NumberOfMemberGateways = types.Int32PointerValue(gateway.NumberOfMemberGateways) + + virtualNetworkAzureResource := supertypes.NewSingleNestedObjectValueOfNull[virtualNetworkAzureResourceModel](ctx) + + if gateway.VirtualNetworkAzureResource != nil { + virtualNetworkAzureResourceModel := &virtualNetworkAzureResourceModel{} + virtualNetworkAzureResourceModel.set(*gateway.VirtualNetworkAzureResource) + + if diags := virtualNetworkAzureResource.Set(ctx, virtualNetworkAzureResourceModel); diags.HasError() { + return diags + } + } + + to.VirtualNetworkAzureResource = virtualNetworkAzureResource + default: + diags.AddError("Unsupported Gateway type", fmt.Sprintf("The Gateway type '%T' is not supported.", gateway)) + + return diags + } + + return nil +} + +type requestCreateGateway struct { + fabcore.CreateGatewayRequestClassification +} + +func (to *requestCreateGateway) set(ctx context.Context, from resourceGatewayModel) diag.Diagnostics { + var diags diag.Diagnostics + + gatewayType := (fabcore.GatewayType)(from.Type.ValueString()) + + switch gatewayType { + case fabcore.GatewayTypeVirtualNetwork: + virtualNetworkAzureResource, diags := from.VirtualNetworkAzureResource.Get(ctx) + if diags.HasError() { + return diags + } + + to.CreateGatewayRequestClassification = &fabcore.CreateVirtualNetworkGatewayRequest{ + Type: &gatewayType, + CapacityID: from.CapacityID.ValueStringPointer(), + DisplayName: from.DisplayName.ValueStringPointer(), + InactivityMinutesBeforeSleep: from.InactivityMinutesBeforeSleep.ValueInt32Pointer(), + NumberOfMemberGateways: from.NumberOfMemberGateways.ValueInt32Pointer(), + VirtualNetworkAzureResource: &fabcore.VirtualNetworkAzureResource{ + SubscriptionID: virtualNetworkAzureResource.SubscriptionID.ValueStringPointer(), + ResourceGroupName: virtualNetworkAzureResource.ResourceGroupName.ValueStringPointer(), + VirtualNetworkName: virtualNetworkAzureResource.VirtualNetworkName.ValueStringPointer(), + SubnetName: virtualNetworkAzureResource.SubnetName.ValueStringPointer(), + }, + } + default: + diags.AddError("Unsupported Gateway type", fmt.Sprintf("The Gateway type '%T' is not supported.", gatewayType)) + + return diags + } + + return nil +} + +type requestUpdateGateway struct { + fabcore.UpdateGatewayRequestClassification +} + +func (to *requestUpdateGateway) set(from resourceGatewayModel) diag.Diagnostics { + var diags diag.Diagnostics + + gatewayType := (fabcore.GatewayType)(from.Type.ValueString()) + + switch gatewayType { + case fabcore.GatewayTypeVirtualNetwork: + to.UpdateGatewayRequestClassification = &fabcore.UpdateVirtualNetworkGatewayRequest{ + Type: &gatewayType, + DisplayName: from.DisplayName.ValueStringPointer(), + CapacityID: from.CapacityID.ValueStringPointer(), + InactivityMinutesBeforeSleep: from.InactivityMinutesBeforeSleep.ValueInt32Pointer(), + NumberOfMemberGateways: from.NumberOfMemberGateways.ValueInt32Pointer(), + } + default: + diags.AddError("Unsupported Gateway type", fmt.Sprintf("The Gateway type '%T' is not supported.", gatewayType)) + + return diags + } + + return nil +} diff --git a/internal/services/gateway/models_resource_gateway_role_assignment.go b/internal/services/gateway/models_resource_gateway_role_assignment.go new file mode 100644 index 00000000..3f84aa22 --- /dev/null +++ b/internal/services/gateway/models_resource_gateway_role_assignment.go @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" +) + +type resourceGatewayRoleAssignmentModel struct { + ID customtypes.UUID `tfsdk:"id"` + PrincipalID customtypes.UUID `tfsdk:"principal_id"` + PrincipalType types.String `tfsdk:"principal_type"` + Role types.String `tfsdk:"role"` + GatewayID customtypes.UUID `tfsdk:"gateway_id"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (to *resourceGatewayRoleAssignmentModel) set(from fabcore.GatewayRoleAssignment) { + to.ID = customtypes.NewUUIDPointerValue(from.ID) + to.PrincipalID = customtypes.NewUUIDPointerValue(from.Principal.ID) + to.PrincipalType = types.StringPointerValue((*string)(from.Principal.Type)) + to.Role = types.StringPointerValue((*string)(from.Role)) +} + +type requestCreateGatewayRoleAssignment struct { + fabcore.AddGatewayRoleAssignmentRequest +} + +func (to *requestCreateGatewayRoleAssignment) set(from resourceGatewayRoleAssignmentModel) { + to.Principal = &fabcore.Principal{ID: from.PrincipalID.ValueStringPointer(), Type: (*fabcore.PrincipalType)(from.PrincipalType.ValueStringPointer())} + to.Role = (*fabcore.GatewayRole)(from.Role.ValueStringPointer()) +} + +type requestUpdateGatewayRoleAssignment struct { + fabcore.UpdateGatewayRoleAssignmentRequest +} + +func (to *requestUpdateGatewayRoleAssignment) set(from resourceGatewayRoleAssignmentModel) { + to.Role = (*fabcore.GatewayRole)(from.Role.ValueStringPointer()) +} diff --git a/internal/services/gateway/resource_gateway.go b/internal/services/gateway/resource_gateway.go new file mode 100644 index 00000000..50b49828 --- /dev/null +++ b/internal/services/gateway/resource_gateway.go @@ -0,0 +1,392 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/int32validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + supertypes "github.com/orange-cloudavenue/terraform-plugin-framework-supertypes" + superint32validator "github.com/orange-cloudavenue/terraform-plugin-framework-validators/int32validator" + superobjectvalidator "github.com/orange-cloudavenue/terraform-plugin-framework-validators/objectvalidator" + superstringvalidator "github.com/orange-cloudavenue/terraform-plugin-framework-validators/stringvalidator" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/pkg/utils" + pconfig "github.com/microsoft/terraform-provider-fabric/internal/provider/config" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.ResourceWithConfigure = (*resourceGateway)(nil) + // _ resource.ResourceWithImportState = (*resourceGateway)(nil). +) + +type resourceGateway struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewResourceGateway() resource.Resource { + return &resourceGateway{} +} + +func (r *resourceGateway) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + ItemTFName +} + +func (r *resourceGateway) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "This resource manages a Fabric " + ItemName + ".\n\n" + + "See [" + ItemName + "s](" + ItemDocsURL + ") for more information.\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " display name.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(200), + superstringvalidator.RequireIfAttributeIsOneOf(path.MatchRoot("type"), + []attr.Value{ + types.StringValue(string(fabcore.GatewayTypeVirtualNetwork)), + }), + }, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " type. Accepted values: " + utils.ConvertStringSlicesToString(fabcore.PossibleGatewayTypeValues(), true, true), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(string(fabcore.GatewayTypeVirtualNetwork)), + }, + }, + "capacity_id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " capacity ID.", + Optional: true, + Computed: true, + CustomType: customtypes.UUIDType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + superstringvalidator.RequireIfAttributeIsOneOf(path.MatchRoot("type"), + []attr.Value{ + types.StringValue(string(fabcore.GatewayTypeVirtualNetwork)), + }), + }, + }, + "inactivity_minutes_before_sleep": schema.Int32Attribute{ + MarkdownDescription: "The " + ItemName + " inactivity minutes before sleep.", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.OneOf(PossibleInactivityMinutesBeforeSleepValues...), + superint32validator.RequireIfAttributeIsOneOf(path.MatchRoot("type"), + []attr.Value{ + types.StringValue(string(fabcore.GatewayTypeVirtualNetwork)), + }), + }, + }, + "number_of_member_gateways": schema.Int32Attribute{ + MarkdownDescription: "The " + ItemName + " number of member gateways.", + Optional: true, + Computed: true, + Validators: []validator.Int32{ + int32validator.Between(MinNumberOfMemberGatewaysValues, MaxNumberOfMemberGatewaysValues), + superint32validator.RequireIfAttributeIsOneOf(path.MatchRoot("type"), + []attr.Value{ + types.StringValue(string(fabcore.GatewayTypeVirtualNetwork)), + }), + }, + }, + "virtual_network_azure_resource": schema.SingleNestedAttribute{ + MarkdownDescription: "The " + ItemName + " virtual network Azure resource.", + Optional: true, + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[virtualNetworkAzureResourceModel](ctx), + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, + Validators: []validator.Object{ + superobjectvalidator.RequireIfAttributeIsOneOf(path.MatchRoot("type"), + []attr.Value{ + types.StringValue(string(fabcore.GatewayTypeVirtualNetwork)), + }), + }, + Attributes: map[string]schema.Attribute{ + "virtual_network_name": schema.StringAttribute{ + MarkdownDescription: "The virtual network name.", + Required: true, + }, + "subnet_name": schema.StringAttribute{ + MarkdownDescription: "The subnet name.", + Required: true, + }, + "resource_group_name": schema.StringAttribute{ + MarkdownDescription: "The resource group name.", + Required: true, + }, + "subscription_id": schema.StringAttribute{ + MarkdownDescription: "The subscription ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + }, + }, + }, + "timeouts": timeouts.AttributesAll(ctx), + }, + } +} + +func (r *resourceGateway) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + pConfigData, ok := req.ProviderData.(*pconfig.ProviderData) + if !ok { + resp.Diagnostics.AddError( + common.ErrorResourceConfigType, + fmt.Sprintf(common.ErrorFabricClientType, req.ProviderData), + ) + + return + } + + r.pConfigData = pConfigData + r.client = fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient() +} + +func (r *resourceGateway) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Debug(ctx, "CREATE", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "CREATE", map[string]any{ + "config": req.Config, + "plan": req.Plan, + }) + + var plan, state resourceGatewayModel + + if resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := plan.Timeouts.Create(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + state.Timeouts = plan.Timeouts + + var reqCreate requestCreateGateway + + reqCreate.set(ctx, plan) + + respCreate, err := r.client.CreateGateway(ctx, reqCreate.CreateGatewayRequestClassification, nil) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationCreate, nil)...); resp.Diagnostics.HasError() { + return + } + + if resp.Diagnostics.Append(state.set(ctx, respCreate.GatewayClassification)...); resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + tflog.Debug(ctx, "CREATE", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGateway) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Debug(ctx, "READ", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "READ", map[string]any{ + "state": req.State, + }) + + var state resourceGatewayModel + + if resp.Diagnostics.Append(req.State.Get(ctx, &state)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := state.Timeouts.Read(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + diags = r.get(ctx, &state) + if utils.IsErrNotFound(state.ID.ValueString(), &diags, fabcore.ErrCommon.EntityNotFound) { + resp.State.RemoveResource(ctx) + + resp.Diagnostics.Append(diags...) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGateway) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Debug(ctx, "UPDATE", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "UPDATE", map[string]any{ + "config": req.Config, + "plan": req.Plan, + "state": req.State, + }) + + var plan resourceGatewayModel + + if resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := plan.Timeouts.Update(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var reqUpdate requestUpdateGateway + + reqUpdate.set(plan) + + respUpdate, err := r.client.UpdateGateway(ctx, plan.ID.ValueString(), reqUpdate.UpdateGatewayRequestClassification, nil) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationUpdate, nil)...); resp.Diagnostics.HasError() { + return + } + + if resp.Diagnostics.Append(plan.set(ctx, respUpdate.GatewayClassification)...); resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + + tflog.Debug(ctx, "UPDATE", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGateway) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + tflog.Debug(ctx, "DELETE", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "DELETE", map[string]any{ + "state": req.State, + }) + + var state resourceGatewayModel + + if resp.Diagnostics.Append(req.State.Get(ctx, &state)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := state.Timeouts.Delete(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + _, err := r.client.DeleteGateway(ctx, state.ID.ValueString(), nil) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationDelete, nil)...); resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "DELETE", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGateway) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, "IMPORT", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "IMPORT", map[string]any{ + "id": req.ID, + }) + + _, diags := customtypes.NewUUIDValueMust(req.ID) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + + tflog.Debug(ctx, "IMPORT", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGateway) get(ctx context.Context, model *resourceGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, "getting "+ItemName) + + respGet, err := r.client.GetGateway(ctx, model.ID.ValueString(), nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationRead, nil); diags.HasError() { + return diags + } + + return model.set(ctx, respGet.GatewayClassification) +} diff --git a/internal/services/gateway/resource_gateway_role_assignment.go b/internal/services/gateway/resource_gateway_role_assignment.go new file mode 100644 index 00000000..83a5282f --- /dev/null +++ b/internal/services/gateway/resource_gateway_role_assignment.go @@ -0,0 +1,351 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + "strings" + + "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" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-log/tflog" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/pkg/utils" + pconfig "github.com/microsoft/terraform-provider-fabric/internal/provider/config" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var ( + _ resource.ResourceWithConfigure = (*resourceGatewayRoleAssignment)(nil) + _ resource.ResourceWithImportState = (*resourceGatewayRoleAssignment)(nil) +) + +type resourceGatewayRoleAssignment struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewResourceGatewayRoleAssignment() resource.Resource { + return &resourceGatewayRoleAssignment{} +} + +func (r *resourceGatewayRoleAssignment) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + GatewayRoleAssignmentTFName +} + +func (r *resourceGatewayRoleAssignment) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manage a " + GatewayRoleAssignmentName + ".\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The " + GatewayRoleAssignmentName + " ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "principal_id": schema.StringAttribute{ + MarkdownDescription: "The Principal ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "principal_type": schema.StringAttribute{ + MarkdownDescription: "The type of the principal. Accepted values: " + utils.ConvertStringSlicesToString(fabcore.PossiblePrincipalTypeValues(), true, true) + ".", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(utils.ConvertEnumsToStringSlices(fabcore.PossiblePrincipalTypeValues(), false)...), + }, + }, + "role": schema.StringAttribute{ + MarkdownDescription: "The Gateway Role of the principal. Accepted values: " + utils.ConvertStringSlicesToString(fabcore.PossibleGatewayRoleValues(), true, true) + ".", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(utils.ConvertEnumsToStringSlices(fabcore.PossibleGatewayRoleValues(), false)...), + }, + }, + "gateway_id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "timeouts": timeouts.AttributesAll(ctx), + }, + } +} + +func (r *resourceGatewayRoleAssignment) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + pConfigData, ok := req.ProviderData.(*pconfig.ProviderData) + + if !ok { + resp.Diagnostics.AddError( + common.ErrorResourceConfigType, + fmt.Sprintf(common.ErrorFabricClientType, req.ProviderData), + ) + + return + } + + r.pConfigData = pConfigData + r.client = fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient() +} + +func (r *resourceGatewayRoleAssignment) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Debug(ctx, "CREATE", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "CREATE", map[string]any{ + "config": req.Config, + "plan": req.Plan, + }) + + var plan resourceGatewayRoleAssignmentModel + + if resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := plan.Timeouts.Create(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var reqCreate requestCreateGatewayRoleAssignment + + reqCreate.set(plan) + + respCreate, err := r.client.AddGatewayRoleAssignment(ctx, plan.GatewayID.ValueString(), reqCreate.AddGatewayRoleAssignmentRequest, nil) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationCreate, nil)...); resp.Diagnostics.HasError() { + return + } + + plan.set(respCreate.GatewayRoleAssignment) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + + tflog.Debug(ctx, "CREATE", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGatewayRoleAssignment) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Debug(ctx, "READ", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "READ", map[string]any{ + "state": req.State, + }) + + var state resourceGatewayRoleAssignmentModel + + if resp.Diagnostics.Append(req.State.Get(ctx, &state)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := state.Timeouts.Read(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := r.get(ctx, &state) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationRead, fabcore.ErrCommon.EntityNotFound); resp.Diagnostics.HasError() { + if utils.IsErrNotFound(state.ID.ValueString(), &diags, fabcore.ErrCommon.EntityNotFound) { + resp.State.RemoveResource(ctx) + } + + resp.Diagnostics.Append(diags...) + + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGatewayRoleAssignment) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Debug(ctx, "UPDATE", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "UPDATE", map[string]any{ + "config": req.Config, + "plan": req.Plan, + "state": req.State, + }) + + var plan resourceGatewayRoleAssignmentModel + + if resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := plan.Timeouts.Update(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + var reqUpdate requestUpdateGatewayRoleAssignment + + reqUpdate.set(plan) + + respUpdate, err := r.client.UpdateGatewayRoleAssignment(ctx, plan.GatewayID.ValueString(), plan.ID.ValueString(), reqUpdate.UpdateGatewayRoleAssignmentRequest, nil) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationUpdate, nil)...); resp.Diagnostics.HasError() { + return + } + + plan.set(respUpdate.GatewayRoleAssignment) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + + tflog.Debug(ctx, "UPDATE", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGatewayRoleAssignment) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + tflog.Debug(ctx, "DELETE", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "DELETE", map[string]any{ + "state": req.State, + }) + + var state resourceGatewayRoleAssignmentModel + + if resp.Diagnostics.Append(req.State.Get(ctx, &state)...); resp.Diagnostics.HasError() { + return + } + + timeout, diags := state.Timeouts.Delete(ctx, r.pConfigData.Timeout) + if resp.Diagnostics.Append(diags...); resp.Diagnostics.HasError() { + return + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + _, err := r.client.DeleteGatewayRoleAssignment(ctx, state.GatewayID.ValueString(), state.ID.ValueString(), nil) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationDelete, nil)...); resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "DELETE", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGatewayRoleAssignment) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, "IMPORT", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "IMPORT", map[string]any{ + "id": req.ID, + }) + + gatewayID, gatewayRoleAssignmentID, found := strings.Cut(req.ID, "/") + if !found { + resp.Diagnostics.AddError( + common.ErrorImportIdentifierHeader, + fmt.Sprintf(common.ErrorImportIdentifierDetails, "GatewayID/GatewayRoleAssignmentID"), + ) + + return + } + + uuidGatewayID, diags := customtypes.NewUUIDValueMust(gatewayID) + resp.Diagnostics.Append(diags...) + + uuidGatewayRoleAssignmentID, diags := customtypes.NewUUIDValueMust(gatewayRoleAssignmentID) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + var timeout timeouts.Value + if resp.Diagnostics.Append(resp.State.GetAttribute(ctx, path.Root("timeouts"), &timeout)...); resp.Diagnostics.HasError() { + return + } + + state := resourceGatewayRoleAssignmentModel{ + ID: uuidGatewayRoleAssignmentID, + GatewayID: uuidGatewayID, + Timeouts: timeout, + } + + err := r.get(ctx, &state) + if resp.Diagnostics.Append(utils.GetDiagsFromError(ctx, err, utils.OperationImport, nil)...); resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + tflog.Debug(ctx, "IMPORT", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceGatewayRoleAssignment) get(ctx context.Context, model *resourceGatewayRoleAssignmentModel) error { + tflog.Trace(ctx, "getting Gateway Role Assignment") + + respGetInfo, err := r.client.GetGatewayRoleAssignment(ctx, model.GatewayID.ValueString(), model.ID.ValueString(), nil) + if err != nil { + return err + } + + model.set(respGetInfo.GatewayRoleAssignment) + + return nil +} diff --git a/internal/services/gateway/resource_gateway_role_assignment_test.go b/internal/services/gateway/resource_gateway_role_assignment_test.go new file mode 100644 index 00000000..ed895a28 --- /dev/null +++ b/internal/services/gateway/resource_gateway_role_assignment_test.go @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "fmt" + "regexp" + "testing" + + at "github.com/dcarbone/terraform-plugin-framework-utils/v3/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp/fakes" +) + +var ( + testResourceGatewayRoleAssignment = testhelp.ResourceFQN("fabric", gatewayRoleAssignmentTFName, "test") + testResourceGatewayRoleAssignmentHeader = at.ResourceHeader(testhelp.TypeName("fabric", gatewayRoleAssignmentTFName), "test") +) + +func TestUnit_GatewayRoleAssignmentResource_Attributes(t *testing.T) { + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, &testResourceGatewayRoleAssignment, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - no required attributes - gateway_id + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "principal_id": "00000000-0000-0000-0000-000000000000", + "principal_type": "User", + "role": "ConnectionCreatorWithResharing", + }, + ), + ExpectError: regexp.MustCompile(`The argument "gateway_id" is required, but no definition was found.`), + }, + // error - no required attributes - principal_id + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": "00000000-0000-0000-0000-000000000000", + "principal_type": "User", + "role": "ConnectionCreatorWithResharing", + }, + ), + ExpectError: regexp.MustCompile(`The argument "principal_id" is required, but no definition was found.`), + }, + // error - no required attributes - principal_type + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": "00000000-0000-0000-0000-000000000000", + "principal_id": "00000000-0000-0000-0000-000000000000", + "role": "ConnectionCreatorWithResharing", + }, + ), + ExpectError: regexp.MustCompile(`The argument "principal_type" is required, but no definition was found.`), + }, + // error - no required attributes - role + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": "00000000-0000-0000-0000-000000000000", + "principal_id": "00000000-0000-0000-0000-000000000000", + "principal_type": "User", + }, + ), + ExpectError: regexp.MustCompile(`The argument "role" is required, but no definition was found.`), + }, + // error - invalid UUID - gateway_id + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": "invalid uuid", + "principal_id": "00000000-0000-0000-0000-000000000000", + "principal_type": "User", + "role": "ConnectionCreatorWithResharing", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // error - invalid UUID - principal_id + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": "00000000-0000-0000-0000-000000000000", + "principal_id": "invalid uuid", + "principal_type": "User", + "role": "ConnectionCreatorWithResharing", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + })) +} + +func TestUnit_GatewayRoleAssignmentResource_ImportState(t *testing.T) { + testCase := at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{}, + ) + + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, &testResourceGatewayRoleAssignment, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + { + ResourceName: testResourceGatewayRoleAssignment, + Config: testCase, + ImportStateId: "not-valid", + ImportState: true, + ExpectError: regexp.MustCompile("GatewayID/GatewayRoleAssignmentID"), + }, + { + ResourceName: testResourceGatewayRoleAssignment, + Config: testCase, + ImportStateId: "test/id", + ImportState: true, + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + { + ResourceName: testResourceGatewayRoleAssignment, + Config: testCase, + ImportStateId: fmt.Sprintf("%s/%s", "test", "00000000-0000-0000-0000-000000000000"), + ImportState: true, + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + { + ResourceName: testResourceGatewayRoleAssignment, + Config: testCase, + ImportStateId: fmt.Sprintf("%s/%s", "00000000-0000-0000-0000-000000000000", "test"), + ImportState: true, + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + })) +} + +func TestAcc_GatewayRoleAssignmentResource_CRUD(t *testing.T) { + if testhelp.ShouldSkipTest(t) { + t.Skip("No SPN support") + } + + gatewayType := string(fabcore.GatewayTypeVirtualNetwork) + gatewayCreateDisplayName := testhelp.RandomName() + gatewayCreateInactivityMinutesBeforeSleep := int(testhelp.RandomElement(gateway.PossibleInactivityMinutesBeforeSleepValues)) + gatewayCreateNumberOfMemberGateways := int(testhelp.RandomInt(gateway.MinNumberOfMemberGatewaysValues, gateway.MaxNumberOfMemberGatewaysValues)) + + capacity := testhelp.WellKnown()["Capacity"].(map[string]any) + capacityID := capacity["id"].(string) + + virtualNetworkAzureResource := testhelp.WellKnown()["VirtualNetwork01"].(map[string]any) + virtualNetworkName := virtualNetworkAzureResource["name"].(string) + resourceGroupName := virtualNetworkAzureResource["resourceGroupName"].(string) + subnetName := virtualNetworkAzureResource["subnetName"].(string) + subscriptionID := virtualNetworkAzureResource["subscriptionId"].(string) + + gatewayResourceHCL := at.CompileConfig( + at.ResourceHeader(testhelp.TypeName("fabric", itemTFName), "test"), + map[string]any{ + "type": gatewayType, + "display_name": gatewayCreateDisplayName, + "inactivity_minutes_before_sleep": gatewayCreateInactivityMinutesBeforeSleep, + "number_of_member_gateways": gatewayCreateNumberOfMemberGateways, + "virtual_network_azure_resource": map[string]any{ + "virtual_network_name": virtualNetworkName, + "resource_group_name": resourceGroupName, + "subnet_name": subnetName, + "subscription_id": subscriptionID, + }, + "capacity_id": capacityID, + }, + ) + gatewayResourceFQN := testhelp.ResourceFQN("fabric", itemTFName, "test") + + entity := testhelp.WellKnown()["Principal"].(map[string]any) + entityID := entity["id"].(string) + entityType := entity["type"].(string) + + resource.Test(t, testhelp.NewTestAccCase(t, &testResourceGatewayRoleAssignment, nil, []resource.TestStep{ + // Create and Read + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.JoinConfigs( + gatewayResourceHCL, + at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": testhelp.RefByFQN(gatewayResourceFQN, "id"), + "principal_id": entityID, + "principal_type": entityType, + "role": "ConnectionCreatorWithResharing", + }, + ), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_id", entityID), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_type", entityType), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "role", "ConnectionCreatorWithResharing"), + ), + }, + // Update and Read + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.JoinConfigs( + gatewayResourceHCL, + at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": testhelp.RefByFQN(gatewayResourceFQN, "id"), + "principal_id": entityID, + "principal_type": entityType, + "role": "ConnectionCreator", + }, + ), + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_id", entityID), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_type", entityType), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "role", "ConnectionCreator"), + ), + }, + })) +} diff --git a/internal/services/gateway/resource_gateway_test.go b/internal/services/gateway/resource_gateway_test.go new file mode 100644 index 00000000..32a13921 --- /dev/null +++ b/internal/services/gateway/resource_gateway_test.go @@ -0,0 +1,549 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "testing" + + at "github.com/dcarbone/terraform-plugin-framework-utils/v3/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + + "github.com/microsoft/terraform-provider-fabric/internal/common" + "github.com/microsoft/terraform-provider-fabric/internal/framework/customtypes" + "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp/fakes" +) + +var ( + testResourceItemFQN = testhelp.ResourceFQN("fabric", itemTFName, "test") + testResourceItemHeader = at.ResourceHeader(testhelp.TypeName("fabric", itemTFName), "test") +) + +func TestUnit_GatewayResource_Attributes(t *testing.T) { + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, &testResourceItemFQN, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - missing attributes + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{}, + ), + ExpectError: regexp.MustCompile(`Missing required argument`), + }, + // error - unexpected attribute + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // error - missing required attributes - type + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{}, + ), + ExpectError: regexp.MustCompile(`The argument "type" is required, but no definition was found.`), + }, + // error - missing required attributes - display_name + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(`Invalid configuration for attribute display_name`), + }, + // error - missing required attributes - inactivity_minutes_before_sleep + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(`Invalid configuration for attribute inactivity_minutes_before_sleep`), + }, + // error - missing required attributes - number_of_member_gateways + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(`Invalid configuration for attribute number_of_member_gateways`), + }, + // error - missing required attributes - virtual_network_azure_resource + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(`Invalid configuration for attribute virtual_network_azure_resource`), + }, + // error - missing required attributes - capacity_id + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + }, + ), + ExpectError: regexp.MustCompile(`Invalid configuration for attribute capacity_id`), + }, + // error - invalid attribute value - inactivity_minutes_before_sleep + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]) - 1, + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(`Invalid Attribute Value`), + }, + // error - invalid attribute value - number_of_member_gateways + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues) - 1, + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(fmt.Sprintf(`Attribute number_of_member_gateways value must be between %d and %d`, gateway.MinNumberOfMemberGatewaysValues, gateway.MaxNumberOfMemberGatewaysValues)), + }, + // error - invalid uuid - capacity_id + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": "test", + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "invalid uuid", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // error - unsupported gateway type - OnPremises + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeOnPremises), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorAttValueMatch), + }, + // error - unsupported gateway type - OnPremisesPersonal + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeOnPremisesPersonal), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorAttValueMatch), + }, + })) +} + +func TestUnit_GatewayResource_ImportState(t *testing.T) { + entity := fakes.NewRandomVirtualNetworkGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + + testCase := at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": (string)(*entity.Type), + "display_name": *entity.DisplayName, + "inactivity_minutes_before_sleep": (int)(*entity.InactivityMinutesBeforeSleep), + "number_of_member_gateways": (int)(*entity.NumberOfMemberGateways), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": *entity.VirtualNetworkAzureResource.ResourceGroupName, + "virtual_network_name": *entity.VirtualNetworkAzureResource.VirtualNetworkName, + "subnet_name": *entity.VirtualNetworkAzureResource.SubnetName, + "subscription_id": *entity.VirtualNetworkAzureResource.SubscriptionID, + }, + "capacity_id": *entity.CapacityID, + }, + ) + + resource.Test(t, testhelp.NewTestUnitCase(t, &testResourceItemFQN, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + { + ResourceName: testResourceItemFQN, + Config: testCase, + ImportStateId: "not-valid", + ImportState: true, + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // Import state testing + { + ResourceName: testResourceItemFQN, + Config: testCase, + ImportStateId: *entity.ID, + ImportState: true, + ImportStatePersist: true, + ImportStateCheck: func(is []*terraform.InstanceState) error { + if len(is) != 1 { + return errors.New("expected one instance state") + } + + if is[0].ID != *entity.ID { + return errors.New(testResourceItemFQN + ": unexpected ID") + } + + return nil + }, + }, + })) +} + +func TestUnit_GatewayResource_CRUD(t *testing.T) { + entityExist := fakes.NewRandomVirtualNetworkGateway() + entityBefore := fakes.NewRandomVirtualNetworkGateway() + entityAfter := fakes.NewRandomVirtualNetworkGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + fakes.FakeServer.Upsert(entityExist) + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + + resource.Test(t, testhelp.NewTestUnitCase(t, &testResourceItemFQN, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - create - existing entity + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": string(fabcore.GatewayTypeVirtualNetwork), + "display_name": *entityExist.DisplayName, + "inactivity_minutes_before_sleep": (int)(gateway.PossibleInactivityMinutesBeforeSleepValues[0]), + "number_of_member_gateways": (int)(gateway.MinNumberOfMemberGatewaysValues), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": "test", + "virtual_network_name": "test", + "subnet_name": "test", + "subscription_id": "00000000-0000-0000-0000-000000000000", + }, + "capacity_id": "00000000-0000-0000-0000-000000000000", + }, + ), + ExpectError: regexp.MustCompile(common.ErrorCreateHeader), + }, + // Create and Read + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": (string)(*entityBefore.Type), + "display_name": *entityBefore.DisplayName, + "inactivity_minutes_before_sleep": (int)(*entityBefore.InactivityMinutesBeforeSleep), + "number_of_member_gateways": (int)(*entityBefore.NumberOfMemberGateways), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": *entityBefore.VirtualNetworkAzureResource.ResourceGroupName, + "virtual_network_name": *entityBefore.VirtualNetworkAzureResource.VirtualNetworkName, + "subnet_name": *entityBefore.VirtualNetworkAzureResource.SubnetName, + "subscription_id": *entityBefore.VirtualNetworkAzureResource.SubscriptionID, + }, + "capacity_id": *entityBefore.CapacityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "type", (*string)(entityBefore.Type)), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "display_name", entityBefore.DisplayName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "capacity_id", entityBefore.CapacityID), + resource.TestCheckResourceAttr(testResourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(int(*entityBefore.InactivityMinutesBeforeSleep))), + resource.TestCheckResourceAttr(testResourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*entityBefore.NumberOfMemberGateways))), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.resource_group_name", entityBefore.VirtualNetworkAzureResource.ResourceGroupName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.subnet_name", entityBefore.VirtualNetworkAzureResource.SubnetName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.subscription_id", entityBefore.VirtualNetworkAzureResource.SubscriptionID), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.virtual_network_name", entityBefore.VirtualNetworkAzureResource.VirtualNetworkName), + ), + }, + // Update and Read - no replacement + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": (string)(*entityBefore.Type), + "display_name": *entityAfter.DisplayName, + "inactivity_minutes_before_sleep": (int)(*entityAfter.InactivityMinutesBeforeSleep), + "number_of_member_gateways": (int)(*entityAfter.NumberOfMemberGateways), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": *entityBefore.VirtualNetworkAzureResource.ResourceGroupName, + "virtual_network_name": *entityBefore.VirtualNetworkAzureResource.VirtualNetworkName, + "subnet_name": *entityBefore.VirtualNetworkAzureResource.SubnetName, + "subscription_id": *entityBefore.VirtualNetworkAzureResource.SubscriptionID, + }, + "capacity_id": *entityAfter.CapacityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceItemFQN, "type", string(*entityBefore.Type)), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "display_name", entityAfter.DisplayName), + resource.TestCheckResourceAttr(testResourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(int(*entityAfter.InactivityMinutesBeforeSleep))), + resource.TestCheckResourceAttr(testResourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*entityAfter.NumberOfMemberGateways))), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "capacity_id", entityAfter.CapacityID), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.resource_group_name", entityBefore.VirtualNetworkAzureResource.ResourceGroupName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.subnet_name", entityBefore.VirtualNetworkAzureResource.SubnetName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.subscription_id", entityBefore.VirtualNetworkAzureResource.SubscriptionID), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.virtual_network_name", entityBefore.VirtualNetworkAzureResource.VirtualNetworkName), + ), + }, + // Update and Read - with replacement + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": (string)(*entityBefore.Type), + "display_name": *entityAfter.DisplayName, + "inactivity_minutes_before_sleep": (int)(*entityAfter.InactivityMinutesBeforeSleep), + "number_of_member_gateways": (int)(*entityAfter.NumberOfMemberGateways), + "virtual_network_azure_resource": map[string]any{ + "resource_group_name": *entityAfter.VirtualNetworkAzureResource.ResourceGroupName, + "virtual_network_name": *entityAfter.VirtualNetworkAzureResource.VirtualNetworkName, + "subnet_name": *entityAfter.VirtualNetworkAzureResource.SubnetName, + "subscription_id": *entityAfter.VirtualNetworkAzureResource.SubscriptionID, + }, + "capacity_id": *entityAfter.CapacityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceItemFQN, "type", string(*entityBefore.Type)), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "display_name", entityAfter.DisplayName), + resource.TestCheckResourceAttr(testResourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(int(*entityAfter.InactivityMinutesBeforeSleep))), + resource.TestCheckResourceAttr(testResourceItemFQN, "number_of_member_gateways", strconv.Itoa(int(*entityAfter.NumberOfMemberGateways))), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "capacity_id", entityAfter.CapacityID), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.resource_group_name", entityAfter.VirtualNetworkAzureResource.ResourceGroupName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.subnet_name", entityAfter.VirtualNetworkAzureResource.SubnetName), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.subscription_id", entityAfter.VirtualNetworkAzureResource.SubscriptionID), + resource.TestCheckResourceAttrPtr(testResourceItemFQN, "virtual_network_azure_resource.virtual_network_name", entityAfter.VirtualNetworkAzureResource.VirtualNetworkName), + ), + }, + // Delete testing automatically occurs in TestCase + })) +} + +func TestAcc_GatewayResource_CRUD(t *testing.T) { + entityType := string(fabcore.GatewayTypeVirtualNetwork) + entityCreateDisplayName := testhelp.RandomName() + entityCreateInactivityMinutesBeforeSleep := int(testhelp.RandomElement(gateway.PossibleInactivityMinutesBeforeSleepValues)) + entityCreateNumberOfMemberGateways := int(testhelp.RandomInt(gateway.MinNumberOfMemberGatewaysValues, gateway.MaxNumberOfMemberGatewaysValues)) + + capacity := testhelp.WellKnown()["Capacity"].(map[string]any) + capacityID := capacity["id"].(string) + + virtualNetworkAzureResource01 := testhelp.WellKnown()["VirtualNetwork01"].(map[string]any) + vNET01VirtualNetworkName := virtualNetworkAzureResource01["name"].(string) + vNET01ResourceGroupName := virtualNetworkAzureResource01["resourceGroupName"].(string) + vNET01SubnetName := virtualNetworkAzureResource01["subnetName"].(string) + vNET01SubscriptionID := virtualNetworkAzureResource01["subscriptionId"].(string) + + entityUpdateDisplayName := testhelp.RandomName() + entityUpdateInactivityMinutesBeforeSleep := int(testhelp.RandomElement(gateway.PossibleInactivityMinutesBeforeSleepValues)) + entityUpdateNumberOfMemberGateways := int(testhelp.RandomInt(gateway.MinNumberOfMemberGatewaysValues, gateway.MaxNumberOfMemberGatewaysValues)) + + virtualNetworkAzureResource02 := testhelp.WellKnown()["VirtualNetwork02"].(map[string]any) + vNET02VirtualNetworkName := virtualNetworkAzureResource02["name"].(string) + vNET02ResourceGroupName := virtualNetworkAzureResource02["resourceGroupName"].(string) + vNET02SubnetName := virtualNetworkAzureResource02["subnetName"].(string) + vNET02SubscriptionID := virtualNetworkAzureResource02["subscriptionId"].(string) + + resource.Test(t, testhelp.NewTestAccCase(t, &testResourceItemFQN, nil, []resource.TestStep{ + // Create and Read + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": entityType, + "display_name": entityCreateDisplayName, + "inactivity_minutes_before_sleep": entityCreateInactivityMinutesBeforeSleep, + "number_of_member_gateways": entityCreateNumberOfMemberGateways, + "virtual_network_azure_resource": map[string]any{ + "virtual_network_name": vNET01VirtualNetworkName, + "resource_group_name": vNET01ResourceGroupName, + "subnet_name": vNET01SubnetName, + "subscription_id": vNET01SubscriptionID, + }, + "capacity_id": capacityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceItemFQN, "type", entityType), + resource.TestCheckResourceAttr(testResourceItemFQN, "display_name", entityCreateDisplayName), + resource.TestCheckResourceAttr(testResourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(entityCreateInactivityMinutesBeforeSleep)), + resource.TestCheckResourceAttr(testResourceItemFQN, "number_of_member_gateways", strconv.Itoa(entityCreateNumberOfMemberGateways)), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.virtual_network_name", vNET01VirtualNetworkName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.resource_group_name", vNET01ResourceGroupName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.subnet_name", vNET01SubnetName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.subscription_id", vNET01SubscriptionID), + resource.TestCheckResourceAttr(testResourceItemFQN, "capacity_id", capacityID), + ), + }, + // Update and Read - no replacement + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": entityType, + "display_name": entityUpdateDisplayName, + "inactivity_minutes_before_sleep": entityUpdateInactivityMinutesBeforeSleep, + "number_of_member_gateways": entityUpdateNumberOfMemberGateways, + "virtual_network_azure_resource": map[string]any{ + "virtual_network_name": vNET01VirtualNetworkName, + "resource_group_name": vNET01ResourceGroupName, + "subnet_name": vNET01SubnetName, + "subscription_id": vNET01SubscriptionID, + }, + "capacity_id": capacityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceItemFQN, "type", entityType), + resource.TestCheckResourceAttr(testResourceItemFQN, "display_name", entityUpdateDisplayName), + resource.TestCheckResourceAttr(testResourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(entityUpdateInactivityMinutesBeforeSleep)), + resource.TestCheckResourceAttr(testResourceItemFQN, "number_of_member_gateways", strconv.Itoa(entityUpdateNumberOfMemberGateways)), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.virtual_network_name", vNET01VirtualNetworkName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.resource_group_name", vNET01ResourceGroupName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.subnet_name", vNET01SubnetName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.subscription_id", vNET01SubscriptionID), + resource.TestCheckResourceAttr(testResourceItemFQN, "capacity_id", capacityID), + ), + }, + // Update and Read - with replacement + { + ResourceName: testResourceItemFQN, + Config: at.CompileConfig( + testResourceItemHeader, + map[string]any{ + "type": entityType, + "display_name": entityUpdateDisplayName, + "inactivity_minutes_before_sleep": entityUpdateInactivityMinutesBeforeSleep, + "number_of_member_gateways": entityUpdateNumberOfMemberGateways, + "virtual_network_azure_resource": map[string]any{ + "virtual_network_name": vNET02VirtualNetworkName, + "resource_group_name": vNET02ResourceGroupName, + "subnet_name": vNET02SubnetName, + "subscription_id": vNET02SubscriptionID, + }, + "capacity_id": capacityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceItemFQN, "type", entityType), + resource.TestCheckResourceAttr(testResourceItemFQN, "display_name", entityUpdateDisplayName), + resource.TestCheckResourceAttr(testResourceItemFQN, "inactivity_minutes_before_sleep", strconv.Itoa(entityUpdateInactivityMinutesBeforeSleep)), + resource.TestCheckResourceAttr(testResourceItemFQN, "number_of_member_gateways", strconv.Itoa(entityUpdateNumberOfMemberGateways)), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.virtual_network_name", vNET02VirtualNetworkName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.resource_group_name", vNET02ResourceGroupName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.subnet_name", vNET02SubnetName), + resource.TestCheckResourceAttr(testResourceItemFQN, "virtual_network_azure_resource.subscription_id", vNET02SubscriptionID), + resource.TestCheckResourceAttr(testResourceItemFQN, "capacity_id", capacityID), + ), + }, + }, + )) +} diff --git a/internal/services/workspace/data_workspace_role_assignments.go b/internal/services/workspace/data_workspace_role_assignments.go index 643d89cf..c4429978 100644 --- a/internal/services/workspace/data_workspace_role_assignments.go +++ b/internal/services/workspace/data_workspace_role_assignments.go @@ -38,7 +38,7 @@ func (d *dataSourceWorkspaceRoleAssignments) Metadata(_ context.Context, req dat func (d *dataSourceWorkspaceRoleAssignments) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "List a Fabric " + WorkspaceRoleAssignmentsName + ".\n\n" + + MarkdownDescription: "List Fabric " + WorkspaceRoleAssignmentsName + ".\n\n" + "Use this data source to list [" + WorkspaceRoleAssignmentsName + "](" + WorkspaceRoleAssignmentDocsURL + ").\n\n" + ItemDocsSPNSupport, Attributes: map[string]schema.Attribute{ diff --git a/internal/services/workspace/resource_workspace_role_assignment.go b/internal/services/workspace/resource_workspace_role_assignment.go index 2ee8637f..fc6af65d 100644 --- a/internal/services/workspace/resource_workspace_role_assignment.go +++ b/internal/services/workspace/resource_workspace_role_assignment.go @@ -46,7 +46,7 @@ func (r *resourceWorkspaceRoleAssignment) Metadata(_ context.Context, req resour func (r *resourceWorkspaceRoleAssignment) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "Manage a Workspace Role Assignment.\n\n" + + MarkdownDescription: "Manage a " + WorkspaceRoleAssignmentName + ".\n\n" + "See [Roles in Workspaces](https://learn.microsoft.com/fabric/get-started/roles-workspaces) for more information.\n\n" + ItemDocsSPNSupport, Attributes: map[string]schema.Attribute{ diff --git a/internal/testhelp/fakes/fabric_gateway.go b/internal/testhelp/fakes/fabric_gateway.go new file mode 100644 index 00000000..d6f3359c --- /dev/null +++ b/internal/testhelp/fakes/fabric_gateway.go @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package fakes + +import ( + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" + fabfake "github.com/microsoft/fabric-sdk-go/fabric/fake" + + gw "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" +) + +// operationsGateway implements SimpleIDOperations. +type operationsGateway struct{} + +func (o *operationsGateway) Create(data fabcore.CreateGatewayRequestClassification) fabcore.GatewayClassification { + switch gateway := data.(type) { + case *fabcore.CreateVirtualNetworkGatewayRequest: + entity := NewRandomVirtualNetworkGateway() + entity.Type = gateway.Type + entity.DisplayName = gateway.DisplayName + entity.CapacityID = gateway.CapacityID + entity.InactivityMinutesBeforeSleep = gateway.InactivityMinutesBeforeSleep + entity.NumberOfMemberGateways = gateway.NumberOfMemberGateways + entity.VirtualNetworkAzureResource = gateway.VirtualNetworkAzureResource + + return entity + default: + panic("Unsupported Gateway type") + } +} + +func (o *operationsGateway) TransformCreate(entity fabcore.GatewayClassification) fabcore.GatewaysClientCreateGatewayResponse { + return fabcore.GatewaysClientCreateGatewayResponse{ + GatewayClassification: entity, + } +} + +func (o *operationsGateway) TransformGet(entity fabcore.GatewayClassification) fabcore.GatewaysClientGetGatewayResponse { + return fabcore.GatewaysClientGetGatewayResponse{ + GatewayClassification: entity, + } +} + +func (o *operationsGateway) TransformList(list []fabcore.GatewayClassification) fabcore.GatewaysClientListGatewaysResponse { + return fabcore.GatewaysClientListGatewaysResponse{ + ListGatewaysResponse: fabcore.ListGatewaysResponse{ + Value: list, + }, + } +} + +func (o *operationsGateway) TransformUpdate(entity fabcore.GatewayClassification) fabcore.GatewaysClientUpdateGatewayResponse { + return fabcore.GatewaysClientUpdateGatewayResponse{ + GatewayClassification: entity, + } +} + +func (o *operationsGateway) Update(base fabcore.GatewayClassification, data fabcore.UpdateGatewayRequestClassification) fabcore.GatewayClassification { + switch gateway := base.(type) { + case *fabcore.VirtualNetworkGateway: + request, ok := data.(*fabcore.UpdateVirtualNetworkGatewayRequest) + if !ok { + panic("Invalid update data for VirtualNetworkGateway") + } + + gateway.CapacityID = request.CapacityID + gateway.DisplayName = request.DisplayName + gateway.InactivityMinutesBeforeSleep = request.InactivityMinutesBeforeSleep + gateway.NumberOfMemberGateways = request.NumberOfMemberGateways + + return gateway + default: + panic("Unsupported Gateway type") + } +} + +func (o *operationsGateway) Validate(newEntity fabcore.GatewayClassification, existing []fabcore.GatewayClassification) (int, error) { + for _, existingGateway := range existing { + if *(existingGateway.GetGateway().Type) != *(newEntity.GetGateway().Type) { + continue + } + + if newVNG, ok := newEntity.(*fabcore.VirtualNetworkGateway); ok { + if existingVNG, ok := existingGateway.(*fabcore.VirtualNetworkGateway); ok && *existingVNG.DisplayName == *newVNG.DisplayName { + return http.StatusConflict, fabfake.SetResponseError(http.StatusConflict, fabcore.ErrGateway.DuplicateGatewayName.Error(), fabcore.ErrGateway.DuplicateGatewayName.Error()) + } + } + } + + return http.StatusCreated, nil +} + +func (o *operationsGateway) GetID(entity fabcore.GatewayClassification) string { + return *entity.GetGateway().ID +} + +func configureVirtualNetworkGateway(server *fakeServer) fabcore.VirtualNetworkGateway { + configureGateway(server) + + return fabcore.VirtualNetworkGateway{} +} + +func configureOnPremisesGateway(server *fakeServer) fabcore.OnPremisesGateway { + configureGateway(server) + + return fabcore.OnPremisesGateway{} +} + +func configureOnPremisesGatewayPersonal(server *fakeServer) fabcore.OnPremisesGatewayPersonal { + configureGateway(server) + + return fabcore.OnPremisesGatewayPersonal{} +} + +func configureGateway(server *fakeServer) { + type concreteEntityOperations interface { + simpleIDOperations[ + fabcore.GatewayClassification, + fabcore.GatewaysClientGetGatewayResponse, + fabcore.GatewaysClientUpdateGatewayResponse, + fabcore.GatewaysClientCreateGatewayResponse, + fabcore.GatewaysClientListGatewaysResponse, + fabcore.CreateGatewayRequestClassification, + fabcore.UpdateGatewayRequestClassification] + } + + var entityOperations concreteEntityOperations = &operationsGateway{} + + handler := newTypedHandler(server, entityOperations) + + configureEntityPagerWithSimpleID( + handler, + entityOperations, + &handler.ServerFactory.Core.GatewaysServer.GetGateway, + &handler.ServerFactory.Core.GatewaysServer.UpdateGateway, + &handler.ServerFactory.Core.GatewaysServer.CreateGateway, + &handler.ServerFactory.Core.GatewaysServer.NewListGatewaysPager, + &handler.ServerFactory.Core.GatewaysServer.DeleteGateway) +} + +func NewRandomGateway() fabcore.GatewayClassification { + gatewayType := testhelp.RandomElement(fabcore.PossibleGatewayTypeValues()) + + switch gatewayType { + case fabcore.GatewayTypeOnPremises: + return NewRandomOnPremisesGateway() + case fabcore.GatewayTypeOnPremisesPersonal: + return NewRandomOnPremisesGatewayPersonal() + case fabcore.GatewayTypeVirtualNetwork: + return NewRandomVirtualNetworkGateway() + default: + panic("Unsupported Gateway type") + } +} + +func NewRandomOnPremisesGateway() *fabcore.OnPremisesGateway { + return &fabcore.OnPremisesGateway{ + ID: to.Ptr(testhelp.RandomUUID()), + Type: to.Ptr(fabcore.GatewayTypeOnPremises), + DisplayName: to.Ptr(testhelp.RandomName()), + AllowCloudConnectionRefresh: to.Ptr(testhelp.RandomBool()), + AllowCustomConnectors: to.Ptr(testhelp.RandomBool()), + NumberOfMemberGateways: to.Ptr(testhelp.RandomInt(gw.MinNumberOfMemberGatewaysValues, gw.MaxNumberOfMemberGatewaysValues)), + LoadBalancingSetting: to.Ptr(testhelp.RandomElement(fabcore.PossibleLoadBalancingSettingValues())), + Version: to.Ptr(testhelp.RandomName()), + PublicKey: NewRadomPublicKey(), + } +} + +func NewRandomOnPremisesGatewayPersonal() *fabcore.OnPremisesGatewayPersonal { + return &fabcore.OnPremisesGatewayPersonal{ + ID: to.Ptr(testhelp.RandomUUID()), + Type: to.Ptr(fabcore.GatewayTypeOnPremisesPersonal), + Version: to.Ptr(testhelp.RandomName()), + PublicKey: NewRadomPublicKey(), + } +} + +func NewRandomVirtualNetworkGateway() *fabcore.VirtualNetworkGateway { + return &fabcore.VirtualNetworkGateway{ + ID: to.Ptr(testhelp.RandomUUID()), + Type: to.Ptr(fabcore.GatewayTypeVirtualNetwork), + DisplayName: to.Ptr(testhelp.RandomName()), + CapacityID: to.Ptr(testhelp.RandomUUID()), + InactivityMinutesBeforeSleep: to.Ptr(testhelp.RandomElement(gw.PossibleInactivityMinutesBeforeSleepValues)), + NumberOfMemberGateways: to.Ptr(testhelp.RandomInt(gw.MinNumberOfMemberGatewaysValues, gw.MaxNumberOfMemberGatewaysValues)), + VirtualNetworkAzureResource: NewRandomVirtualNetworkAzureResource(), + } +} + +func NewRadomPublicKey() *fabcore.PublicKey { + return &fabcore.PublicKey{ + Exponent: to.Ptr(testhelp.RandomName()), + Modulus: to.Ptr(testhelp.RandomName()), + } +} + +func NewRandomVirtualNetworkAzureResource() *fabcore.VirtualNetworkAzureResource { + return &fabcore.VirtualNetworkAzureResource{ + ResourceGroupName: to.Ptr(testhelp.RandomName()), + SubnetName: to.Ptr(testhelp.RandomName()), + SubscriptionID: to.Ptr(testhelp.RandomUUID()), + VirtualNetworkName: to.Ptr(testhelp.RandomName()), + } +} diff --git a/internal/testhelp/fakes/fake_server.go b/internal/testhelp/fakes/fake_server.go index b6f671e9..e848bc85 100644 --- a/internal/testhelp/fakes/fake_server.go +++ b/internal/testhelp/fakes/fake_server.go @@ -35,6 +35,9 @@ func newFakeServer() *fakeServer { handleEntity(server, configureDomain) handleEntity(server, configureEventhouse) handleEntity(server, configureEnvironment) + handleEntity(server, configureVirtualNetworkGateway) + handleEntity(server, configureOnPremisesGateway) + handleEntity(server, configureOnPremisesGatewayPersonal) handleEntity(server, configureKQLDatabase) handleEntity(server, configureLakehouse) handleEntity(server, configureNotebook) @@ -57,7 +60,12 @@ func handleEntity[TEntity any](server *fakeServer, configureFunction func(server // SupportsType returns true if the server supports the given type. func (s *fakeServer) isSupportedType(t reflect.Type) bool { for _, supportedType := range s.types { - if supportedType == t { + // if supportedType is an interface, check if t implements it + if supportedType.Kind() == reflect.Interface { + if t.Implements(supportedType) { + return true + } + } else if supportedType == t { return true } } @@ -68,7 +76,13 @@ func (s *fakeServer) isSupportedType(t reflect.Type) bool { // Upsert inserts or updates an element in the server. // It panics if the element type is not supported. func (s *fakeServer) Upsert(element any) { - if !s.isSupportedType(reflect.TypeOf(element)) { + elementType := reflect.TypeOf(element) + // if elementType is a pointer, get the underlying type + if elementType.Kind() == reflect.Ptr { + elementType = elementType.Elem() + } + + if !s.isSupportedType(elementType) { panic("Unsupported type: " + reflect.TypeOf(element).String() + ". Did you forget to call HandleEntity in NewFakeServer?") // lintignore:R009 } diff --git a/internal/testhelp/fakes/fake_typedhandler.go b/internal/testhelp/fakes/fake_typedhandler.go index 02edc146..7b47d5e0 100644 --- a/internal/testhelp/fakes/fake_typedhandler.go +++ b/internal/testhelp/fakes/fake_typedhandler.go @@ -287,6 +287,12 @@ func (h *typedHandler[TEntity]) entityTypeIsFabricItem() bool { func (h *typedHandler[TEntity]) entityTypeCanBeConvertedToFabricItem() bool { var entity TEntity + // if entity is an interface, return false + entityAsAny := (any)(entity) + if entityAsAny == nil { + return false + } + requiredPropertyNames := []string{"ID", "WorkspaceID", "DisplayName", "Description", "Type"} for _, propertyName := range requiredPropertyNames { diff --git a/internal/testhelp/fixtures/.wellknown.template.json b/internal/testhelp/fixtures/.wellknown.template.json deleted file mode 100644 index c4986090..00000000 --- a/internal/testhelp/fixtures/.wellknown.template.json +++ /dev/null @@ -1,110 +0,0 @@ -{ - "workspace": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_workspace", - "description": "testacc_fabric_workspace" - }, - "dashboard": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_dashboard", - "description": "testacc_fabric_dashboard" - }, - "datamart": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_datamart", - "description": "testacc_fabric_datamart" - }, - "dataPipeline": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_data_pipeline", - "description": "testacc_fabric_data_pipeline" - }, - "environment": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_environment", - "description": "testacc_fabric_environment" - }, - "eventhouse": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_eventhouse", - "description": "testacc_fabric_eventhouse" - }, - "eventstream": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_eventstream", - "description": "testacc_fabric_eventstream" - }, - "kqlDatabase": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_kql_database", - "description": "testacc_fabric_kql_database" - }, - "kqlQueryset": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_kql_queryset", - "description": "testacc_fabric_kql_queryset" - }, - "lakehouse": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_lakehouse", - "description": "testacc_fabric_lakehouse" - }, - "mlExperiment": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_ml_experiment", - "description": "testacc_fabric_ml_experiment" - }, - "mlModel": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_ml_model", - "description": "testacc_fabric_ml_model" - }, - "notebook": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_notebook", - "description": "testacc_fabric_notebook" - }, - "report": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_report", - "description": "testacc_fabric_report" - }, - "semanticModel": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_semantic_model", - "description": "testacc_fabric_semantic_model" - }, - "warehouse": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_warehouse", - "description": "testacc_fabric_warehouse" - }, - "sparkJobDefinition": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_spark_job_definition", - "description": "testacc_fabric_spark_job_definition" - }, - "capacity": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_fabric_capacity", - "description": "testacc_fabric_capacity" - }, - "domainParent": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_domain_parent", - "description": "testacc_domain_parent" - }, - "domainChild": { - "id": "00000000-0000-0000-0000-000000000000", - "displayName": "testacc_domain_child", - "description": "testacc_domain_child" - }, - "principal": { - "id": "00000000-0000-0000-0000-000000000000", - "type": "ServicePrincipal" - }, - "group": { - "id": "00000000-0000-0000-0000-000000000000", - "type": "Group" - } -} diff --git a/internal/testhelp/utils.go b/internal/testhelp/utils.go index 167b1d45..ba2e8433 100644 --- a/internal/testhelp/utils.go +++ b/internal/testhelp/utils.go @@ -9,6 +9,7 @@ import ( "encoding/pem" "errors" "fmt" + "math" "strings" "github.com/hashicorp/go-uuid" @@ -27,12 +28,32 @@ func RandomName(length ...int) string { return acctest.RandStringFromCharSet(size, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") } +// RandomInt returns a random integer between minInt (inclusive) and maxInt (exclusive). +func RandomInt(minInt, maxInt int32) int32 { + // #nosec G115 -- safe cast because we know minInt/maxInt fit in int32 + return int32(acctest.RandIntRange(int(minInt), int(maxInt))) +} + +func RandomBool() bool { + return acctest.RandIntRange(0, 2) == 1 +} + func RandomUUID() string { result, _ := uuid.GenerateUUID() return result } +func RandomElement[T any](elements []T) T { + elementsLen := len(elements) + + if elementsLen < math.MinInt32 || elementsLen > math.MaxInt32 { + panic(fmt.Sprintf("random number %d out of int32 bounds", elementsLen)) + } + + return elements[RandomInt(0, int32(elementsLen))] +} + func RandomURI() string { return fmt.Sprintf("https://%s.com", strings.ToLower(RandomName())) } diff --git a/tools/scripts/Set-WellKnown.ps1 b/tools/scripts/Set-WellKnown.ps1 old mode 100644 new mode 100755 index e5093093..d53ef888 --- a/tools/scripts/Set-WellKnown.ps1 +++ b/tools/scripts/Set-WellKnown.ps1 @@ -449,7 +449,7 @@ function Set-FabricWorkspaceCapacity { $payload = @{ capacityId = $CapacityId } - $result = (Invoke-FabricRest -Method 'POST' -Endpoint "workspaces/$WorkspaceId/assignToCapacity" -Payload $payload).Response + _ = (Invoke-FabricRest -Method 'POST' -Endpoint "workspaces/$WorkspaceId/assignToCapacity" -Payload $payload).Response $workspace.Response.capacityId = $CapacityId } @@ -480,8 +480,167 @@ function Set-FabricWorkspaceRoleAssignment { } } +function Set-FabricGatewayVirtualNetwork { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$DisplayName, + + [Parameter(Mandatory = $true)] + [string]$CapacityId, + + # Inactivity time (in minutes) before the gateway goes to auto-sleep. + # Allowed values: 30, 60, 90, 120, 150, 240, 360, 480, 720, 1440. + [Parameter(Mandatory = $true)] + [int]$InactivityMinutesBeforeSleep, + + # Number of member gateways (between 1 and 7). + [Parameter(Mandatory = $true)] + [int]$NumberOfMemberGateways, + + # Azure virtual network details: + [Parameter(Mandatory = $true)] + [string]$SubscriptionId, + + [Parameter(Mandatory = $true)] + [string]$ResourceGroupName, + + [Parameter(Mandatory = $true)] + [string]$VirtualNetworkName, + + [Parameter(Mandatory = $true)] + [string]$SubnetName + ) + + # Attempt to check for an existing gateway with the same display name. + $existingGateways = Invoke-FabricRest -Method 'GET' -Endpoint "gateways" + $result = $existingGateways.Response.value | Where-Object { $_.displayName -eq $DisplayName } + if (!$result) { + # Construct the payload for creating a Virtual Network gateway. + # Refer to the API documentation for details on the request format :contentReference[oaicite:1]{index=1} and the Virtual Network Azure Resource :contentReference[oaicite:2]{index=2}. + $payload = @{ + type = "VirtualNetwork" + displayName = $DisplayName + capacityId = $CapacityId + inactivityMinutesBeforeSleep = $InactivityMinutesBeforeSleep + numberOfMemberGateways = $NumberOfMemberGateways + virtualNetworkAzureResource = @{ + subscriptionId = $SubscriptionId + resourceGroupName = $ResourceGroupName + virtualNetworkName = $VirtualNetworkName + subnetName = $SubnetName + } + } + + Write-Log -Message "Creating Virtual Network Gateway: $DisplayName" -Level 'WARN' + $newGateway = Invoke-FabricRest -Method 'POST' -Endpoint "gateways" -Payload $payload + $result = $newGateway.Response + } + + Write-Log -Message "Gateway Virtual Network - Name: $($result.displayName) / ID: $($result.id)" + return $result +} + + +function Set-AzureVirtualNetwork { + param( + [Parameter(Mandatory = $true)] + [string]$ResourceGroupName, + + [Parameter(Mandatory = $true)] + [string]$VNetName, + + [Parameter(Mandatory = $true)] + [string]$Location, + + [Parameter(Mandatory = $true)] + [string[]]$AddressPrefixes, + + [Parameter(Mandatory = $true)] + [string]$SubnetName, + + [Parameter(Mandatory = $true)] + [string[]]$SubnetAddressPrefixes + ) + + # Attempt to get the existing Virtual Network + try { + $vnet = Get-AzVirtualNetwork -Name $VNetName -ResourceGroupName $ResourceGroupName -ErrorAction Stop + } + catch { + # VNet does not exist, so create it + Write-Log -Message "Creating VNet: $VNetName in Resource Group: $ResourceGroupName" -Level 'WARN' + $subnetConfig = New-AzVirtualNetworkSubnetConfig ` + -Name $SubnetName ` + -AddressPrefix $SubnetAddressPrefixes ` + + $subnetConfig = Add-AzDelegation ` + -Name 'PowerPlatformVnetAccess' ` + -ServiceName 'Microsoft.PowerPlatform/vnetaccesslinks' ` + -Subnet $subnetConfig + + $vnet = New-AzVirtualNetwork ` + -Name $VNetName ` + -ResourceGroupName $ResourceGroupName ` + -Location $Location ` + -AddressPrefix $AddressPrefixes ` + -Subnet $subnetConfig + + # Commit creation + $vnet = $vnet | Set-AzVirtualNetwork + Write-Log -Message "Created VNet: $VNetName" -Level 'INFO' + } + + # If the VNet already exists, check for the subnet + $subnet = $vnet.Subnets | Where-Object { $_.Name -eq $SubnetName } + if (-not $subnet) { + # Subnet does not exist; add one with the delegation + Write-Log -Message "Adding subnet '$SubnetName' with delegation 'Microsoft.PowerPlatform/vnetaccesslinks' to VNet '$VNetName'." -Level 'WARN' + $subnetConfig = New-AzVirtualNetworkSubnetConfig ` + -Name $SubnetName ` + -AddressPrefix $SubnetAddressPrefixes ` + + $subnetConfig = Add-AzDelegation ` + -Name 'PowerPlatformVnetAccess' ` + -ServiceName 'Microsoft.PowerPlatform/vnetaccesslinks' ` + -Subnet $subnetConfig + + $vnet = $vnet | Set-AzVirtualNetwork + } + else { + # Subnet exists; ensure it has the correct delegation + $existingDelegation = $subnet.Delegations | Where-Object { $_.ServiceName -eq 'Microsoft.PowerPlatform/vnetaccesslinks' } + if (-not $existingDelegation) { + Write-Log -Message "Subnet '$SubnetName' found but missing delegation to 'Microsoft.PowerPlatform/vnetaccesslinks'. Adding Microsoft.PowerPlatform/vnetaccesslinks delegation..." -Level 'WARN' + + $subnetConfig = Add-AzDelegation ` + -Name 'PowerPlatformVnetAccess' ` + -ServiceName 'Microsoft.PowerPlatform/vnetaccesslinks' ` + -Subnet $subnet + + $vnet = $vnet | Set-AzVirtualNetwork + Write-Log -Message "Added missing delegation to subnet '$SubnetName'." -Level 'INFO' + } + } + Write-Log -Message "Az Virtual Network - Name: $($vnet.Name)" + + $userPrincipalName = $azContext.Account.Id + $principal = Get-AzADUser -UserPrincipalName $userPrincipalName + + $existingAssignment = Get-AzRoleAssignment -Scope $vnet.Id -ObjectId $principal.Id -ErrorAction SilentlyContinue | Where-Object { + $_.RoleDefinitionName -eq "Network Contributor" + } + + Write-Log "Assigning Network Contributor role to the principal on the virtual network $($VNetName)" + if (!$existingAssignment) { + New-AzRoleAssignment -ObjectId $principal.Id -RoleDefinitionName "Network Contributor" -Scope $vnet.Id + } + + return $vnet +} + # Define an array of modules to install -$modules = @('Az.Accounts', 'Az.Resources', 'Az.Fabric', 'pwsh-dotenv', 'ADOPS') +$modules = @('Az.Accounts', 'Az.Resources', 'Az.Fabric', 'pwsh-dotenv', 'ADOPS', 'Az.Network') # Loop through each module and install if not installed foreach ($module in $modules) { @@ -494,8 +653,8 @@ if (Test-Path -Path './wellknown.env') { Import-Dotenv -Path ./wellknown.env -AllowClobber } -if (!$Env:FABRIC_TESTACC_WELLKNOWN_ENTRA_TENANT_ID -or !$Env:FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID -or !$Env:FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME -or !$Env:FABRIC_TESTACC_WELLKNOWN_AZDO_ORGANIZATION_NAME -or !$Env:FABRIC_TESTACC_WELLKNOWN_NAME_PREFIX) { - Write-Log -Message 'FABRIC_TESTACC_WELLKNOWN_ENTRA_TENANT_ID, FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID, FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME, FABRIC_TESTACC_WELLKNOWN_AZDO_ORGANIZATION_NAME and FABRIC_TESTACC_WELLKNOWN_NAME_PREFIX are required environment variables.' -Level 'ERROR' +if (!$Env:FABRIC_TESTACC_WELLKNOWN_ENTRA_TENANT_ID -or !$Env:FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID -or !$Env:FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME -or !$Env:FABRIC_TESTACC_WELLKNOWN_AZDO_ORGANIZATION_NAME -or !$Env:FABRIC_TESTACC_WELLKNOWN_NAME_PREFIX -or !$Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME -or !$Env:FABRIC_TESTACC_WELLKNOWN_AZURE_LOCATION) { + Write-Log -Message 'FABRIC_TESTACC_WELLKNOWN_ENTRA_TENANT_ID, FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID, FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME, FABRIC_TESTACC_WELLKNOWN_AZDO_ORGANIZATION_NAME and FABRIC_TESTACC_WELLKNOWN_NAME_PREFIX and FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME and FABRIC_TESTACC_WELLKNOWN_AZURE_LOCATION are required environment variables.' -Level 'ERROR' } # Check if already logged in to Azure, if not then login @@ -566,6 +725,10 @@ $itemNaming = @{ 'EntraServicePrincipal' = 'sp' 'EntraGroup' = 'grp' 'AzDOProject' = 'proj' + 'VirtualNetwork01' = 'vnet01' + 'VirtualNetwork02' = 'vnet02' + 'VirtualNetworkSubnet' = 'subnet' + 'GatewayVirtualNetwork' = 'gvnet' } $baseName = Get-BaseName @@ -575,6 +738,8 @@ $Env:FABRIC_TESTACC_WELLKNOWN_NAME_BASE = $baseName $envVarNames = @( 'FABRIC_TESTACC_WELLKNOWN_ENTRA_TENANT_ID', 'FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID', + 'FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME' + 'FABRIC_TESTACC_WELLKNOWN_AZURE_LOCATION', 'FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME', 'FABRIC_TESTACC_WELLKNOWN_AZDO_ORGANIZATION_NAME', 'FABRIC_TESTACC_WELLKNOWN_NAME_PREFIX', @@ -852,6 +1017,74 @@ if ($SPN) { $result = Set-ADOPSGitPermission -ProjectId $azdoProject.id -RepositoryId $azdoRepo.id -Descriptor $azdoSPN.descriptor -Allow 'GenericContribute', 'PullRequestContribute', 'CreateBranch', 'CreateTag', 'GenericRead' } +# Register the Microsoft.PowerPlatform resource provider +Write-Log -Message "Registering Microsoft.PowerPlatform resource provider" -Level 'WARN' +Register-AzResourceProvider -ProviderNamespace "Microsoft.PowerPlatform" + +# Create Azure Virtual Network 1 if not exists +$vnetName = "${displayName}_$($itemNaming['VirtualNetwork01'])" +$addrRange = "10.10.0.0/16" +$subName = "${displayName}_$($itemNaming['VirtualNetworkSubnet'])" +$subRange = "10.10.1.0/24" + +$vnet = Set-AzureVirtualNetwork ` + -ResourceGroupName $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME ` + -VNetName $vnetName ` + -Location $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_LOCATION ` + -AddressPrefixes $addrRange ` + -SubnetName $subName ` + -SubnetAddressPrefixes $subRange + +$wellKnown['VirtualNetwork01'] = @{ + name = $vnet.Name + resourceGroupName = $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME + subnetName = $subName + subscriptionId = $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID +} + + +# Create Azure Virtual Network 2 if not exists +$vnetName = "${displayName}_$($itemNaming['VirtualNetwork02'])" +$addrRange = "10.10.0.0/16" +$subName = "${displayName}_$($itemNaming['VirtualNetworkSubnet'])" +$subRange = "10.10.1.0/24" + +$vnet = Set-AzureVirtualNetwork ` + -ResourceGroupName $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME ` + -VNetName $vnetName ` + -Location $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_LOCATION ` + -AddressPrefixes $addrRange ` + -SubnetName $subName ` + -SubnetAddressPrefixes $subRange + +$wellKnown['VirtualNetwork02'] = @{ + name = $vnet.Name + resourceGroupName = $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME + subnetName = $subName + subscriptionId = $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID +} + +# Create Fabric Gateway Virtual Network if not exists +$displayNameTemp = "${displayName}_$($itemNaming['GatewayVirtualNetwork'])" +$inactivityMinutesBeforeSleep = 30 +$numberOfMemberGateways = 1 + +$gateway = Set-FabricGatewayVirtualNetwork ` + -DisplayName $displayNameTemp ` + -CapacityId $capacity.id ` + -InactivityMinutesBeforeSleep $inactivityMinutesBeforeSleep ` + -NumberOfMemberGateways $numberOfMemberGateways ` + -SubscriptionId $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID ` + -ResourceGroupName $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME ` + -VirtualNetworkName $wellKnown['VirtualNetwork01'].name ` + -SubnetName $wellKnown['VirtualNetwork01'].subnetName + +$wellKnown['GatewayVirtualNetwork'] = @{ + id = $gateway.id + displayName = $gateway.displayName + type = $gateway.type +} + # Save wellknown.json file $wellKnownJson = $wellKnown | ConvertTo-Json $wellKnownJson