diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f6fdc914..6b038b0e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -44,6 +44,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/kqldashboard" "github.com/microsoft/terraform-provider-fabric/internal/services/kqldatabase" "github.com/microsoft/terraform-provider-fabric/internal/services/kqlqueryset" @@ -387,6 +388,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.NewResourceGatewayRoleAssignment, + gateway.NewResourceVirtualNetworkGateway, kqldashboard.NewResourceKQLDashboard, kqldatabase.NewResourceKQLDatabase, kqlqueryset.NewResourceKQLQueryset, @@ -424,6 +427,13 @@ func (p *FabricProvider) DataSources(ctx context.Context) []func() datasource.Da func() datasource.DataSource { return eventhouse.NewDataSourceEventhouses(ctx) }, eventstream.NewDataSourceEventstream, eventstream.NewDataSourceEventstreams, + gateway.NewDataSourceGatewayRoleAssignments, + gateway.NewDataSourceOnPremisesGateway, + gateway.NewDataSourceOnPremisesGateways, + gateway.NewDataSourceOnPremisesGatewayPersonal, + gateway.NewDataSourceOnPremisesGatewayPersonals, + gateway.NewDataSourceVirtualNetworkGateway, + gateway.NewDataSourceVirtualNetworkGateways, kqldashboard.NewDataSourceKQLDashboard, kqldashboard.NewDataSourceKQLDashboards, kqldatabase.NewDataSourceKQLDatabase, diff --git a/internal/services/gateway/base.go b/internal/services/gateway/base.go new file mode 100644 index 00000000..62b9ce8a --- /dev/null +++ b/internal/services/gateway/base.go @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "github.com/microsoft/terraform-provider-fabric/internal/common" +) + +const ( + OnPremisesItemTFType = "on_premises_gateway" + OnPremisesItemsTFType = "on_premises_gateways" + OnPremisesPersonalItemType = "on_premises_personal_gateway" + OnPremisesPersonalItemsType = "on_premises_personal_gateways" + VirtualNetworkItemTFType = "virtual_network_gateway" + VirtualNetworkItemsTFType = "virtual_network_gateways" + ItemName = "Gateway" + ItemsName = "Gateways" + ItemsTFName = "gateways" + ItemDocsSPNSupport = common.DocsSPNSupported + ItemDocsURL = "https://learn.microsoft.com/en-us/fabric/data-factory/how-to-access-on-premises-data" +) + +var ( + PossibleInactivityMinutesBeforeSleepValues = []int32{30, 60, 90, 120, 150, 240, 360, 480, 720, 1440} + + MinNumberOfMemberGatewaysValues = int32(1) + + MaxNumberOfMemberGatewaysValues = int32(7) +) diff --git a/internal/services/gateway/base_test.go b/internal/services/gateway/base_test.go new file mode 100644 index 00000000..e8b0ae76 --- /dev/null +++ b/internal/services/gateway/base_test.go @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway_test + +import ( + "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" +) + +const ( + VirtualNetworkItemTFName = gateway.VirtualNetworkItemTFType + VirtualNetworkItemsTFName = gateway.VirtualNetworkItemsTFType + OnPremisesItemTFName = gateway.OnPremisesItemTFType + OnPremisesItemsTFName = gateway.OnPremisesItemsTFType + OnPremisesPersonalItemTFName = gateway.OnPremisesPersonalItemType + OnPremisesPersonalItemsTFName = gateway.OnPremisesPersonalItemsType + itemsTFName = gateway.ItemsTFName + itemType = gateway.ItemName +) 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..463129b5 --- /dev/null +++ b/internal/services/gateway/data_gateway_role_assignments.go @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "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 + "_" + "gateway_role_assignments" +} + +func (d *dataSourceGatewayRoleAssignments) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "List the Fabric gateway role assignments.\n\n" + + "Use this data source to list the role assignments for a gateway.\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: "A list of gateway role assignments.", + Computed: true, + CustomType: supertypes.NewListNestedObjectTypeOf[gatewayRoleAssignmentModel](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The role assignment ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "role": schema.StringAttribute{ + MarkdownDescription: "The gateway role of the principal.", + Computed: true, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The principal's display name.", + Computed: true, + }, + "type": schema.StringAttribute{ + MarkdownDescription: "The type of the principal.", + Computed: true, + }, + }, + }, + }, + "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 + // Create a gateways client via the provider's FabricClient. + d.client = (*fabcore.GatewaysClient)(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() + + respList, err := d.client.ListGatewayRoleAssignments(ctx, data.GatewayID.ValueString(), nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + if diags := data.setValues(ctx, respList); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) +} 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..4c40d6e0 --- /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", "gateway_role_assignments", "test") + testDataSourceGatewayRoleAssignmentsHeader = at.DataSourceHeader(testhelp.TypeName("fabric", "gateway_role_assignments"), "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{ + // Step 1: Error on unexpected attribute. + { + 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`), + }, + // Step 2: Read the role assignments. + { + 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) { + // For acceptance testing, assume a well-known gateway is provided. + gateway := testhelp.WellKnown()["GatewayVirtualNetwork"].(map[string]any) + gatewayID := gateway["id"].(string) + + resource.ParallelTest(t, testhelp.NewTestAccCase(t, nil, nil, []resource.TestStep{ + // Step: Read the gateway role assignments. + { + 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_on_premises_gateway.go b/internal/services/gateway/data_on_premises_gateway.go new file mode 100644 index 00000000..7c9d6871 --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateway.go @@ -0,0 +1,220 @@ +// 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-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 = (*dataSourceOnPremisesGateway)(nil) + _ datasource.DataSourceWithConfigure = (*dataSourceOnPremisesGateway)(nil) +) + +type dataSourceOnPremisesGateway struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceOnPremisesGateway() datasource.DataSource { + return &dataSourceOnPremisesGateway{} +} + +func (d *dataSourceOnPremisesGateway) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + OnPremisesItemTFType +} + +func (d *dataSourceOnPremisesGateway) 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 fetch [" + ItemName + "](" + ItemDocsURL + ").\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("The %s ID.", ItemName), + Optional: true, + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("The %s display name.", ItemName), + Optional: true, + Computed: true, + }, + "allow_cloud_connection_refresh": schema.BoolAttribute{ + MarkdownDescription: "Defines if cloud connection refresh is allowed.", + Computed: true, + }, + "allow_custom_connectors": schema.BoolAttribute{ + MarkdownDescription: "Defines if custom connectors are allowed.", + Computed: true, + }, + "load_balancing_setting": schema.StringAttribute{ + MarkdownDescription: "Gateway load balancing setting.", + Computed: true, + }, + "number_of_member_gateways": schema.Int32Attribute{ + MarkdownDescription: "The number of member gateways.", + Computed: true, + }, + "public_key": schema.SingleNestedAttribute{ + MarkdownDescription: "The public key settings.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[publicKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "exponent": schema.StringAttribute{ + MarkdownDescription: "The RSA exponent.", + Computed: true, + }, + "modulus": schema.StringAttribute{ + MarkdownDescription: "The RSA modulus.", + Computed: true, + }, + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "The gateway version.", + Computed: true, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceOnPremisesGateway) 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"), + ), + } +} + +// Configure adds the provider configured client to the data source. +func (d *dataSourceOnPremisesGateway) 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 *dataSourceOnPremisesGateway) 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 datasourceOnPremisesGatewayModel + + 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 *dataSourceOnPremisesGateway) getByID(ctx context.Context, model *datasourceOnPremisesGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, "GET 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 gw, ok := respGet.GatewayClassification.(*fabcore.OnPremisesGateway); ok { + model.set(ctx, *gw) + return nil + } else { + var diags diag.Diagnostics + diags.AddError(common.ErrorReadHeader, "expected gateway to be an on-premises gateway") + return diags + } +} + +func (d *dataSourceOnPremisesGateway) getByDisplayName(ctx context.Context, model *datasourceOnPremisesGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, fmt.Sprintf("getting %s by 'display_name'", ItemName)) + + gateways, err := d.client.ListGateways(ctx, nil) + + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationRead, nil); diags.HasError() { + return diags + } + + for _, gw := range gateways { + if onPremisesGateway, ok := gw.(*fabcore.OnPremisesGateway); ok { + if *onPremisesGateway.DisplayName == model.DisplayName.ValueString() { + model.set(ctx, *onPremisesGateway) + return nil + } + } + } + + return diag.Diagnostics{diag.NewErrorDiagnostic(common.ErrorReadHeader, "on-premises gateway not found")} +} diff --git a/internal/services/gateway/data_on_premises_gateway_personal.go b/internal/services/gateway/data_on_premises_gateway_personal.go new file mode 100644 index 00000000..88141650 --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateway_personal.go @@ -0,0 +1,134 @@ +// 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-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" + pconfig "github.com/microsoft/terraform-provider-fabric/internal/provider/config" +) + +type dataSourceOnPremisesGatewayPersonal struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceOnPremisesGatewayPersonal() datasource.DataSource { + return &dataSourceOnPremisesGatewayPersonal{} +} + +func (d *dataSourceOnPremisesGatewayPersonal) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + OnPremisesPersonalItemType +} + +func (d *dataSourceOnPremisesGatewayPersonal) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieve an on-premises gateway in its 'personal' form (ID, public key, type, version).", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Required: true, + MarkdownDescription: "The gateway ID.", + CustomType: customtypes.UUIDType{}, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "The gateway version.", + Computed: true, + }, + "public_key": schema.SingleNestedAttribute{ + MarkdownDescription: "The public key settings of the gateway.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[publicKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "exponent": schema.StringAttribute{ + MarkdownDescription: "The RSA exponent.", + Computed: true, + }, + "modulus": schema.StringAttribute{ + MarkdownDescription: "The RSA modulus.", + Computed: true, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceOnPremisesGatewayPersonal) Configure(ctx 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 *dataSourceOnPremisesGatewayPersonal) 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 datasourceOnPremisesGatewayPersonalModel + 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() + + gatewayResp, err := d.client.GetGateway(ctx, data.ID.ValueString(), nil) + if err != nil { + resp.Diagnostics.AddError("GetGateway failed", err.Error()) + return + } + + realGw, ok := gatewayResp.GatewayClassification.(*fabcore.OnPremisesGatewayPersonal) + if !ok { + resp.Diagnostics.AddError("Unexpected Gateway Type", "Result is not an OnPremisesGatewayPersonal.") + return + } + + data.set(ctx, *realGw) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + tflog.Debug(ctx, "READ", map[string]any{ + "action": "end", + }) + + if diags := resp.State.Set(ctx, data); diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } +} diff --git a/internal/services/gateway/data_on_premises_gateway_personal_test.go b/internal/services/gateway/data_on_premises_gateway_personal_test.go new file mode 100644 index 00000000..cd11d063 --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateway_personal_test.go @@ -0,0 +1,91 @@ +// 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/common" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp/fakes" +) + +var ( + testDataSourceOnPremisesPersonalFQN = testhelp.DataSourceFQN("fabric", OnPremisesPersonalItemTFName, "test") + testDataSourceOnPremisesPersonalHeader = at.DataSourceHeader(testhelp.TypeName("fabric", OnPremisesPersonalItemTFName), "test") +) + +func TestUnit_OnPremisesGatewayPersonalDataSource(t *testing.T) { + entity := fakes.NewRandomOnPremisesGatewayPersonal() + + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGatewayPersonal()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGatewayPersonal()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase( + t, + nil, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + // Step 1: Unexpected attribute should trigger an error. + { + Config: at.CompileConfig( + testDataSourceOnPremisesPersonalHeader, + map[string]any{ + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // Step 2: Missing ID should trigger an error since ID is required for lookup. + { + Config: at.CompileConfig( + testDataSourceOnPremisesPersonalHeader, + map[string]any{}, + ), + ExpectError: regexp.MustCompile(`The argument "id" is required`), + }, + // Step 3: Read by id - not found + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "id": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // Step 4: Invalid UUID string should trigger an error. + { + Config: at.CompileConfig( + testDataSourceOnPremisesPersonalHeader, + map[string]any{ + "id": "invalid uuid", + }, + ), + ExpectError: regexp.MustCompile(`invalid uuid`), + }, + // Step 5: Valid read test using the entity's ID. + { + Config: at.CompileConfig( + testDataSourceOnPremisesPersonalHeader, + map[string]any{ + "id": *entity.ID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceOnPremisesPersonalFQN, "id", *entity.ID), + resource.TestCheckResourceAttr(testDataSourceOnPremisesPersonalFQN, "version", *entity.Version), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesPersonalFQN, "public_key.exponent"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesPersonalFQN, "public_key.modulus"), + ), + }, + }, + )) +} diff --git a/internal/services/gateway/data_on_premises_gateway_test.go b/internal/services/gateway/data_on_premises_gateway_test.go new file mode 100644 index 00000000..1853ba3d --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateway_test.go @@ -0,0 +1,133 @@ +// 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/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 ( + testDataSourceOnPremisesItemFabricFQN = testhelp.DataSourceFQN("fabric", OnPremisesItemTFName, "test") + testDataSourceOnPremisesItemHeader = at.DataSourceHeader(testhelp.TypeName("fabric", OnPremisesItemTFName), "test") +) + +func TestUnit_OnPremisesGatewayDataSource(t *testing.T) { + entity := fakes.NewRandomOnPremisesGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGateway()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGateway()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, nil, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - no attributes + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{}, + ), + ExpectError: regexp.MustCompile(`Exactly one of these attributes must be configured: \[id,display_name\]`), + }, + // error - id - invalid UUID + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "id": "invalid uuid", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // error - unexpected attribute + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "id": *entity.ID, + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // error - conflicting attributes + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "id": *entity.ID, + "display_name": *entity.DisplayName, + }, + ), + ExpectError: regexp.MustCompile(`These attributes cannot be configured together: \[id,display_name\]`), + }, + // read by id - not found + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "id": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // read by id + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "id": *entity.ID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceOnPremisesItemFabricFQN, "id", *entity.ID), + resource.TestCheckResourceAttr(testDataSourceOnPremisesItemFabricFQN, "display_name", *entity.DisplayName), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "allow_cloud_connection_refresh"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "allow_custom_connectors"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "load_balancing_setting"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "number_of_member_gateways"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "public_key.exponent"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "public_key.modulus"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "version"), + ), + }, + // read by name - not found + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "display_name": testhelp.RandomName(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // read by name + { + Config: at.CompileConfig( + testDataSourceOnPremisesItemHeader, + map[string]any{ + "display_name": *entity.DisplayName, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceOnPremisesItemFabricFQN, "id", *entity.ID), + resource.TestCheckResourceAttr(testDataSourceOnPremisesItemFabricFQN, "display_name", *entity.DisplayName), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "allow_cloud_connection_refresh"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "allow_custom_connectors"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "load_balancing_setting"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "number_of_member_gateways"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "public_key.exponent"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "public_key.modulus"), + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesItemFabricFQN, "version"), + ), + }, + })) +} diff --git a/internal/services/gateway/data_on_premises_gateways.go b/internal/services/gateway/data_on_premises_gateways.go new file mode 100644 index 00000000..1573bfad --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateways.go @@ -0,0 +1,156 @@ +// 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" +) + +// Ensure it implements the interface. +var _ datasource.DataSourceWithConfigure = (*dataSourceOnPremisesGateways)(nil) + +// dataSourceOnPremisesGateways is analogous to data_virtual_network_gateways.go, but for on-premises gateways (plural). +type dataSourceOnPremisesGateways struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceOnPremisesGateways() datasource.DataSource { + return &dataSourceOnPremisesGateways{} +} + +func (d *dataSourceOnPremisesGateways) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + OnPremisesItemsTFType +} + +func (d *dataSourceOnPremisesGateways) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "List all Fabric on-premises gateways.", + Attributes: map[string]schema.Attribute{ + "values": schema.ListNestedAttribute{ + Computed: true, + MarkdownDescription: "A list of on-premises gateways.", + CustomType: supertypes.NewListNestedObjectTypeOf[onPremisesGatewayModelBase](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The on-premises gateway ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The display name of the on-premises gateway.", + Computed: true, + }, + "allow_custom_connectors": schema.BoolAttribute{ + MarkdownDescription: "Allow custom connectors.", + Computed: true, + }, + "allow_cloud_connection_refresh": schema.BoolAttribute{ + MarkdownDescription: "Allow custom connectors refresh.", + Computed: true, + }, + "number_of_member_gateways": schema.Int64Attribute{ + MarkdownDescription: "The number of member gateways.", + Computed: true, + }, + "load_balancing_setting": schema.StringAttribute{ + MarkdownDescription: "The load balancing setting.", + Computed: true, + }, + "public_key": schema.SingleNestedAttribute{ + MarkdownDescription: "The public key settings.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[publicKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "exponent": schema.StringAttribute{ + MarkdownDescription: "The RSA exponent.", + Computed: true, + }, + "modulus": schema.StringAttribute{ + MarkdownDescription: "The RSA modulus.", + Computed: true, + }, + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "The gateway version.", + Computed: true, + }, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceOnPremisesGateways) 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.GatewaysClient)(fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient()) +} + +func (d *dataSourceOnPremisesGateways) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "READ-ALL-On-Premises-Gateways", map[string]any{"action": "start"}) + + var data dataSourceOnPremisesGatewaysModel + 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-ALL-On-Premises-Gateways", map[string]any{"action": "end"}) +} + +// list retrieves all gateways from the Fabric SDK and filters only the on-premises gateway ones. +func (d *dataSourceOnPremisesGateways) list(ctx context.Context, model *dataSourceOnPremisesGatewaysModel) diag.Diagnostics { + tflog.Trace(ctx, "Listing all on-premises gateways") + + gatewaysResp, err := d.client.ListGateways(ctx, nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + return diags + } + + return model.setValues(ctx, gatewaysResp) +} diff --git a/internal/services/gateway/data_on_premises_gateways_personal.go b/internal/services/gateway/data_on_premises_gateways_personal.go new file mode 100644 index 00000000..55555673 --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateways_personal.go @@ -0,0 +1,132 @@ +// 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" +) + +// dataSourceOnPremisesGatewayPersonals is the "plural" version of data_on_premises_gateway_personal.go. +type dataSourceOnPremisesGatewayPersonals struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceOnPremisesGatewayPersonals() datasource.DataSource { + return &dataSourceOnPremisesGatewayPersonals{} +} + +func (d *dataSourceOnPremisesGatewayPersonals) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + OnPremisesPersonalItemsType +} + +func (d *dataSourceOnPremisesGatewayPersonals) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "List all on-premises personal gateways.", + Attributes: map[string]schema.Attribute{ + "values": schema.ListNestedAttribute{ + Computed: true, + MarkdownDescription: "A list of on-premises personal gateways.", + CustomType: supertypes.NewListNestedObjectTypeOf[onPremisesGatewayPersonalModelBase](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The on-premises personal gateway ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "public_key": schema.SingleNestedAttribute{ + MarkdownDescription: "The public key settings.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[publicKeyModel](ctx), + Attributes: map[string]schema.Attribute{ + "exponent": schema.StringAttribute{ + MarkdownDescription: "RSA exponent.", + Computed: true, + }, + "modulus": schema.StringAttribute{ + MarkdownDescription: "RSA modulus.", + Computed: true, + }, + }, + }, + "version": schema.StringAttribute{ + MarkdownDescription: "The personal gateway version.", + Computed: true, + }, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceOnPremisesGatewayPersonals) 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.GatewaysClient)(fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient()) +} + +func (d *dataSourceOnPremisesGatewayPersonals) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "READ-ALL-On-Premises-Gateway-Personals", map[string]any{"action": "start"}) + + var data dataSourceOnPremisesGatewayPersonalsModel + 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-ALL-On-Premises-Gateway-Personals", map[string]any{"action": "end"}) +} + +func (d *dataSourceOnPremisesGatewayPersonals) list(ctx context.Context, model *dataSourceOnPremisesGatewayPersonalsModel) diag.Diagnostics { + tflog.Trace(ctx, "Listing all on-premises personal gateways") + + allItems, err := d.client.ListGateways(ctx, nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + return diags + } + + return model.setValues(ctx, allItems) +} diff --git a/internal/services/gateway/data_on_premises_gateways_personal_test.go b/internal/services/gateway/data_on_premises_gateways_personal_test.go new file mode 100644 index 00000000..8419e2b9 --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateways_personal_test.go @@ -0,0 +1,54 @@ +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 ( + testDataSourceOnPremisesPersonalsFQN = testhelp.DataSourceFQN("fabric", OnPremisesPersonalItemsTFName, "test") + testDataSourceOnPremisesPersonalsHeader = at.DataSourceHeader(testhelp.TypeName("fabric", OnPremisesPersonalItemsTFName), "test") +) + +func TestUnit_OnPremisesGatewaysPersonalDataSource(t *testing.T) { + entity := fakes.NewRandomOnPremisesGatewayPersonal() + + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGatewayPersonal()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGatewayPersonal()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase( + t, + nil, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + // Step 1: Use an unexpected attribute to trigger an error. + { + Config: at.CompileConfig( + testDataSourceOnPremisesPersonalsHeader, + map[string]any{ + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // Step 2: Normal read test with empty configuration. + { + Config: at.CompileConfig( + testDataSourceOnPremisesPersonalsHeader, + map[string]any{}, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesPersonalsFQN, "values.0.id"), + ), + }, + }, + )) +} diff --git a/internal/services/gateway/data_on_premises_gateways_test.go b/internal/services/gateway/data_on_premises_gateways_test.go new file mode 100644 index 00000000..39544eb2 --- /dev/null +++ b/internal/services/gateway/data_on_premises_gateways_test.go @@ -0,0 +1,54 @@ +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 ( + testDataSourceOnPremisesGatewaysFQN = testhelp.DataSourceFQN("fabric", OnPremisesItemsTFName, "test") + testDataSourceOnPremisesGatewaysHeader = at.DataSourceHeader(testhelp.TypeName("fabric", OnPremisesItemsTFName), "test") +) + +func TestUnit_OnPremisesGatewaysDataSource(t *testing.T) { + entity := fakes.NewRandomOnPremisesGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGateway()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomOnPremisesGateway()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase( + t, + nil, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + // Step to ensure an unexpected attribute triggers an error. + { + Config: at.CompileConfig( + testDataSourceOnPremisesGatewaysHeader, + map[string]any{ + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // A normal read test with an empty config. + { + Config: at.CompileConfig( + testDataSourceOnPremisesGatewaysHeader, + map[string]any{}, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(testDataSourceOnPremisesGatewaysFQN, "values.0.id"), + ), + }, + }, + )) +} diff --git a/internal/services/gateway/data_virtual_network_gateway.go b/internal/services/gateway/data_virtual_network_gateway.go new file mode 100644 index 00000000..75b585d5 --- /dev/null +++ b/internal/services/gateway/data_virtual_network_gateway.go @@ -0,0 +1,222 @@ +// 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-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 = (*dataSourceVirtualNetworkGateway)(nil) + _ datasource.DataSourceWithConfigure = (*dataSourceVirtualNetworkGateway)(nil) +) + +type dataSourceVirtualNetworkGateway struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceVirtualNetworkGateway() datasource.DataSource { + return &dataSourceVirtualNetworkGateway{} +} + +func (d *dataSourceVirtualNetworkGateway) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + VirtualNetworkItemTFType +} + +func (d *dataSourceVirtualNetworkGateway) 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 fetch [" + ItemName + "](" + ItemDocsURL + ").\n\n" + + ItemDocsSPNSupport, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("The %s ID.", ItemName), + Optional: true, + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("The %s display name.", ItemName), + Optional: true, + Computed: true, + }, + "capacity_id": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("The %s capacity Id.", ItemName), + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "inactivity_minutes_before_sleep": schema.Int32Attribute{ + MarkdownDescription: "The number of minutes of inactivity before the gateway goes to sleep.", + Computed: true, + }, + "number_of_member_gateways": schema.Int32Attribute{ + MarkdownDescription: "The number of member gateways.", + Computed: true, + }, + "virtual_network_azure_resource": schema.SingleNestedAttribute{ + MarkdownDescription: "The Azure resource of the virtual network.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[virtualNetworkAzureResourceModel](ctx), + Attributes: map[string]schema.Attribute{ + "subscription_id": schema.StringAttribute{ + MarkdownDescription: "The subscription ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "resource_group_name": schema.StringAttribute{ + MarkdownDescription: "The name of the resource group.", + Computed: true, + }, + "virtual_network_name": schema.StringAttribute{ + MarkdownDescription: "The name of the virtual network.", + Computed: true, + }, + "subnet_name": schema.StringAttribute{ + MarkdownDescription: "The name of the subnet.", + Computed: true, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceVirtualNetworkGateway) 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"), + ), + } +} + +// Configure adds the provider configured client to the data source. +func (d *dataSourceVirtualNetworkGateway) 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() +} + +// Read refreshes the Terraform state with the latest data. +func (d *dataSourceVirtualNetworkGateway) 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 datasourceVirtualNetworkGatewayModel + + 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 *dataSourceVirtualNetworkGateway) getByID(ctx context.Context, model *datasourceVirtualNetworkGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, "GET 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 gw, ok := respGet.GatewayClassification.(*fabcore.VirtualNetworkGateway); ok { + model.set(ctx, *gw) + return nil + } + + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + common.ErrorReadHeader, + "expected gateway to be a virtual network gateway", + ), + } +} + +func (d *dataSourceVirtualNetworkGateway) getByDisplayName(ctx context.Context, model *datasourceVirtualNetworkGatewayModel) diag.Diagnostics { + tflog.Trace(ctx, fmt.Sprintf("getting %s by 'display_name'", ItemName)) + + gateways, err := d.client.ListGateways(ctx, nil) + + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationRead, nil); diags.HasError() { + return diags + } + + for _, gw := range gateways { + if virtualNetworkGateway, ok := gw.(*fabcore.VirtualNetworkGateway); ok { + if *virtualNetworkGateway.DisplayName == model.DisplayName.ValueString() { + model.set(ctx, *virtualNetworkGateway) + return nil + } + } + } + + return diag.Diagnostics{diag.NewErrorDiagnostic(common.ErrorReadHeader, "virtual network gateway not found")} +} diff --git a/internal/services/gateway/data_virtual_network_gateway_test.go b/internal/services/gateway/data_virtual_network_gateway_test.go new file mode 100644 index 00000000..f3974a34 --- /dev/null +++ b/internal/services/gateway/data_virtual_network_gateway_test.go @@ -0,0 +1,171 @@ +// 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/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 ( + testDataSourceVirtualNetworkFQN = testhelp.DataSourceFQN("fabric", VirtualNetworkItemTFName, "test") + testVirtualNetworkDataSourceHeader = at.DataSourceHeader(testhelp.TypeName("fabric", VirtualNetworkItemTFName), "test") +) + +func TestUnit_VirtualNetworkGatewayDataSource(t *testing.T) { + entity := fakes.NewRandomVirtualNetworkGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, nil, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - no attributes + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{}, + ), + ExpectError: regexp.MustCompile(`Exactly one of these attributes must be configured: \[id,display_name\]`), + }, + // error - id - invalid UUID + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": "invalid uuid", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // error - unexpected attribute + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": *entity.ID, + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // error - conflicting attributes + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": *entity.ID, + "display_name": *entity.DisplayName, + }, + ), + ExpectError: regexp.MustCompile(`These attributes cannot be configured together: \[id,display_name\]`), + }, + // read by id - not found + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // read by id + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": *entity.ID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceVirtualNetworkFQN, "id", *entity.ID), + resource.TestCheckResourceAttr(testDataSourceVirtualNetworkFQN, "display_name", *entity.DisplayName), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "inactivity_minutes_before_sleep"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "capacity_id"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "number_of_member_gateways"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.subscription_id"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.resource_group_name"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.virtual_network_name"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.subnet_name"), + ), + }, + // read by name - not found + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "display_name": testhelp.RandomName(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + // read by name + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "display_name": *entity.DisplayName, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceVirtualNetworkFQN, "id", *entity.ID), + resource.TestCheckResourceAttr(testDataSourceVirtualNetworkFQN, "display_name", *entity.DisplayName), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "inactivity_minutes_before_sleep"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "capacity_id"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "number_of_member_gateways"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.subscription_id"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.resource_group_name"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.virtual_network_name"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.subnet_name"), + ), + }, + })) +} + +func TestAcc_VirtualNetworkGatewayDataSource(t *testing.T) { + entity := testhelp.WellKnown()["GatewayVirtualNetwork"].(map[string]any) + entityID := entity["id"].(string) + entityDisplayName := entity["displayName"].(string) + + resource.ParallelTest(t, testhelp.NewTestAccCase(t, nil, nil, []resource.TestStep{ + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": entityID, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testDataSourceVirtualNetworkFQN, "id", entityID), + resource.TestCheckResourceAttr(testDataSourceVirtualNetworkFQN, "display_name", entityDisplayName), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "inactivity_minutes_before_sleep"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "capacity_id"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "number_of_member_gateways"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.subscription_id"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.resource_group_name"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.virtual_network_name"), + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkFQN, "virtual_network_azure_resource.subnet_name"), + ), + }, + // read by id - not found + { + Config: at.CompileConfig( + testVirtualNetworkDataSourceHeader, + map[string]any{ + "id": testhelp.RandomUUID(), + }, + ), + ExpectError: regexp.MustCompile(common.ErrorReadHeader), + }, + })) +} diff --git a/internal/services/gateway/data_virtual_network_gateways.go b/internal/services/gateway/data_virtual_network_gateways.go new file mode 100644 index 00000000..39ce3dfe --- /dev/null +++ b/internal/services/gateway/data_virtual_network_gateways.go @@ -0,0 +1,152 @@ +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 = (*dataSourceVirtualNetworkGateways)(nil) + +type dataSourceVirtualNetworkGateways struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewDataSourceVirtualNetworkGateways() datasource.DataSource { + return &dataSourceVirtualNetworkGateways{} +} + +func (d *dataSourceVirtualNetworkGateways) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + VirtualNetworkItemsTFType +} + +func (d *dataSourceVirtualNetworkGateways) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "List all Fabric Virtual Network Gateways.", + Attributes: map[string]schema.Attribute{ + "values": schema.ListNestedAttribute{ + Computed: true, + MarkdownDescription: "A list of Virtual Network Gateways.", + CustomType: supertypes.NewListNestedObjectTypeOf[virtualNetworkGatewayModelBase](ctx), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The gateway ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "display_name": schema.StringAttribute{ + MarkdownDescription: "The display name of the gateway.", + Computed: true, + }, + "capacity_id": schema.StringAttribute{ + MarkdownDescription: "The Fabric license capacity ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "inactivity_minutes_before_sleep": schema.Int64Attribute{ + MarkdownDescription: "Minutes of inactivity before auto-sleep.", + Computed: true, + }, + "number_of_member_gateways": schema.Int64Attribute{ + MarkdownDescription: "The number of member gateways.", + Computed: true, + }, + "virtual_network_azure_resource": schema.SingleNestedAttribute{ + MarkdownDescription: "The Azure resource details for this gateway's Virtual Network.", + Computed: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[virtualNetworkAzureResourceModel](ctx), + Attributes: map[string]schema.Attribute{ + "subscription_id": schema.StringAttribute{ + MarkdownDescription: "The subscription ID.", + Computed: true, + CustomType: customtypes.UUIDType{}, + }, + "resource_group_name": schema.StringAttribute{ + MarkdownDescription: "The name of the resource group.", + Computed: true, + }, + "virtual_network_name": schema.StringAttribute{ + MarkdownDescription: "The name of the virtual network.", + Computed: true, + }, + "subnet_name": schema.StringAttribute{ + MarkdownDescription: "The name of the subnet.", + Computed: true, + }, + }, + }, + }, + }, + }, + "timeouts": timeouts.Attributes(ctx), + }, + } +} + +func (d *dataSourceVirtualNetworkGateways) 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.GatewaysClient)(fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient()) +} + +func (d *dataSourceVirtualNetworkGateways) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Debug(ctx, "READ-ALL-Virtual-Network-Gateways", map[string]any{"action": "start"}) + + var data dataSourceVirtualNetworkGatewaysModel + 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-ALL-Virtual-Network-Gateways", map[string]any{"action": "end"}) +} + +func (d *dataSourceVirtualNetworkGateways) list(ctx context.Context, model *dataSourceVirtualNetworkGatewaysModel) diag.Diagnostics { + tflog.Trace(ctx, "Listing all virtual network gateways") + + gatewaysResp, err := d.client.ListGateways(ctx, nil) + if diags := utils.GetDiagsFromError(ctx, err, utils.OperationList, nil); diags.HasError() { + return diags + } + + return model.setValues(ctx, gatewaysResp) +} diff --git a/internal/services/gateway/data_virtual_network_gateways_test.go b/internal/services/gateway/data_virtual_network_gateways_test.go new file mode 100644 index 00000000..84a64d63 --- /dev/null +++ b/internal/services/gateway/data_virtual_network_gateways_test.go @@ -0,0 +1,74 @@ +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 ( + testDataSourceVirtualNetworkGatewaysFQN = testhelp.DataSourceFQN("fabric", "virtual_network_gateways", "test") + testDataSourceVirtualNetworkGatewaysHeader = at.DataSourceHeader(testhelp.TypeName("fabric", "virtual_network_gateways"), "test") +) + +func TestUnit_VirtualNetworkGatewaysDataSource(t *testing.T) { + entity := fakes.NewRandomVirtualNetworkGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + + resource.ParallelTest(t, testhelp.NewTestUnitCase( + t, + nil, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + // Check that using an unexpected attribute fails. + { + Config: at.CompileConfig( + testDataSourceVirtualNetworkGatewaysHeader, + map[string]any{ + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // A normal read test with an empty config (no filter). + { + Config: at.CompileConfig( + testDataSourceVirtualNetworkGatewaysHeader, + map[string]any{}, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkGatewaysFQN, "values.0.id"), + ), + }, + }, + )) +} + +func TestAcc_VirtualNetworkGatewaysDataSource(t *testing.T) { + resource.ParallelTest(t, testhelp.NewTestAccCase( + t, + nil, + nil, + []resource.TestStep{ + // read test. + { + Config: at.CompileConfig( + testDataSourceVirtualNetworkGatewaysHeader, + map[string]any{}, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet(testDataSourceVirtualNetworkGatewaysFQN, "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..64f8149d --- /dev/null +++ b/internal/services/gateway/fake_test.go @@ -0,0 +1,63 @@ +// 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" +) + +// Replace "Workspace" with "Gateway" in types and function names. +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 + } +} + +// NewRandomGatewayRoleAssignments creates a random GatewayRoleAssignments object for testing. +func NewRandomGatewayRoleAssignments() fabcore.GatewayRoleAssignments { + // Generate random IDs for two role assignments. + assignmentID0 := testhelp.RandomUUID() + assignmentID1 := testhelp.RandomUUID() + principalID0 := testhelp.RandomUUID() + principalID1 := testhelp.RandomUUID() + + return fabcore.GatewayRoleAssignments{ + Value: []fabcore.GatewayRoleAssignment{ + { + ID: azto.Ptr(assignmentID0), + Role: azto.Ptr(fabcore.GatewayRoleAdmin), // assuming GatewayRoleAdmin exists + Principal: &fabcore.Principal{ + ID: azto.Ptr(principalID0), + Type: azto.Ptr(fabcore.PrincipalTypeGroup), + DisplayName: azto.Ptr(testhelp.RandomName()), + GroupDetails: &fabcore.PrincipalGroupDetails{ + GroupType: azto.Ptr(fabcore.GroupTypeSecurityGroup), + }, + }, + }, + { + ID: azto.Ptr(assignmentID1), + Role: azto.Ptr(fabcore.GatewayRoleConnectionCreator), + Principal: &fabcore.Principal{ + ID: azto.Ptr(principalID1), + Type: azto.Ptr(fabcore.PrincipalTypeUser), + DisplayName: azto.Ptr(testhelp.RandomName()), + UserDetails: &fabcore.PrincipalUserDetails{ + UserPrincipalName: azto.Ptr(testhelp.RandomName()), + }, + }, + }, + }, + ContinuationToken: nil, + ContinuationURI: nil, + } +} diff --git a/internal/services/gateway/models_base_data_gateway.go b/internal/services/gateway/models_base_data_gateway.go new file mode 100644 index 00000000..d706454a --- /dev/null +++ b/internal/services/gateway/models_base_data_gateway.go @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "context" + + 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" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type onPremisesGatewayModelBase struct { + ID customtypes.UUID `tfsdk:"id"` + + DisplayName types.String `tfsdk:"display_name"` + + AllowCloudConnectionRefresh types.Bool `tfsdk:"allow_cloud_connection_refresh"` + + AllowCustomConnectors types.Bool `tfsdk:"allow_custom_connectors"` + + LoadBalancingSetting types.String `tfsdk:"load_balancing_setting"` + + NumberOfMemberGateways types.Int32 `tfsdk:"number_of_member_gateways"` + + PublicKey supertypes.SingleNestedObjectValueOf[publicKeyModel] `tfsdk:"public_key"` + + Version types.String `tfsdk:"version"` +} + +type virtualNetworkGatewayModelBase struct { + ID customtypes.UUID `tfsdk:"id"` + + DisplayName types.String `tfsdk:"display_name"` + + CapacityId customtypes.UUID `tfsdk:"capacity_id"` + + InactivityMinutesBeforeSleep types.Int32 `tfsdk:"inactivity_minutes_before_sleep"` + + NumberOfMemberGateways types.Int32 `tfsdk:"number_of_member_gateways"` + + VirtualNetworkAzureResource supertypes.SingleNestedObjectValueOf[virtualNetworkAzureResourceModel] `tfsdk:"virtual_network_azure_resource"` +} + +type onPremisesGatewayPersonalModelBase struct { + ID customtypes.UUID `tfsdk:"id"` + + PublicKey supertypes.SingleNestedObjectValueOf[publicKeyModel] `tfsdk:"public_key"` + + Version types.String `tfsdk:"version"` +} + +type virtualNetworkAzureResourceModel struct { + SubscriptionID customtypes.UUID `tfsdk:"subscription_id"` + + ResourceGroupName types.String `tfsdk:"resource_group_name"` + + VirtualNetworkName types.String `tfsdk:"virtual_network_name"` + + SubnetName types.String `tfsdk:"subnet_name"` +} + +type publicKeyModel struct { + Exponent types.String `tfsdk:"exponent"` + + Modulus types.String `tfsdk:"modulus"` +} + +func (to *onPremisesGatewayPersonalModelBase) set(ctx context.Context, from fabcore.OnPremisesGatewayPersonal) diag.Diagnostics { + var diags diag.Diagnostics + to.ID = customtypes.NewUUIDPointerValue(from.ID) + + publicKey := supertypes.NewSingleNestedObjectValueOfNull[publicKeyModel](ctx) + if from.PublicKey != nil { + publicKeyModel := &publicKeyModel{} + publicKeyModel.set(*from.PublicKey) + + if pkDiags := publicKey.Set(ctx, publicKeyModel); pkDiags.HasError() { + diags.Append(pkDiags...) + return diags + } + } + to.PublicKey = publicKey + + to.Version = types.StringPointerValue(from.Version) + + return diags +} + +func (to *onPremisesGatewayModelBase) set(ctx context.Context, from fabcore.OnPremisesGateway) diag.Diagnostics { + to.ID = customtypes.NewUUIDPointerValue(from.ID) + to.DisplayName = types.StringPointerValue(from.DisplayName) + to.AllowCloudConnectionRefresh = types.BoolPointerValue(from.AllowCloudConnectionRefresh) + to.AllowCustomConnectors = types.BoolPointerValue(from.AllowCustomConnectors) + to.LoadBalancingSetting = types.StringPointerValue((*string)(from.LoadBalancingSetting)) + to.NumberOfMemberGateways = types.Int32PointerValue(from.NumberOfMemberGateways) + to.Version = types.StringPointerValue(from.Version) + + publicKey := supertypes.NewSingleNestedObjectValueOfNull[publicKeyModel](ctx) + + if from.PublicKey != nil { + publicKeyModel := &publicKeyModel{} + publicKeyModel.set(*from.PublicKey) + + if diags := publicKey.Set(ctx, publicKeyModel); diags.HasError() { + return diags + } + } + + to.PublicKey = publicKey + + return nil +} + +func (to *virtualNetworkGatewayModelBase) set(ctx context.Context, from fabcore.VirtualNetworkGateway) diag.Diagnostics { + to.ID = customtypes.NewUUIDPointerValue(from.ID) + to.DisplayName = types.StringPointerValue(from.DisplayName) + to.CapacityId = customtypes.NewUUIDPointerValue(from.CapacityID) + to.InactivityMinutesBeforeSleep = types.Int32PointerValue(from.InactivityMinutesBeforeSleep) + to.NumberOfMemberGateways = types.Int32PointerValue(from.NumberOfMemberGateways) + + virtualNetworkAzureResource := supertypes.NewSingleNestedObjectValueOfNull[virtualNetworkAzureResourceModel](ctx) + + if from.VirtualNetworkAzureResource != nil { + virtualNetworkAzureResourceModel := &virtualNetworkAzureResourceModel{} + virtualNetworkAzureResourceModel.set(*from.VirtualNetworkAzureResource) + + if diags := virtualNetworkAzureResource.Set(ctx, virtualNetworkAzureResourceModel); diags.HasError() { + return diags + } + } + + to.VirtualNetworkAzureResource = virtualNetworkAzureResource + + return nil +} + +func (to *virtualNetworkAzureResourceModel) set(from fabcore.VirtualNetworkAzureResource) { + to.SubscriptionID = customtypes.NewUUIDPointerValue(from.SubscriptionID) + to.ResourceGroupName = types.StringPointerValue(from.ResourceGroupName) + to.VirtualNetworkName = types.StringPointerValue(from.VirtualNetworkName) + to.SubnetName = types.StringPointerValue(from.SubnetName) +} + +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.go b/internal/services/gateway/models_data_gateway.go new file mode 100644 index 00000000..c9c5c586 --- /dev/null +++ b/internal/services/gateway/models_data_gateway.go @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation +// SPDX-License-Identifier: MPL-2.0 + +package gateway + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" +) + +type datasourceOnPremisesGatewayModel struct { + onPremisesGatewayModelBase + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type datasourceVirtualNetworkGatewayModel struct { + virtualNetworkGatewayModelBase + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type datasourceOnPremisesGatewayPersonalModel struct { + onPremisesGatewayPersonalModelBase + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} diff --git a/internal/services/gateway/models_data_gateways.go b/internal/services/gateway/models_data_gateways.go new file mode 100644 index 00000000..36c31ffe --- /dev/null +++ b/internal/services/gateway/models_data_gateways.go @@ -0,0 +1,101 @@ +// 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 dataSourceOnPremisesGatewaysModel struct { + Values supertypes.ListNestedObjectValueOf[onPremisesGatewayModelBase] `tfsdk:"values"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (m *dataSourceOnPremisesGatewaysModel) setValues(ctx context.Context, from []fabcore.GatewayClassification) diag.Diagnostics { + var diags diag.Diagnostics + slice := make([]*onPremisesGatewayModelBase, 0, len(from)) + + for _, classification := range from { + gw, ok := classification.(*fabcore.OnPremisesGateway) + if !ok { + continue // skip non-OnPremisesGateway types + } + + var entityModel onPremisesGatewayModelBase + if setDiags := entityModel.set(ctx, *gw); setDiags.HasError() { + diags.Append(setDiags...) + continue + } + slice = append(slice, &entityModel) + } + + if listDiags := m.Values.Set(ctx, slice); listDiags.HasError() { + diags.Append(listDiags...) + } + return diags +} + +type dataSourceVirtualNetworkGatewaysModel struct { + Values supertypes.ListNestedObjectValueOf[virtualNetworkGatewayModelBase] `tfsdk:"values"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (m *dataSourceVirtualNetworkGatewaysModel) setValues(ctx context.Context, from []fabcore.GatewayClassification) diag.Diagnostics { + var diags diag.Diagnostics + slice := make([]*virtualNetworkGatewayModelBase, 0, len(from)) + + for _, entity := range from { + gw, ok := entity.(*fabcore.VirtualNetworkGateway) + + if !ok { + continue // skip non-VirtualNetworkGateway types + } + + var entityModel virtualNetworkGatewayModelBase + if setDiags := entityModel.set(ctx, *gw); setDiags.HasError() { + diags.Append(setDiags...) + continue + } + slice = append(slice, &entityModel) + } + + if listDiags := m.Values.Set(ctx, slice); listDiags.HasError() { + diags.Append(listDiags...) + } + return diags +} + +type dataSourceOnPremisesGatewayPersonalsModel struct { + Values supertypes.ListNestedObjectValueOf[onPremisesGatewayPersonalModelBase] `tfsdk:"values"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (m *dataSourceOnPremisesGatewayPersonalsModel) setValues(ctx context.Context, from []fabcore.GatewayClassification) diag.Diagnostics { + var diags diag.Diagnostics + slice := make([]*onPremisesGatewayPersonalModelBase, 0, len(from)) + + for _, classification := range from { + gw, ok := classification.(*fabcore.OnPremisesGatewayPersonal) + if !ok { + continue // skip non-OnPremisesGatewayPersonal types + } + + var entityModel onPremisesGatewayPersonalModelBase + if setDiags := entityModel.set(ctx, *gw); setDiags.HasError() { + diags.Append(setDiags...) + continue + } + slice = append(slice, &entityModel) + } + + if listDiags := m.Values.Set(ctx, slice); listDiags.HasError() { + diags.Append(listDiags...) + } + return diags +} diff --git a/internal/services/gateway/models_gateway_data_role_assignment.go b/internal/services/gateway/models_gateway_data_role_assignment.go new file mode 100644 index 00000000..d4e37c47 --- /dev/null +++ b/internal/services/gateway/models_gateway_data_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"` + DisplayName types.String `tfsdk:"display_name"` + 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)) + + return nil +} diff --git a/internal/services/gateway/models_gateway_resource_role_assignment.go b/internal/services/gateway/models_gateway_resource_role_assignment.go new file mode 100644 index 00000000..81520cf0 --- /dev/null +++ b/internal/services/gateway/models_gateway_resource_role_assignment.go @@ -0,0 +1,49 @@ +// 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/models_resource_gateway.go b/internal/services/gateway/models_resource_gateway.go new file mode 100644 index 00000000..f725ba0f --- /dev/null +++ b/internal/services/gateway/models_resource_gateway.go @@ -0,0 +1,78 @@ +// 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" + fabcore "github.com/microsoft/fabric-sdk-go/fabric/core" +) + +type ResourceVirtualNetworkGatewayModel struct { + virtualNetworkGatewayModelBase + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type requestCreateGateway struct { + fabcore.CreateGatewayRequestClassification +} + +type requestUpdateGateway struct { + fabcore.UpdateGatewayRequestClassification +} + +func (to *requestCreateGateway) set(ctx context.Context, from ResourceVirtualNetworkGatewayModel) diag.Diagnostics { + var diags diag.Diagnostics + + gatewayType := 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(), + }, + } + + return nil +} + +func (to *requestUpdateGateway) set(from ResourceVirtualNetworkGatewayModel) diag.Diagnostics { + var diags diag.Diagnostics + + gatewayType := fabcore.GatewayTypeVirtualNetwork + + 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/resource_gateway_role_assignments.go b/internal/services/gateway/resource_gateway_role_assignments.go new file mode 100644 index 00000000..d0323032 --- /dev/null +++ b/internal/services/gateway/resource_gateway_role_assignments.go @@ -0,0 +1,301 @@ +// 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" +) + +var ( + _ resource.ResourceWithConfigure = (*resourceGatewayRoleAssignment)(nil) + _ resource.ResourceWithImportState = (*resourceGatewayRoleAssignment)(nil) +) + +const GatewayRoleAssignmentTFName = "gateway_role_assignment" + +type resourceGatewayRoleAssignment struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewResourceGatewayRoleAssignment() resource.Resource { + return &resourceGatewayRoleAssignment{} +} + +func (r *resourceGatewayRoleAssignment) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + GatewayRoleAssignmentTFName +} + +func (r *resourceGatewayRoleAssignment) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manage a Gateway Role Assignment.\n\n" + + "Assign a role to a principal for a specific gateway.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "The Gateway Role Assignment 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 assigned to 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 Gateway ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "timeouts": timeouts.AttributesAll(ctx), + }, + } +} + +func (r *resourceGatewayRoleAssignment) Configure(ctx 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 + // Create a gateways client using the provider's FabricClient. + r.client = fabcore.NewClientFactoryWithClient(*pConfigData.FabricClient).NewGatewaysClient() +} + +func (r *resourceGatewayRoleAssignment) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + tflog.Debug(ctx, "CREATE GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "start", + }) + tflog.Trace(ctx, "CREATE GATEWAY ROLE ASSIGNMENT", map[string]any{ + "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 GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGatewayRoleAssignment) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Debug(ctx, "READ GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "start", + }) + 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); diags.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 GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGatewayRoleAssignment) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Debug(ctx, "UPDATE GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "start", + }) + 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 GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGatewayRoleAssignment) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + tflog.Debug(ctx, "DELETE GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "start", + }) + 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 GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGatewayRoleAssignment) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Debug(ctx, "IMPORT GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "start", + }) + // Expected import ID format: gatewayID/gatewayRoleAssignmentID + 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 GATEWAY ROLE ASSIGNMENT", map[string]any{ + "action": "end", + }) +} + +func (r *resourceGatewayRoleAssignment) get(ctx context.Context, model *resourceGatewayRoleAssignmentModel) error { + tflog.Trace(ctx, "getting Gateway Role Assignment") + respGet, err := r.client.GetGatewayRoleAssignment(ctx, model.GatewayID.ValueString(), model.ID.ValueString(), nil) + if err != nil { + return err + } + + model.set(respGet.GatewayRoleAssignment) + return nil +} diff --git a/internal/services/gateway/resource_gateway_role_assignments_test.go b/internal/services/gateway/resource_gateway_role_assignments_test.go new file mode 100644 index 00000000..d066cafc --- /dev/null +++ b/internal/services/gateway/resource_gateway_role_assignments_test.go @@ -0,0 +1,197 @@ +// 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" + + "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 ( + testResourceGatewayRoleAssignment = testhelp.ResourceFQN("fabric", "gateway_role_assignment", "test") + testResourceGatewayRoleAssignmentHeader = at.ResourceHeader(testhelp.TypeName("fabric", "gateway_role_assignment"), "test") +) + +func TestUnit_GatewayRoleAssignmentResource_Attributes(t *testing.T) { + resource.ParallelTest(t, testhelp.NewTestUnitCase(t, &testResourceGatewayRoleAssignment, fakes.FakeServer.ServerFactory, nil, []resource.TestStep{ + // error - missing required attribute: gateway_id + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "principal_id": "00000000-0000-0000-0000-000000000000", + "principal_type": "User", + "role": "ConnectionCreator", + }, + ), + ExpectError: regexp.MustCompile(`The argument "gateway_id" is required, but no definition was found.`), + }, + // error - missing required attribute: principal_id + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": "00000000-0000-0000-0000-000000000000", + "principal_type": "User", + "role": "Admin", + }, + ), + ExpectError: regexp.MustCompile(`The argument "principal_id" is required, but no definition was found.`), + }, + // error - missing required attribute: 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": "ConnectionCreator", + }, + ), + ExpectError: regexp.MustCompile(`The argument "principal_type" is required, but no definition was found.`), + }, + // error - missing required attribute: 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": "Admin", + }, + ), + ExpectError: regexp.MustCompile(`The argument "role" is required, but no definition was found.`), + }, + // error - invalid UUID for 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": "ConnectionCreator", + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // error - invalid UUID for 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": "ConnectionCreator", + }, + ), + 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) { + // Assume a well-known gateway is defined in the test environment. + gateway := testhelp.WellKnown()["GatewayVirtualNetwork"].(map[string]any) + gatewayID := gateway["id"].(string) + + // Assume a known principal is available. + principal := testhelp.WellKnown()["Principal"].(map[string]any) + principalID := principal["id"].(string) + principalType := principal["type"].(string) + + resource.Test(t, testhelp.NewTestAccCase(t, &testResourceGatewayRoleAssignment, nil, []resource.TestStep{ + // Create and Read + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": gatewayID, + "principal_id": principalID, + "principal_type": principalType, + "role": "ConnectionCreator", + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "gateway_id", gatewayID), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_id", principalID), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_type", principalType), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "role", "ConnectionCreator"), + ), + }, + // Update and Read + { + ResourceName: testResourceGatewayRoleAssignment, + Config: at.CompileConfig( + testResourceGatewayRoleAssignmentHeader, + map[string]any{ + "gateway_id": gatewayID, + "principal_id": principalID, + "principal_type": principalType, + "role": "Admin", + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "gateway_id", gatewayID), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_id", principalID), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "principal_type", principalType), + resource.TestCheckResourceAttr(testResourceGatewayRoleAssignment, "role", "Admin"), + ), + }, + })) +} diff --git a/internal/services/gateway/resource_virtual_network_gateway.go b/internal/services/gateway/resource_virtual_network_gateway.go new file mode 100644 index 00000000..0d4e8fbe --- /dev/null +++ b/internal/services/gateway/resource_virtual_network_gateway.go @@ -0,0 +1,354 @@ +// 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/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int32planmodifier" + "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-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" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = (*resourceVirtualNetworkGateway)(nil) + _ resource.ResourceWithConfigure = (*resourceVirtualNetworkGateway)(nil) + _ resource.ResourceWithImportState = (*resourceVirtualNetworkGateway)(nil) +) + +type resourceVirtualNetworkGateway struct { + pConfigData *pconfig.ProviderData + client *fabcore.GatewaysClient +} + +func NewResourceVirtualNetworkGateway() resource.Resource { + return &resourceVirtualNetworkGateway{} +} + +func (r *resourceVirtualNetworkGateway) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_" + VirtualNetworkItemTFType +} + +func (r *resourceVirtualNetworkGateway) 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.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(200), + }, + }, + "capacity_id": schema.StringAttribute{ + MarkdownDescription: "The " + ItemName + " capacity ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + }, + "inactivity_minutes_before_sleep": schema.Int32Attribute{ + MarkdownDescription: "The " + ItemName + " inactivity minutes before sleep.", + Required: true, + Validators: []validator.Int32{ + int32validator.OneOf(PossibleInactivityMinutesBeforeSleepValues...), + }, + }, + "number_of_member_gateways": schema.Int32Attribute{ + MarkdownDescription: "The " + ItemName + " number of member gateways.", + Required: true, + Validators: []validator.Int32{ + int32validator.Between(MinNumberOfMemberGatewaysValues, MaxNumberOfMemberGatewaysValues), + }, + PlanModifiers: []planmodifier.Int32{ + int32planmodifier.RequiresReplace(), + }, + }, + "virtual_network_azure_resource": schema.SingleNestedAttribute{ + MarkdownDescription: "The Azure resource of the virtual network.", + Required: true, + CustomType: supertypes.NewSingleNestedObjectTypeOf[virtualNetworkAzureResourceModel](ctx), + Attributes: map[string]schema.Attribute{ + "subscription_id": schema.StringAttribute{ + MarkdownDescription: "The subscription ID.", + Required: true, + CustomType: customtypes.UUIDType{}, + }, + "resource_group_name": schema.StringAttribute{ + MarkdownDescription: "The name of the resource group.", + Required: true, + }, + "virtual_network_name": schema.StringAttribute{ + MarkdownDescription: "The name of the virtual network.", + Required: true, + }, + "subnet_name": schema.StringAttribute{ + MarkdownDescription: "The name of the subnet.", + Required: true, + }, + }, + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplace(), + }, + }, + "timeouts": timeouts.AttributesAll(ctx), + }, + } +} + +func (r *resourceVirtualNetworkGateway) 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 *resourceVirtualNetworkGateway) 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 ResourceVirtualNetworkGatewayModel + + 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 + } + + // get the virtual gateway from the classifiaction + vng := respCreate.GatewayClassification.(*fabcore.VirtualNetworkGateway) + state.set(ctx, *vng) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + tflog.Debug(ctx, "CREATE", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceVirtualNetworkGateway) 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 ResourceVirtualNetworkGatewayModel + + 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); diags.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 *resourceVirtualNetworkGateway) 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 ResourceVirtualNetworkGatewayModel + + 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 + } + + // get the virtual gateway from the classifiaction + vng := respUpdate.GatewayClassification.(*fabcore.VirtualNetworkGateway) + plan.set(ctx, *vng) + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + + tflog.Debug(ctx, "UPDATE", map[string]any{ + "action": "end", + }) + + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resourceVirtualNetworkGateway) 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 ResourceVirtualNetworkGatewayModel + + 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 *resourceVirtualNetworkGateway) 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 *resourceVirtualNetworkGateway) get(ctx context.Context, model *ResourceVirtualNetworkGatewayModel) error { + tflog.Trace(ctx, "getting "+ItemName) + + respGet, err := r.client.GetGateway(ctx, model.ID.ValueString(), nil) + if err != nil { + return err + } + + vng := respGet.GatewayClassification.(*fabcore.VirtualNetworkGateway) + model.set(ctx, *vng) + + return nil +} diff --git a/internal/services/gateway/resource_virtual_network_gateway_test.go b/internal/services/gateway/resource_virtual_network_gateway_test.go new file mode 100644 index 00000000..688f22d1 --- /dev/null +++ b/internal/services/gateway/resource_virtual_network_gateway_test.go @@ -0,0 +1,418 @@ +// 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" + + "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 ( + testResourceVirtualNetworkGatewayFQN = testhelp.ResourceFQN("fabric", VirtualNetworkItemTFName, "test") + testResourceVirtualNetworkGatewayHeader = at.ResourceHeader(testhelp.TypeName("fabric", VirtualNetworkItemTFName), "test") +) + +func TestUnit_VirtualNetworkGatewayResource_Attributes(t *testing.T) { + resource.ParallelTest(t, testhelp.NewTestUnitCase( + t, + &testResourceVirtualNetworkGatewayFQN, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + // Error: Missing required attribute "display_name" + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "capacity_id": "123e4567-e89b-12d3-a456-426614174000", + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + }, + ), + ExpectError: regexp.MustCompile(`The argument "display_name" is required`), + }, + // Error: Unexpected attribute provided. + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": "test gateway", + "capacity_id": "123e4567-e89b-12d3-a456-426614174000", + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + "unexpected_attr": "test", + }, + ), + ExpectError: regexp.MustCompile(`An argument named "unexpected_attr" is not expected here`), + }, + // Error: Invalid UUID for "capacity_id" + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": "test gateway", + "capacity_id": "not-a-valid-uuid", + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + }, + ), + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + // Add a test step for a successful creation/read with all required attributes. + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": "test gateway", + "capacity_id": "123e4567-e89b-12d3-a456-426614174000", + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "display_name", "test gateway"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "capacity_id", "123e4567-e89b-12d3-a456-426614174000"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "inactivity_minutes_before_sleep", "30"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "number_of_member_gateways", "3"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subscription_id", "123e4567-e89b-12d3-a456-426614174001"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.resource_group_name", "test-rg"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.virtual_network_name", "test-vnet"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subnet_name", "test-subnet"), + ), + }, + }, + )) +} + +func TestUnit_VirtualNetworkGatewayResource_ImportState(t *testing.T) { + // Create a fake Virtual Network Gateway. + entity := fakes.NewRandomVirtualNetworkGateway() + + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + fakes.FakeServer.Upsert(entity) + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + + testConfig := at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": *entity.DisplayName, + "capacity_id": *entity.CapacityID, + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + }, + ) + + resource.Test(t, testhelp.NewTestUnitCase( + t, + &testResourceVirtualNetworkGatewayFQN, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: testConfig, + ImportStateId: "not-a-valid-uuid", + ImportState: true, + ExpectError: regexp.MustCompile(customtypes.UUIDTypeErrorInvalidStringHeader), + }, + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: testConfig, + ImportStateId: *entity.ID, + ImportState: true, + ImportStatePersist: true, + ImportStateCheck: func(is []*terraform.InstanceState) error { + // Optionally, add additional state validations here. + if len(is) != 1 { + return errors.New("expected one instance state") + } + + if is[0].ID != *entity.ID { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected ID") + } + + if is[0].Attributes["display_name"] != *entity.DisplayName { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected display_name") + } + + if is[0].Attributes["capacity_id"] != *entity.CapacityID { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected capacity_id") + } + + // Convert inactivity_minutes_before_sleep from string to int32. + inactivityStr := is[0].Attributes["inactivity_minutes_before_sleep"] + inactivityVal, err := strconv.ParseInt(inactivityStr, 10, 32) + if err != nil { + return fmt.Errorf("failed to parse inactivity_minutes_before_sleep: %w", err) + } + if int32(inactivityVal) != *entity.InactivityMinutesBeforeSleep { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected inactivity_minutes_before_sleep") + } + + // Convert number_of_member_gateways from string to int32. + memberGatewaysStr := is[0].Attributes["number_of_member_gateways"] + memberGatewaysVal, err := strconv.ParseInt(memberGatewaysStr, 10, 32) + if err != nil { + return fmt.Errorf("failed to parse number_of_member_gateways: %w", err) + } + if int32(memberGatewaysVal) != *entity.NumberOfMemberGateways { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected number_of_member_gateways") + } + + if is[0].Attributes["virtual_network_azure_resource.subscription_id"] != *entity.VirtualNetworkAzureResource.SubscriptionID { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected virtual_network_azure_resource.subscription_id") + } + + if is[0].Attributes["virtual_network_azure_resource.resource_group_name"] != *entity.VirtualNetworkAzureResource.ResourceGroupName { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected virtual_network_azure_resource.resource_group_name") + } + + if is[0].Attributes["virtual_network_azure_resource.virtual_network_name"] != *entity.VirtualNetworkAzureResource.VirtualNetworkName { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected virtual_network_azure_resource.virtual_network_name") + } + + if is[0].Attributes["virtual_network_azure_resource.subnet_name"] != *entity.VirtualNetworkAzureResource.SubnetName { + return errors.New(testResourceVirtualNetworkGatewayFQN + ": unexpected virtual_network_azure_resource.subnet_name") + } + + return nil + }, + }, + }, + )) +} + +func TestUnit_VirtualNetworkGatewayResource_CRUD(t *testing.T) { + // Create fake entities. + entityExist := fakes.NewRandomVirtualNetworkGateway() + entityBefore := fakes.NewRandomVirtualNetworkGateway() + entityAfter := fakes.NewRandomVirtualNetworkGateway() + + // Upsert some fake virtual network gateways. + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + fakes.FakeServer.Upsert(entityExist) + fakes.FakeServer.Upsert(fakes.NewRandomVirtualNetworkGateway()) + + resource.Test(t, testhelp.NewTestUnitCase( + t, + &testResourceVirtualNetworkGatewayFQN, + fakes.FakeServer.ServerFactory, + nil, + []resource.TestStep{ + // Error: Attempting to create a duplicate gateway (existing entity). + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": *entityExist.DisplayName, + "capacity_id": *entityBefore.CapacityID, + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + }, + ), + ExpectError: regexp.MustCompile(common.ErrorCreateHeader), + }, + // Create and Read + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": *entityBefore.DisplayName, + "capacity_id": *entityBefore.CapacityID, + "inactivity_minutes_before_sleep": 30, + "number_of_member_gateways": 3, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": "123e4567-e89b-12d3-a456-426614174001", + "resource_group_name": "test-rg", + "virtual_network_name": "test-vnet", + "subnet_name": "test-subnet", + }, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testResourceVirtualNetworkGatewayFQN, "display_name", entityBefore.DisplayName), + resource.TestCheckResourceAttrPtr(testResourceVirtualNetworkGatewayFQN, "capacity_id", entityBefore.CapacityID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "inactivity_minutes_before_sleep", "30"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "number_of_member_gateways", "3"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subscription_id", "123e4567-e89b-12d3-a456-426614174001"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.resource_group_name", "test-rg"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.virtual_network_name", "test-vnet"), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subnet_name", "test-subnet"), + ), + }, + // Update and Read + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": *entityAfter.DisplayName, + "capacity_id": *entityAfter.CapacityID, + "inactivity_minutes_before_sleep": int(*entityAfter.InactivityMinutesBeforeSleep), + "number_of_member_gateways": int(*entityAfter.NumberOfMemberGateways), + "virtual_network_azure_resource": map[string]any{ + "subscription_id": *entityAfter.VirtualNetworkAzureResource.SubscriptionID, + "resource_group_name": *entityAfter.VirtualNetworkAzureResource.ResourceGroupName, + "virtual_network_name": *entityAfter.VirtualNetworkAzureResource.VirtualNetworkName, + "subnet_name": *entityAfter.VirtualNetworkAzureResource.SubnetName, + }, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPtr(testResourceVirtualNetworkGatewayFQN, "display_name", entityAfter.DisplayName), + resource.TestCheckResourceAttrPtr(testResourceVirtualNetworkGatewayFQN, "capacity_id", entityAfter.CapacityID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "inactivity_minutes_before_sleep", strconv.Itoa(int(*entityAfter.InactivityMinutesBeforeSleep))), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "number_of_member_gateways", strconv.Itoa(int(*entityAfter.NumberOfMemberGateways))), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subscription_id", *entityAfter.VirtualNetworkAzureResource.SubscriptionID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.resource_group_name", *entityAfter.VirtualNetworkAzureResource.ResourceGroupName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.virtual_network_name", *entityAfter.VirtualNetworkAzureResource.VirtualNetworkName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subnet_name", *entityAfter.VirtualNetworkAzureResource.SubnetName), + ), + }, + }, + )) +} + +func TestAcc_VirtualNetworkGatewayResource_CRUD(t *testing.T) { + // Get well-known test values + capacity := testhelp.WellKnown()["Capacity"].(map[string]any) + capacityID := capacity["id"].(string) + + // Generate random names for testing + entityCreateDisplayName := testhelp.RandomName() + entityUpdateDisplayName := testhelp.RandomName() + + initialVirtualNetworkAzureResource := testhelp.WellKnown()["VirtualNetworkInitial"].(map[string]any) + initSubscriptionID := initialVirtualNetworkAzureResource["subscriptionId"].(string) + initResourceGroupName := initialVirtualNetworkAzureResource["resourceGroupName"].(string) + initVirtualNetworkName := initialVirtualNetworkAzureResource["name"].(string) + initSubnetName := initialVirtualNetworkAzureResource["subnetName"].(string) + + entityInitialInactivityMinutesBeforeSleep := 30 + entityUpdateInactivityMinutesBeforeSleep := 60 + entityInitialNumberOfMemberGateways := int(testhelp.RandomInt32Range(gateway.MinNumberOfMemberGatewaysValues, gateway.MaxNumberOfMemberGatewaysValues)) + entityUpdateNumberOfMemberGateways := int(testhelp.RandomInt32Range(gateway.MinNumberOfMemberGatewaysValues, gateway.MaxNumberOfMemberGatewaysValues)) + + updateVirtualNetworkAzureResource := testhelp.WellKnown()["VirtualNetworkUpdate"].(map[string]any) + updateVirtualNetworkName := updateVirtualNetworkAzureResource["name"].(string) + updateResourceGroupName := updateVirtualNetworkAzureResource["resourceGroupName"].(string) + updateSubnetName := updateVirtualNetworkAzureResource["subnetName"].(string) + updateSubscriptionID := updateVirtualNetworkAzureResource["subscriptionId"].(string) + + resource.Test(t, testhelp.NewTestAccCase(t, &testResourceVirtualNetworkGatewayFQN, nil, []resource.TestStep{ + // Create and Read + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": entityCreateDisplayName, + "capacity_id": capacityID, + "inactivity_minutes_before_sleep": entityInitialInactivityMinutesBeforeSleep, + "number_of_member_gateways": entityInitialNumberOfMemberGateways, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": initSubscriptionID, + "resource_group_name": initResourceGroupName, + "virtual_network_name": initVirtualNetworkName, + "subnet_name": initSubnetName, + }, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "display_name", entityCreateDisplayName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "capacity_id", capacityID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "inactivity_minutes_before_sleep", strconv.Itoa(entityInitialInactivityMinutesBeforeSleep)), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "number_of_member_gateways", strconv.Itoa(entityInitialNumberOfMemberGateways)), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subscription_id", initSubscriptionID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.resource_group_name", initResourceGroupName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.virtual_network_name", initVirtualNetworkName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subnet_name", initSubnetName), + ), + }, + // Update and Read + { + ResourceName: testResourceVirtualNetworkGatewayFQN, + Config: at.CompileConfig( + testResourceVirtualNetworkGatewayHeader, + map[string]any{ + "display_name": entityUpdateDisplayName, + "capacity_id": capacityID, + "inactivity_minutes_before_sleep": entityUpdateInactivityMinutesBeforeSleep, + "number_of_member_gateways": entityUpdateNumberOfMemberGateways, + "virtual_network_azure_resource": map[string]any{ + "subscription_id": updateSubscriptionID, + "resource_group_name": updateResourceGroupName, + "virtual_network_name": updateVirtualNetworkName, + "subnet_name": updateSubnetName, + }, + }, + ), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "display_name", entityUpdateDisplayName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "capacity_id", capacityID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "inactivity_minutes_before_sleep", strconv.Itoa(entityUpdateInactivityMinutesBeforeSleep)), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "number_of_member_gateways", strconv.Itoa(entityUpdateNumberOfMemberGateways)), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subscription_id", updateSubscriptionID), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.resource_group_name", updateResourceGroupName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.virtual_network_name", updateVirtualNetworkName), + resource.TestCheckResourceAttr(testResourceVirtualNetworkGatewayFQN, "virtual_network_azure_resource.subnet_name", updateSubnetName), + ), + }, + })) +} diff --git a/internal/testhelp/fakes/fabric_gateway.go b/internal/testhelp/fakes/fabric_gateway.go new file mode 100644 index 00000000..fb20f22f --- /dev/null +++ b/internal/testhelp/fakes/fabric_gateway.go @@ -0,0 +1,203 @@ +// 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" + + "github.com/microsoft/terraform-provider-fabric/internal/services/gateway" + "github.com/microsoft/terraform-provider-fabric/internal/testhelp" +) + +// operationsGateway implements SimpleIDOperations. +type operationsGateway struct{} + +// Create implements concreteEntityOperations. +func (o *operationsGateway) Create(data fabcore.CreateGatewayRequestClassification) fabcore.GatewayClassification { + switch gateway := data.(type) { + case *fabcore.CreateVirtualNetworkGatewayRequest: + returnGateway := NewRandomVirtualNetworkGateway() + returnGateway.DisplayName = gateway.DisplayName + returnGateway.CapacityID = gateway.CapacityID + returnGateway.InactivityMinutesBeforeSleep = gateway.InactivityMinutesBeforeSleep + returnGateway.NumberOfMemberGateways = gateway.NumberOfMemberGateways + returnGateway.VirtualNetworkAzureResource = gateway.VirtualNetworkAzureResource + return returnGateway + default: + panic("unimplemented") + } +} + +// GetID implements concreteEntityOperations. +func (o *operationsGateway) GetID(entity fabcore.GatewayClassification) string { + return *entity.GetGateway().ID +} + +// TransformGet implements concreteEntityOperations. +func (o *operationsGateway) TransformGet(entity fabcore.GatewayClassification) fabcore.GatewaysClientGetGatewayResponse { + return fabcore.GatewaysClientGetGatewayResponse{ + GatewayClassification: entity, + } +} + +// TransformList implements concreteEntityOperations. +func (o *operationsGateway) TransformList(list []fabcore.GatewayClassification) fabcore.GatewaysClientListGatewaysResponse { + return fabcore.GatewaysClientListGatewaysResponse{ + ListGatewaysResponse: fabcore.ListGatewaysResponse{ + Value: list, + }, + } +} + +// TransformUpdate implements concreteEntityOperations. +func (o *operationsGateway) TransformUpdate(entity fabcore.GatewayClassification) fabcore.GatewaysClientUpdateGatewayResponse { + return fabcore.GatewaysClientUpdateGatewayResponse{ + GatewayClassification: entity, + } +} + +// Update implements concreteEntityOperations. +func (o *operationsGateway) Update(base fabcore.GatewayClassification, data fabcore.UpdateGatewayRequestClassification) fabcore.GatewayClassification { + switch request := data.(type) { + case *fabcore.UpdateVirtualNetworkGatewayRequest: + gateway, _ := base.(*fabcore.VirtualNetworkGateway) + gateway.CapacityID = request.CapacityID + gateway.DisplayName = request.DisplayName + gateway.InactivityMinutesBeforeSleep = request.InactivityMinutesBeforeSleep + gateway.NumberOfMemberGateways = request.NumberOfMemberGateways + return gateway + case *fabcore.UpdateOnPremisesGatewayRequest: + gateway, _ := base.(*fabcore.OnPremisesGateway) + gateway.AllowCloudConnectionRefresh = request.AllowCloudConnectionRefresh + gateway.AllowCustomConnectors = request.AllowCustomConnectors + gateway.LoadBalancingSetting = request.LoadBalancingSetting + gateway.DisplayName = request.DisplayName + return gateway + default: + panic("unimplemented") + } +} + +// Validate implements concreteEntityOperations. +func (o *operationsGateway) Validate(newEntity fabcore.GatewayClassification, existing []fabcore.GatewayClassification) (statusCode int, err error) { + for _, existingGateway := range existing { + switch gateway := newEntity.(type) { + case *fabcore.VirtualNetworkGateway: + vng := existingGateway.(*fabcore.VirtualNetworkGateway) + if *vng.DisplayName == *gateway.DisplayName { + return http.StatusConflict, fabfake.SetResponseError(http.StatusConflict, fabcore.ErrWorkspace.WorkspaceNameAlreadyExists.Error(), fabcore.ErrWorkspace.WorkspaceNameAlreadyExists.Error()) + } + case *fabcore.OnPremisesGateway: + opg := existingGateway.(*fabcore.OnPremisesGateway) + if *opg.DisplayName == *gateway.DisplayName { + return http.StatusConflict, fabfake.SetResponseError(http.StatusConflict, fabcore.ErrWorkspace.WorkspaceNameAlreadyExists.Error(), fabcore.ErrWorkspace.WorkspaceNameAlreadyExists.Error()) + } + } + } + + return http.StatusCreated, nil +} + +// TransformCreate implements concreteEntityOperations. +func (o *operationsGateway) TransformCreate(entity fabcore.GatewayClassification) fabcore.GatewaysClientCreateGatewayResponse { + return fabcore.GatewaysClientCreateGatewayResponse{ + GatewayClassification: entity, + } +} + +func configureVirtualNetworkGateway(server *fakeServer) fabcore.VirtualNetworkGateway { + configureGatewayClassification(server) + + return fabcore.VirtualNetworkGateway{} +} + +func configureOnPremisesGatewayPersonal(server *fakeServer) fabcore.OnPremisesGatewayPersonal { + configureGatewayClassification(server) + + return fabcore.OnPremisesGatewayPersonal{} +} + +func configureOnPremisesGateway(server *fakeServer) fabcore.OnPremisesGateway { + configureGatewayClassification(server) + + return fabcore.OnPremisesGateway{} +} + +func configureGatewayClassification(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) + + handleGetWithSimpleID(handler, entityOperations, &handler.ServerFactory.Core.GatewaysServer.GetGateway) + handleUpdateWithSimpleID(handler, entityOperations, entityOperations, &handler.ServerFactory.Core.GatewaysServer.UpdateGateway) + handleCreate(handler, entityOperations, entityOperations, entityOperations, &handler.ServerFactory.Core.GatewaysServer.CreateGateway) + handleDeleteWithSimpleID(handler, &handler.ServerFactory.Core.GatewaysServer.DeleteGateway) + + handleListPager( + handler, + entityOperations, + &handler.ServerFactory.Core.GatewaysServer.NewListGatewaysPager) +} + +func NewRandomVirtualNetworkGateway() *fabcore.VirtualNetworkGateway { + return &fabcore.VirtualNetworkGateway{ + ID: to.Ptr(testhelp.RandomUUID()), + DisplayName: to.Ptr(testhelp.RandomName()), + InactivityMinutesBeforeSleep: to.Ptr(testhelp.RandomElement(gateway.PossibleInactivityMinutesBeforeSleepValues)), + NumberOfMemberGateways: to.Ptr(testhelp.RandomInt32Max(7)), + CapacityID: to.Ptr(testhelp.RandomUUID()), + Type: to.Ptr(fabcore.GatewayTypeVirtualNetwork), + VirtualNetworkAzureResource: &fabcore.VirtualNetworkAzureResource{ + SubscriptionID: to.Ptr(testhelp.RandomUUID()), + ResourceGroupName: to.Ptr(testhelp.RandomName()), + VirtualNetworkName: to.Ptr(testhelp.RandomName()), + SubnetName: to.Ptr(testhelp.RandomName()), + }, + } +} + +func NewRandomOnPremisesGateway() *fabcore.OnPremisesGateway { + return &fabcore.OnPremisesGateway{ + ID: to.Ptr(testhelp.RandomUUID()), + DisplayName: to.Ptr(testhelp.RandomName()), + NumberOfMemberGateways: to.Ptr(testhelp.RandomInt32Max(7)), + Type: to.Ptr(fabcore.GatewayTypeOnPremises), + AllowCloudConnectionRefresh: to.Ptr(true), + AllowCustomConnectors: to.Ptr(false), + LoadBalancingSetting: to.Ptr(fabcore.LoadBalancingSettingDistributeEvenly), + PublicKey: &fabcore.PublicKey{ + Exponent: to.Ptr(testhelp.RandomName()), + Modulus: to.Ptr(testhelp.RandomName()), + }, + Version: to.Ptr("1.0"), + } +} + +func NewRandomOnPremisesGatewayPersonal() *fabcore.OnPremisesGatewayPersonal { + return &fabcore.OnPremisesGatewayPersonal{ + ID: to.Ptr(testhelp.RandomUUID()), + Type: to.Ptr(fabcore.GatewayTypeOnPremisesPersonal), + Version: to.Ptr("1.0"), + PublicKey: &fabcore.PublicKey{ + Exponent: to.Ptr(testhelp.RandomName()), + Modulus: to.Ptr(testhelp.RandomName()), + }, + } +} diff --git a/internal/testhelp/fakes/fake_server.go b/internal/testhelp/fakes/fake_server.go index b6f671e9..d1ae4a26 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, configureOnPremisesGateway) + handleEntity(server, configureOnPremisesGatewayPersonal) + handleEntity(server, configureVirtualNetworkGateway) 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,12 @@ 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/utils.go b/internal/testhelp/utils.go index 167b1d45..656e7db1 100644 --- a/internal/testhelp/utils.go +++ b/internal/testhelp/utils.go @@ -9,6 +9,7 @@ import ( "encoding/pem" "errors" "fmt" + "math/rand" "strings" "github.com/hashicorp/go-uuid" @@ -50,6 +51,18 @@ func RandomP12Cert(password string) []byte { return p12 } +func RandomInt32Max(max int32) int32 { + return rand.Int31n(max) +} + +func RandomInt32Range(min int32, max int32) int32 { + return min + rand.Int31n(max-min+1) +} + +func RandomElement[T any](slice []T) T { + return slice[rand.Intn(len(slice))] +} + func createP12Bundle(certPEMStr, privateKeyPEMStr, password string) ([]byte, error) { // Decode the private key PEM block block, _ := pem.Decode([]byte(privateKeyPEMStr)) diff --git a/tools/scripts/Set-WellKnown.ps1 b/tools/scripts/Set-WellKnown.ps1 index 15c22db8..80dee62e 100644 --- a/tools/scripts/Set-WellKnown.ps1 +++ b/tools/scripts/Set-WellKnown.ps1 @@ -474,8 +474,152 @@ 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' + $result = Invoke-FabricRest -Method 'POST' -Endpoint "gateways" -Payload $payload + } + + 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 + $vnet = Get-AzVirtualNetwork -Name $VNetName -ResourceGroupName $ResourceGroupName + if (!$vnet) { + # 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' + return $vnet + } + + # 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)" + return $vnet +} + # Define an array of modules to install -$modules = @('Az.Accounts', 'Az.Resources', 'Az.Fabric', 'pwsh-dotenv', 'ADOPS') +$modules = @('Az.Accounts', 'Az.Network', 'Az.Resources', 'Az.Fabric', 'pwsh-dotenv', 'ADOPS') # Loop through each module and install if not installed foreach ($module in $modules) { @@ -488,8 +632,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 @@ -521,7 +665,7 @@ $capacity = $capacities.Response.value | Where-Object { $_.displayName -eq $Env: if (!$capacity) { Write-Log -Message "Fabric Capacity: $($Env:FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME)" } -Write-Log -Message "Fabric Capacity - Name: $($Env:FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME) / ID: $($capacity.id)" +Write-Log -Message "Fabric Capacity - Name: $($Env:FABRIC_TESTACC_WELLKNOWN_FABRIC_CAPACITY_NAME) / ID: $($capacity.ID)" $wellKnown['Capacity'] = @{ id = $capacity.id displayName = $capacity.displayName @@ -535,6 +679,7 @@ $itemNaming = @{ 'Environment' = 'env' 'Eventhouse' = 'eh' 'Eventstream' = 'es' + 'GatewayVirtualNetwork' = 'gvn' 'KQLDashboard' = 'kqldash' 'KQLDatabase' = 'kqldb' 'KQLQueryset' = 'kqlqs' @@ -559,6 +704,9 @@ $itemNaming = @{ 'EntraServicePrincipal' = 'sp' 'EntraGroup' = 'grp' 'AzDOProject' = 'proj' + 'VirtualNetworkSubnet' = 'subnet' + 'VirtualNetworkInitial' = 'vneti' + 'VirtualNetworkUpdate' = 'vnetu' } $baseName = Get-BaseName @@ -568,6 +716,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', @@ -660,7 +810,7 @@ $definition = @{ parts = @( @{ path = "mirroring.json" - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/mirrored_database/mirroring.json' + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/mirrored_database/mirroring.json' payloadType = 'InlineBase64' } ) @@ -678,12 +828,12 @@ $definition = @{ parts = @( @{ path = 'definition.pbism' - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/semantic_model_tmsl/definition.pbism' + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/semantic_model_tmsl/definition.pbism' payloadType = 'InlineBase64' } @{ path = 'model.bim' - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/semantic_model_tmsl/model.bim.tmpl' -Values @(@{ key = '{{ .ColumnName }}'; value = 'ColumnTest1' }) + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/semantic_model_tmsl/model.bim.tmpl' -Values @(@{ key = '{{ .ColumnName }}'; value = 'ColumnTest1' }) payloadType = 'InlineBase64' } ) @@ -701,22 +851,22 @@ $definition = @{ parts = @( @{ path = 'definition.pbir' - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/report_pbir_legacy/definition.pbir.tmpl' -Values @(@{ key = '{{ .SemanticModelID }}'; value = $semanticModel.id }) + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/report_pbir_legacy/definition.pbir.tmpl' -Values @(@{ key = '{{ .SemanticModelID }}'; value = $semanticModel.id }) payloadType = 'InlineBase64' }, @{ path = 'report.json' - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/report_pbir_legacy/report.json' + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/report_pbir_legacy/report.json' payloadType = 'InlineBase64' }, @{ path = 'StaticResources/SharedResources/BaseThemes/CY24SU10.json' - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/report_pbir_legacy/StaticResources/SharedResources/BaseThemes/CY24SU10.json' + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/report_pbir_legacy/StaticResources/SharedResources/BaseThemes/CY24SU10.json' payloadType = 'InlineBase64' } @{ path = 'StaticResources/RegisteredResources/fabric_48_color10148978481469717.png' - payload = Get-DefinitionPartBase64 -Path 'internal/testhelp/fixtures/report_pbir_legacy/StaticResources/RegisteredResources/fabric_48_color10148978481469717.png' + payload = Get-DefinitionPartBase64 -Path './internal/testhelp/fixtures/report_pbir_legacy/StaticResources/RegisteredResources/fabric_48_color10148978481469717.png' payloadType = 'InlineBase64' } ) @@ -746,6 +896,76 @@ $wellKnown['DomainChild'] = @{ description = $childDomain.description } + +# Register the Microsoft.PowerPlatform resource provider +Write-Log -Message "Registering Microsoft.PowerPlatform resource provider" -Level 'WARN' +Register-AzResourceProvider -ProviderNamespace "Microsoft.PowerPlatform" + +# Create Azure initial Virtual Network if not exists +$vnetName = "${displayName}_$($itemNaming['VirtualNetworkInitial'])" +$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['VirtualNetworkInitial'] = @{ + name = $vnet.Name + resourceGroupName = $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_RESOURCE_GROUP_NAME + subnetName = $subName + subscriptionId = $Env:FABRIC_TESTACC_WELLKNOWN_AZURE_SUBSCRIPTION_ID +} + + +# Create Azure update Virtual Network if not exists +$vnetName = "${displayName}_$($itemNaming['VirtualNetworkUpdate'])" +$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['VirtualNetworkUpdate'] = @{ + 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['VirtualNetworkInitial'].name ` + -SubnetName $wellKnown['VirtualNetworkInitial'].subnetName + +$wellKnown['GatewayVirtualNetwork'] = @{ + id = $gateway.id + displayName = $gateway.displayName + type = $gateway.type + description = $gateway.description +} + $results = Invoke-FabricRest -Method 'GET' -Endpoint "workspaces/$($workspace.id)/lakehouses/$($wellKnown['Lakehouse']['id'])/tables" $result = $results.Response.data | Where-Object { $_.name -eq 'publicholidays' } if (!$result) {