From 64c318b9a461a2c30155496d6304725f89342811 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Tue, 22 Nov 2022 15:50:01 +0800 Subject: [PATCH 1/7] New Resource: azurerm_route_map --- internal/services/network/client/client.go | 5 + internal/services/network/parse/route_map.go | 75 +++ .../services/network/parse/route_map_test.go | 128 ++++ internal/services/network/registration.go | 20 + internal/services/network/resourceids.go | 1 + .../services/network/route_map_resource.go | 605 ++++++++++++++++++ .../services/network/validate/route_map_id.go | 23 + .../network/validate/route_map_id_test.go | 88 +++ 8 files changed, 945 insertions(+) create mode 100644 internal/services/network/parse/route_map.go create mode 100644 internal/services/network/parse/route_map_test.go create mode 100644 internal/services/network/route_map_resource.go create mode 100644 internal/services/network/validate/route_map_id.go create mode 100644 internal/services/network/validate/route_map_id_test.go diff --git a/internal/services/network/client/client.go b/internal/services/network/client/client.go index f8d5ef234eab..08cdefc1f2b0 100644 --- a/internal/services/network/client/client.go +++ b/internal/services/network/client/client.go @@ -32,6 +32,7 @@ type Client struct { PrivateEndpointClient *network.PrivateEndpointsClient PublicIPsClient *network.PublicIPAddressesClient PublicIPPrefixesClient *network.PublicIPPrefixesClient + RouteMapsClient *network.RouteMapsClient RoutesClient *network.RoutesClient RouteFiltersClient *network.RouteFiltersClient RouteTablesClient *network.RouteTablesClient @@ -158,6 +159,9 @@ func NewClient(o *common.ClientOptions) *Client { PrivateLinkServiceClient := network.NewPrivateLinkServicesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&PrivateLinkServiceClient.Client, o.ResourceManagerAuthorizer) + RouteMapsClient := network.NewRouteMapsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&RouteMapsClient.Client, o.ResourceManagerAuthorizer) + RoutesClient := network.NewRoutesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&RoutesClient.Client, o.ResourceManagerAuthorizer) @@ -260,6 +264,7 @@ func NewClient(o *common.ClientOptions) *Client { PrivateEndpointClient: &PrivateEndpointClient, PublicIPsClient: &PublicIPsClient, PublicIPPrefixesClient: &PublicIPPrefixesClient, + RouteMapsClient: &RouteMapsClient, RoutesClient: &RoutesClient, RouteFiltersClient: &RouteFiltersClient, RouteTablesClient: &RouteTablesClient, diff --git a/internal/services/network/parse/route_map.go b/internal/services/network/parse/route_map.go new file mode 100644 index 000000000000..8df9f549cfe9 --- /dev/null +++ b/internal/services/network/parse/route_map.go @@ -0,0 +1,75 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +type RouteMapId struct { + SubscriptionId string + ResourceGroup string + VirtualHubName string + Name string +} + +func NewRouteMapID(subscriptionId, resourceGroup, virtualHubName, name string) RouteMapId { + return RouteMapId{ + SubscriptionId: subscriptionId, + ResourceGroup: resourceGroup, + VirtualHubName: virtualHubName, + Name: name, + } +} + +func (id RouteMapId) String() string { + segments := []string{ + fmt.Sprintf("Name %q", id.Name), + fmt.Sprintf("Virtual Hub Name %q", id.VirtualHubName), + fmt.Sprintf("Resource Group %q", id.ResourceGroup), + } + segmentsStr := strings.Join(segments, " / ") + return fmt.Sprintf("%s: (%s)", "Route Map", segmentsStr) +} + +func (id RouteMapId) ID() string { + fmtString := "/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualHubs/%s/routeMaps/%s" + return fmt.Sprintf(fmtString, id.SubscriptionId, id.ResourceGroup, id.VirtualHubName, id.Name) +} + +// RouteMapID parses a RouteMap ID into an RouteMapId struct +func RouteMapID(input string) (*RouteMapId, error) { + id, err := resourceids.ParseAzureResourceID(input) + if err != nil { + return nil, err + } + + resourceId := RouteMapId{ + SubscriptionId: id.SubscriptionID, + ResourceGroup: id.ResourceGroup, + } + + if resourceId.SubscriptionId == "" { + return nil, fmt.Errorf("ID was missing the 'subscriptions' element") + } + + if resourceId.ResourceGroup == "" { + return nil, fmt.Errorf("ID was missing the 'resourceGroups' element") + } + + if resourceId.VirtualHubName, err = id.PopSegment("virtualHubs"); err != nil { + return nil, err + } + if resourceId.Name, err = id.PopSegment("routeMaps"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &resourceId, nil +} diff --git a/internal/services/network/parse/route_map_test.go b/internal/services/network/parse/route_map_test.go new file mode 100644 index 000000000000..a6359c5b7987 --- /dev/null +++ b/internal/services/network/parse/route_map_test.go @@ -0,0 +1,128 @@ +package parse + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "testing" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = RouteMapId{} + +func TestRouteMapIDFormatter(t *testing.T) { + actual := NewRouteMapID("12345678-1234-9876-4563-123456789012", "resGroup1", "vhub1", "routeMap1").ID() + expected := "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/routeMaps/routeMap1" + if actual != expected { + t.Fatalf("Expected %q but got %q", expected, actual) + } +} + +func TestRouteMapID(t *testing.T) { + testData := []struct { + Input string + Error bool + Expected *RouteMapId + }{ + + { + // empty + Input: "", + Error: true, + }, + + { + // missing SubscriptionId + Input: "/", + Error: true, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Error: true, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Error: true, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Error: true, + }, + + { + // missing VirtualHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/", + Error: true, + }, + + { + // missing value for VirtualHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/", + Error: true, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/", + Error: true, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/routeMaps/", + Error: true, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/routeMaps/routeMap1", + Expected: &RouteMapId{ + SubscriptionId: "12345678-1234-9876-4563-123456789012", + ResourceGroup: "resGroup1", + VirtualHubName: "vhub1", + Name: "routeMap1", + }, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.NETWORK/VIRTUALHUBS/VHUB1/ROUTEMAPS/ROUTEMAP1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Input) + + actual, err := RouteMapID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expect a value but got an error: %s", err) + } + if v.Error { + t.Fatal("Expect an error but didn't get one") + } + + if actual.SubscriptionId != v.Expected.SubscriptionId { + t.Fatalf("Expected %q but got %q for SubscriptionId", v.Expected.SubscriptionId, actual.SubscriptionId) + } + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for ResourceGroup", v.Expected.ResourceGroup, actual.ResourceGroup) + } + if actual.VirtualHubName != v.Expected.VirtualHubName { + t.Fatalf("Expected %q but got %q for VirtualHubName", v.Expected.VirtualHubName, actual.VirtualHubName) + } + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + } +} diff --git a/internal/services/network/registration.go b/internal/services/network/registration.go index 87082130a4a8..3d776506068b 100644 --- a/internal/services/network/registration.go +++ b/internal/services/network/registration.go @@ -1,11 +1,31 @@ package network import ( + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" ) type Registration struct{} +var ( + _ sdk.TypedServiceRegistration = Registration{} + _ sdk.UntypedServiceRegistrationWithAGitHubLabel = Registration{} +) + +func (r Registration) AssociatedGitHubLabel() string { + return "service/network" +} + +func (r Registration) DataSources() []sdk.DataSource { + return []sdk.DataSource{} +} + +func (r Registration) Resources() []sdk.Resource { + return []sdk.Resource{ + RouteMapResource{}, + } +} + // Name is the name of this Service func (r Registration) Name() string { return "Network" diff --git a/internal/services/network/resourceids.go b/internal/services/network/resourceids.go index 2771982832a5..e9ce9a885d4c 100644 --- a/internal/services/network/resourceids.go +++ b/internal/services/network/resourceids.go @@ -79,6 +79,7 @@ package network //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=VpnServerConfigurationPolicyGroup -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/vpnServerConfigurations/serverConfiguration1/configurationPolicyGroups/configurationPolicyGroup1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=VpnSite -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/vpnSites/vpnSite1 //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=VpnSiteLink -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/vpnSites/vpnSite1/vpnSiteLinks/vpnSiteLink1 +//go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=RouteMap -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/routeMaps/routeMap1 // Subnet Service Endpoint Policy //go:generate go run ../../tools/generator-resource-id/main.go -path=./ -name=SubnetServiceEndpointStoragePolicy -id=/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/serviceEndpointPolicies/policy1 diff --git a/internal/services/network/route_map_resource.go b/internal/services/network/route_map_resource.go new file mode 100644 index 000000000000..37803d0f3014 --- /dev/null +++ b/internal/services/network/route_map_resource.go @@ -0,0 +1,605 @@ +package network + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" + "github.com/hashicorp/terraform-provider-azurerm/utils" + "github.com/tombuildsstuff/kermit/sdk/network/2022-05-01/network" +) + +type RouteMapModel struct { + Name string `tfschema:"name"` + VirtualHubId string `tfschema:"virtual_hub_id"` + AssociatedInboundConnections []string `tfschema:"associated_inbound_connections"` + AssociatedOutboundConnections []string `tfschema:"associated_outbound_connections"` + Rules []Rule `tfschema:"rules"` +} + +type Rule struct { + Actions []Action `tfschema:"actions"` + MatchCriteria []Criterion `tfschema:"match_criteria"` + Name string `tfschema:"name"` + NextStepIfMatched network.NextStep `tfschema:"next_step_if_matched"` +} + +type Action struct { + Parameters []Parameter `tfschema:"parameters"` + Type network.RouteMapActionType `tfschema:"type"` +} + +type Parameter struct { + AsPath []string `tfschema:"as_path"` + Community []string `tfschema:"community"` + RoutePrefix []string `tfschema:"route_prefix"` +} + +type Criterion struct { + AsPath []string `tfschema:"as_path"` + Community []string `tfschema:"community"` + MatchCondition network.RouteMapMatchCondition `tfschema:"match_condition"` + RoutePrefix []string `tfschema:"route_prefix"` +} + +type RouteMapResource struct{} + +var _ sdk.ResourceWithUpdate = RouteMapResource{} + +func (r RouteMapResource) ResourceType() string { + return "azurerm_route_map" +} + +func (r RouteMapResource) ModelObject() interface{} { + return &RouteMapModel{} +} + +func (r RouteMapResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.RouteMapID +} + +func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "virtual_hub_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.VirtualHubID, + }, + + "associated_inbound_connections": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "associated_outbound_connections": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "rules": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "actions": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "parameters": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "as_path": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "community": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "route_prefix": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + + "type": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.RouteMapActionTypeReplace), + string(network.RouteMapActionTypeDrop), + string(network.RouteMapActionTypeUnknown), + string(network.RouteMapActionTypeRemove), + string(network.RouteMapActionTypeAdd), + }, false), + }, + }, + }, + }, + + "match_criteria": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "as_path": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "community": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + + "match_condition": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.RouteMapMatchConditionEquals), + string(network.RouteMapMatchConditionNotContains), + string(network.RouteMapMatchConditionNotEquals), + string(network.RouteMapMatchConditionUnknown), + string(network.RouteMapMatchConditionContains), + }, false), + }, + + "route_prefix": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Schema{ + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + }, + }, + }, + }, + + "name": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "next_step_if_matched": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.NextStepUnknown), + string(network.NextStepContinue), + string(network.NextStepTerminate), + }, false), + }, + }, + }, + }, + } +} + +func (r RouteMapResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r RouteMapResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var model RouteMapModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + client := metadata.Client.Network.RouteMapsClient + virtualHubId, err := parse.VirtualHubID(model.VirtualHubId) + if err != nil { + return err + } + + id := parse.NewRouteMapID(virtualHubId.SubscriptionId, virtualHubId.ResourceGroup, virtualHubId.Name, model.Name) + existing, err := client.Get(ctx, id.ResourceGroup, id.VirtualHubName, id.Name) + if err != nil && !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + if !utils.ResponseWasNotFound(existing.Response) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + props := &network.RouteMap{ + RouteMapProperties: &network.RouteMapProperties{ + AssociatedInboundConnections: &model.AssociatedInboundConnections, + AssociatedOutboundConnections: &model.AssociatedOutboundConnections, + }, + } + + rules, err := expandRules(model.Rules) + if err != nil { + return err + } + props.RouteMapProperties.Rules = rules + + future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.VirtualHubName, id.Name, *props) + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for creation of %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r RouteMapResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Network.RouteMapsClient + + id, err := parse.RouteMapID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var model RouteMapModel + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + existing, err := client.Get(ctx, id.ResourceGroup, id.VirtualHubName, id.Name) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + if metadata.ResourceData.HasChange("associated_inbound_connections") { + existing.RouteMapProperties.AssociatedInboundConnections = &model.AssociatedInboundConnections + } + + if metadata.ResourceData.HasChange("associated_outbound_connections") { + existing.RouteMapProperties.AssociatedOutboundConnections = &model.AssociatedOutboundConnections + } + + if metadata.ResourceData.HasChange("rules") { + rules, err := expandRules(model.Rules) + if err != nil { + return err + } + existing.RouteMapProperties.Rules = rules + } + + future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.VirtualHubName, id.Name, existing) + if err != nil { + return fmt.Errorf("updating %s: %+v", *id, err) + } + + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for update to %s: %+v", *id, err) + } + + return nil + }, + } +} + +func (r RouteMapResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Network.RouteMapsClient + + id, err := parse.RouteMapID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + resp, err := client.Get(ctx, id.ResourceGroup, id.VirtualHubName, id.Name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return metadata.MarkAsGone(id) + } + + return fmt.Errorf("retrieving %s: %+v", *id, err) + } + + state := RouteMapModel{ + Name: id.Name, + VirtualHubId: parse.NewVirtualHubID(id.SubscriptionId, id.ResourceGroup, id.VirtualHubName).ID(), + } + + if props := resp.RouteMapProperties; props != nil { + if props.AssociatedInboundConnections != nil { + state.AssociatedInboundConnections = *props.AssociatedInboundConnections + } + + if props.AssociatedOutboundConnections != nil { + state.AssociatedOutboundConnections = *props.AssociatedOutboundConnections + } + + rules, err := flattenRules(props.Rules) + if err != nil { + return err + } + state.Rules = rules + } + + return metadata.Encode(&state) + }, + } +} + +func (r RouteMapResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Network.RouteMapsClient + + id, err := parse.RouteMapID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + future, err := client.Delete(ctx, id.ResourceGroup, id.VirtualHubName, id.Name) + if err != nil { + return fmt.Errorf("deleting %s: %+v", *id, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for the deletion of %s: %+v", *id, err) + } + + return nil + }, + } +} + +func expandRules(input []Rule) (*[]network.RouteMapRule, error) { + var rules []network.RouteMapRule + + for _, v := range input { + rule := network.RouteMapRule{ + NextStepIfMatched: v.NextStepIfMatched, + } + + actions, err := expandActions(v.Actions) + if err != nil { + return nil, err + } + rule.Actions = actions + + matchCriteria, err := expandCriterions(v.MatchCriteria) + if err != nil { + return nil, err + } + rule.MatchCriteria = matchCriteria + + if v.Name != "" { + rule.Name = &v.Name + } + + rules = append(rules, rule) + } + + return &rules, nil +} + +func expandActions(input []Action) (*[]network.Action, error) { + var actions []network.Action + + for _, v := range input { + action := network.Action{ + Type: v.Type, + } + + parameters, err := expandParameters(v.Parameters) + if err != nil { + return nil, err + } + action.Parameters = parameters + + actions = append(actions, action) + } + + return &actions, nil +} + +func expandParameters(input []Parameter) (*[]network.Parameter, error) { + var paramters []network.Parameter + + for _, v := range input { + parameter := network.Parameter{ + AsPath: &v.AsPath, + Community: &v.Community, + RoutePrefix: &v.RoutePrefix, + } + + paramters = append(paramters, parameter) + } + + return ¶mters, nil +} + +func expandCriterions(input []Criterion) (*[]network.Criterion, error) { + var criterions []network.Criterion + + for _, v := range input { + criterion := network.Criterion{ + AsPath: &v.AsPath, + Community: &v.Community, + MatchCondition: v.MatchCondition, + RoutePrefix: &v.RoutePrefix, + } + + criterions = append(criterions, criterion) + } + + return &criterions, nil +} + +func flattenRules(input *[]network.RouteMapRule) ([]Rule, error) { + var rules []Rule + if input == nil { + return rules, nil + } + + for _, v := range *input { + rule := Rule{} + + actions, err := flattenActions(v.Actions) + if err != nil { + return nil, err + } + rule.Actions = actions + + matchCriteria, err := flattenCriterions(v.MatchCriteria) + if err != nil { + return nil, err + } + rule.MatchCriteria = matchCriteria + + if v.Name != nil { + rule.Name = *v.Name + } + + if v.NextStepIfMatched != "" { + rule.NextStepIfMatched = v.NextStepIfMatched + } + + rules = append(rules, rule) + } + + return rules, nil +} + +func flattenActions(input *[]network.Action) ([]Action, error) { + var actions []Action + if input == nil { + return actions, nil + } + + for _, v := range *input { + action := Action{} + + parameters, err := flattenParameters(v.Parameters) + if err != nil { + return nil, err + } + action.Parameters = parameters + + if v.Type != "" { + action.Type = v.Type + } + + actions = append(actions, action) + } + + return actions, nil +} + +func flattenParameters(input *[]network.Parameter) ([]Parameter, error) { + var parameters []Parameter + if input == nil { + return parameters, nil + } + + for _, v := range *input { + parameter := Parameter{} + + if v.AsPath != nil { + parameter.AsPath = *v.AsPath + } + + if v.Community != nil { + parameter.Community = *v.Community + } + + if v.RoutePrefix != nil { + parameter.RoutePrefix = *v.RoutePrefix + } + + parameters = append(parameters, parameter) + } + + return parameters, nil +} + +func flattenCriterions(input *[]network.Criterion) ([]Criterion, error) { + var criterions []Criterion + if input == nil { + return criterions, nil + } + + for _, v := range *input { + criterion := Criterion{} + + if v.AsPath != nil { + criterion.AsPath = *v.AsPath + } + + if v.Community != nil { + criterion.Community = *v.Community + } + + if v.MatchCondition != "" { + criterion.MatchCondition = v.MatchCondition + } + + if v.RoutePrefix != nil { + criterion.RoutePrefix = *v.RoutePrefix + } + + criterions = append(criterions, criterion) + } + + return criterions, nil +} diff --git a/internal/services/network/validate/route_map_id.go b/internal/services/network/validate/route_map_id.go new file mode 100644 index 000000000000..c3a2bcd7d93c --- /dev/null +++ b/internal/services/network/validate/route_map_id.go @@ -0,0 +1,23 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import ( + "fmt" + + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/parse" +) + +func RouteMapID(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := parse.RouteMapID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/network/validate/route_map_id_test.go b/internal/services/network/validate/route_map_id_test.go new file mode 100644 index 000000000000..11fe480fb09a --- /dev/null +++ b/internal/services/network/validate/route_map_id_test.go @@ -0,0 +1,88 @@ +package validate + +// NOTE: this file is generated via 'go:generate' - manual changes will be overwritten + +import "testing" + +func TestRouteMapID(t *testing.T) { + cases := []struct { + Input string + Valid bool + }{ + + { + // empty + Input: "", + Valid: false, + }, + + { + // missing SubscriptionId + Input: "/", + Valid: false, + }, + + { + // missing value for SubscriptionId + Input: "/subscriptions/", + Valid: false, + }, + + { + // missing ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/", + Valid: false, + }, + + { + // missing value for ResourceGroup + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/", + Valid: false, + }, + + { + // missing VirtualHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/", + Valid: false, + }, + + { + // missing value for VirtualHubName + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/", + Valid: false, + }, + + { + // missing Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/", + Valid: false, + }, + + { + // missing value for Name + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/routeMaps/", + Valid: false, + }, + + { + // valid + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/resGroup1/providers/Microsoft.Network/virtualHubs/vhub1/routeMaps/routeMap1", + Valid: true, + }, + + { + // upper-cased + Input: "/SUBSCRIPTIONS/12345678-1234-9876-4563-123456789012/RESOURCEGROUPS/RESGROUP1/PROVIDERS/MICROSOFT.NETWORK/VIRTUALHUBS/VHUB1/ROUTEMAPS/ROUTEMAP1", + Valid: false, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.Input) + _, errors := RouteMapID(tc.Input, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} From b39bf529365c93414c4d7f67b43ce695478b5d73 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Tue, 22 Nov 2022 17:36:17 +0800 Subject: [PATCH 2/7] update code --- .github/labeler-pull-request-triage.yml | 3 + .../services/network/route_map_resource.go | 138 +++++++----------- 2 files changed, 53 insertions(+), 88 deletions(-) diff --git a/.github/labeler-pull-request-triage.yml b/.github/labeler-pull-request-triage.yml index f36e9cd5d889..6f3c9203b15c 100644 --- a/.github/labeler-pull-request-triage.yml +++ b/.github/labeler-pull-request-triage.yml @@ -207,6 +207,9 @@ service/mysql: service/netapp: - internal/services/netapp/**/* +service/network: + - internal/services/network/**/* + service/nginx: - internal/services/nginx/**/* diff --git a/internal/services/network/route_map_resource.go b/internal/services/network/route_map_resource.go index 37803d0f3014..737c0d6c7312 100644 --- a/internal/services/network/route_map_resource.go +++ b/internal/services/network/route_map_resource.go @@ -15,22 +15,20 @@ import ( ) type RouteMapModel struct { - Name string `tfschema:"name"` - VirtualHubId string `tfschema:"virtual_hub_id"` - AssociatedInboundConnections []string `tfschema:"associated_inbound_connections"` - AssociatedOutboundConnections []string `tfschema:"associated_outbound_connections"` - Rules []Rule `tfschema:"rules"` + Name string `tfschema:"name"` + VirtualHubId string `tfschema:"virtual_hub_id"` + Rules []Rule `tfschema:"rule"` } type Rule struct { - Actions []Action `tfschema:"actions"` - MatchCriteria []Criterion `tfschema:"match_criteria"` + Actions []Action `tfschema:"action"` + MatchCriteria []Criterion `tfschema:"match_criterion"` Name string `tfschema:"name"` NextStepIfMatched network.NextStep `tfschema:"next_step_if_matched"` } type Action struct { - Parameters []Parameter `tfschema:"parameters"` + Parameters []Parameter `tfschema:"parameter"` Type network.RouteMapActionType `tfschema:"type"` } @@ -79,37 +77,25 @@ func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { ValidateFunc: validate.VirtualHubID, }, - "associated_inbound_connections": { - Type: pluginsdk.TypeList, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateFunc: validation.StringIsNotEmpty, - }, - }, - - "associated_outbound_connections": { - Type: pluginsdk.TypeList, - Optional: true, - Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, - ValidateFunc: validation.StringIsNotEmpty, - }, - }, - - "rules": { + "rule": { Type: pluginsdk.TypeList, Optional: true, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ - "actions": { + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "action": { Type: pluginsdk.TypeList, Optional: true, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ - "parameters": { + "parameter": { Type: pluginsdk.TypeList, - Optional: true, + Required: true, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ "as_path": { @@ -144,24 +130,36 @@ func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { "type": { Type: pluginsdk.TypeString, - Optional: true, + Required: true, ValidateFunc: validation.StringInSlice([]string{ - string(network.RouteMapActionTypeReplace), + string(network.RouteMapActionTypeAdd), string(network.RouteMapActionTypeDrop), - string(network.RouteMapActionTypeUnknown), string(network.RouteMapActionTypeRemove), - string(network.RouteMapActionTypeAdd), + string(network.RouteMapActionTypeReplace), + string(network.RouteMapActionTypeUnknown), }, false), }, }, }, }, - "match_criteria": { + "match_criterion": { Type: pluginsdk.TypeList, Optional: true, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ + "match_condition": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.RouteMapMatchConditionContains), + string(network.RouteMapMatchConditionEquals), + string(network.RouteMapMatchConditionNotContains), + string(network.RouteMapMatchConditionNotEquals), + string(network.RouteMapMatchConditionUnknown), + }, false), + }, + "as_path": { Type: pluginsdk.TypeList, Optional: true, @@ -180,18 +178,6 @@ func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { }, }, - "match_condition": { - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{ - string(network.RouteMapMatchConditionEquals), - string(network.RouteMapMatchConditionNotContains), - string(network.RouteMapMatchConditionNotEquals), - string(network.RouteMapMatchConditionUnknown), - string(network.RouteMapMatchConditionContains), - }, false), - }, - "route_prefix": { Type: pluginsdk.TypeList, Optional: true, @@ -204,15 +190,10 @@ func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { }, }, - "name": { - Type: pluginsdk.TypeString, - Optional: true, - ValidateFunc: validation.StringIsNotEmpty, - }, - "next_step_if_matched": { Type: pluginsdk.TypeString, Optional: true, + Default: string(network.NextStepUnknown), ValidateFunc: validation.StringInSlice([]string{ string(network.NextStepUnknown), string(network.NextStepContinue), @@ -254,10 +235,7 @@ func (r RouteMapResource) Create() sdk.ResourceFunc { } props := &network.RouteMap{ - RouteMapProperties: &network.RouteMapProperties{ - AssociatedInboundConnections: &model.AssociatedInboundConnections, - AssociatedOutboundConnections: &model.AssociatedOutboundConnections, - }, + RouteMapProperties: &network.RouteMapProperties{}, } rules, err := expandRules(model.Rules) @@ -302,15 +280,7 @@ func (r RouteMapResource) Update() sdk.ResourceFunc { return fmt.Errorf("retrieving %s: %+v", *id, err) } - if metadata.ResourceData.HasChange("associated_inbound_connections") { - existing.RouteMapProperties.AssociatedInboundConnections = &model.AssociatedInboundConnections - } - - if metadata.ResourceData.HasChange("associated_outbound_connections") { - existing.RouteMapProperties.AssociatedOutboundConnections = &model.AssociatedOutboundConnections - } - - if metadata.ResourceData.HasChange("rules") { + if metadata.ResourceData.HasChange("rule") { rules, err := expandRules(model.Rules) if err != nil { return err @@ -358,14 +328,6 @@ func (r RouteMapResource) Read() sdk.ResourceFunc { } if props := resp.RouteMapProperties; props != nil { - if props.AssociatedInboundConnections != nil { - state.AssociatedInboundConnections = *props.AssociatedInboundConnections - } - - if props.AssociatedOutboundConnections != nil { - state.AssociatedOutboundConnections = *props.AssociatedOutboundConnections - } - rules, err := flattenRules(props.Rules) if err != nil { return err @@ -408,7 +370,7 @@ func expandRules(input []Rule) (*[]network.RouteMapRule, error) { for _, v := range input { rule := network.RouteMapRule{ - NextStepIfMatched: v.NextStepIfMatched, + Name: &v.Name, } actions, err := expandActions(v.Actions) @@ -417,14 +379,14 @@ func expandRules(input []Rule) (*[]network.RouteMapRule, error) { } rule.Actions = actions - matchCriteria, err := expandCriterions(v.MatchCriteria) + matchCriteria, err := expandCriteria(v.MatchCriteria) if err != nil { return nil, err } rule.MatchCriteria = matchCriteria - if v.Name != "" { - rule.Name = &v.Name + if v.NextStepIfMatched != "" { + rule.NextStepIfMatched = v.NextStepIfMatched } rules = append(rules, rule) @@ -469,8 +431,8 @@ func expandParameters(input []Parameter) (*[]network.Parameter, error) { return ¶mters, nil } -func expandCriterions(input []Criterion) (*[]network.Criterion, error) { - var criterions []network.Criterion +func expandCriteria(input []Criterion) (*[]network.Criterion, error) { + var criteria []network.Criterion for _, v := range input { criterion := network.Criterion{ @@ -480,10 +442,10 @@ func expandCriterions(input []Criterion) (*[]network.Criterion, error) { RoutePrefix: &v.RoutePrefix, } - criterions = append(criterions, criterion) + criteria = append(criteria, criterion) } - return &criterions, nil + return &criteria, nil } func flattenRules(input *[]network.RouteMapRule) ([]Rule, error) { @@ -501,7 +463,7 @@ func flattenRules(input *[]network.RouteMapRule) ([]Rule, error) { } rule.Actions = actions - matchCriteria, err := flattenCriterions(v.MatchCriteria) + matchCriteria, err := flattenCriteria(v.MatchCriteria) if err != nil { return nil, err } @@ -573,10 +535,10 @@ func flattenParameters(input *[]network.Parameter) ([]Parameter, error) { return parameters, nil } -func flattenCriterions(input *[]network.Criterion) ([]Criterion, error) { - var criterions []Criterion +func flattenCriteria(input *[]network.Criterion) ([]Criterion, error) { + var criteria []Criterion if input == nil { - return criterions, nil + return criteria, nil } for _, v := range *input { @@ -598,8 +560,8 @@ func flattenCriterions(input *[]network.Criterion) ([]Criterion, error) { criterion.RoutePrefix = *v.RoutePrefix } - criterions = append(criterions, criterion) + criteria = append(criteria, criterion) } - return criterions, nil + return criteria, nil } From 4a22c0e4afbcfff8c6cfafa1caf92815a9af5f9f Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Wed, 23 Nov 2022 14:29:28 +0800 Subject: [PATCH 3/7] update code --- internal/provider/services.go | 1 + .../services/network/route_map_resource.go | 47 +++-- .../network/route_map_resource_test.go | 186 ++++++++++++++++++ website/docs/r/route_map.html.markdown | 114 +++++++++++ 4 files changed, 334 insertions(+), 14 deletions(-) create mode 100644 internal/services/network/route_map_resource_test.go create mode 100644 website/docs/r/route_map.html.markdown diff --git a/internal/provider/services.go b/internal/provider/services.go index 3f04cdd0e438..d24f98436680 100644 --- a/internal/provider/services.go +++ b/internal/provider/services.go @@ -141,6 +141,7 @@ func SupportedTypedServices() []sdk.TypedServiceRegistration { loganalytics.Registration{}, monitor.Registration{}, mssql.Registration{}, + network.Registration{}, nginx.Registration{}, policy.Registration{}, privatednsresolver.Registration{}, diff --git a/internal/services/network/route_map_resource.go b/internal/services/network/route_map_resource.go index 737c0d6c7312..90fcb9dc3fb4 100644 --- a/internal/services/network/route_map_resource.go +++ b/internal/services/network/route_map_resource.go @@ -195,9 +195,9 @@ func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { Optional: true, Default: string(network.NextStepUnknown), ValidateFunc: validation.StringInSlice([]string{ - string(network.NextStepUnknown), string(network.NextStepContinue), string(network.NextStepTerminate), + string(network.NextStepUnknown), }, false), }, }, @@ -370,7 +370,7 @@ func expandRules(input []Rule) (*[]network.RouteMapRule, error) { for _, v := range input { rule := network.RouteMapRule{ - Name: &v.Name, + Name: utils.String(v.Name), } actions, err := expandActions(v.Actions) @@ -416,30 +416,49 @@ func expandActions(input []Action) (*[]network.Action, error) { } func expandParameters(input []Parameter) (*[]network.Parameter, error) { - var paramters []network.Parameter + var parameters []network.Parameter - for _, v := range input { - parameter := network.Parameter{ - AsPath: &v.AsPath, - Community: &v.Community, - RoutePrefix: &v.RoutePrefix, + for _, item := range input { + v := item + parameter := network.Parameter{} + + if v.AsPath != nil { + parameter.AsPath = &v.AsPath + } + + if v.Community != nil { + parameter.Community = &v.Community + } + + if v.RoutePrefix != nil { + parameter.RoutePrefix = &v.RoutePrefix } - paramters = append(paramters, parameter) + parameters = append(parameters, parameter) } - return ¶mters, nil + return ¶meters, nil } func expandCriteria(input []Criterion) (*[]network.Criterion, error) { var criteria []network.Criterion - for _, v := range input { + for _, item := range input { + v := item criterion := network.Criterion{ - AsPath: &v.AsPath, - Community: &v.Community, MatchCondition: v.MatchCondition, - RoutePrefix: &v.RoutePrefix, + } + + if v.AsPath != nil { + criterion.AsPath = &v.AsPath + } + + if v.Community != nil { + criterion.Community = &v.Community + } + + if v.RoutePrefix != nil { + criterion.RoutePrefix = &v.RoutePrefix } criteria = append(criteria, criterion) diff --git a/internal/services/network/route_map_resource_test.go b/internal/services/network/route_map_resource_test.go new file mode 100644 index 000000000000..24371a312991 --- /dev/null +++ b/internal/services/network/route_map_resource_test.go @@ -0,0 +1,186 @@ +package network_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" +) + +type RouteMapResource struct{} + +func TestAccRouteMap_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_route_map", "test") + r := RouteMapResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccRouteMap_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_route_map", "test") + r := RouteMapResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccRouteMap_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_route_map", "test") + r := RouteMapResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (r RouteMapResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.RouteMapID(state.ID) + if err != nil { + return nil, err + } + + client := clients.Network.RouteMapsClient + resp, err := client.Get(ctx, id.ResourceGroup, id.VirtualHubName, id.Name) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", id, err) + } + + return utils.Bool(resp.ID != nil), nil +} + +func (r RouteMapResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-rm-%d" + location = "%s" +} + +resource "azurerm_virtual_wan" "test" { + name = "acctestvwan-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location +} + +resource "azurerm_virtual_hub" "test" { + name = "acctestvhub-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + virtual_wan_id = azurerm_virtual_wan.test.id + address_prefix = "10.0.1.0/24" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +} + +func (r RouteMapResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_route_map" "test" { + name = "acctestrm-%d" + virtual_hub_id = azurerm_virtual_hub.test.id +} +`, r.template(data), data.RandomInteger) +} + +func (r RouteMapResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_route_map" "test" { + name = "acctestrm-%d" + virtual_hub_id = azurerm_virtual_hub.test.id + + rule { + name = "rule1" + next_step_if_matched = "Continue" + + action { + type = "Add" + + parameter { + as_path = ["22334"] + } + } + + match_criterion { + match_condition = "Contains" + route_prefix = ["10.0.0.0/8"] + } + } +} +`, r.template(data), data.RandomInteger) +} + +func (r RouteMapResource) update(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_route_map" "test" { + name = "acctestrm-%d" + virtual_hub_id = azurerm_virtual_hub.test.id + + rule { + name = "rule2" + next_step_if_matched = "Terminate" + + action { + type = "Replace" + + parameter { + route_prefix = ["10.0.1.0/8"] + } + } + + match_criterion { + match_condition = "NotContains" + as_path = ["223345"] + } + } +} +`, r.template(data), data.RandomInteger) +} diff --git a/website/docs/r/route_map.html.markdown b/website/docs/r/route_map.html.markdown new file mode 100644 index 000000000000..73cdcd0b71a0 --- /dev/null +++ b/website/docs/r/route_map.html.markdown @@ -0,0 +1,114 @@ +--- +subcategory: "Network" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_route_map" +description: |- + Manages a Route Map. +--- + +# azurerm_route_map + +Manages a Route Map. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-resources" + location = "West Europe" +} + +resource "azurerm_virtual_wan" "example" { + name = "example-vwan" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location +} + +resource "azurerm_virtual_hub" "test" { + name = "example-vhub" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + virtual_wan_id = azurerm_virtual_wan.example.id + address_prefix = "10.0.1.0/24" +} + +resource "azurerm_route_map" "example" { + name = "example-rm" + virtual_hub_id = azurerm_virtual_hub.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name which should be used for this Route Map. Changing this forces a new resource to be created. + +* `virtual_hub_id` - (Required) The resource ID of the Virtual Hub. Changing this forces a new resource to be created. + +* `rule` - (Optional) A `rule` block as defined below. + +--- + +A `rule` block supports the following: + +* `name` - (Required) The unique name for the rule. + +* `action` - (Optional) An `action` block as defined below. + +* `match_criterion` - (Optional) A `match_criterion` block as defined below. + +* `next_step_if_matched` - (Optional) The next step after the rule is evaluated. Possible values are `Continue`, `Terminate` and `Unknown`. Defaults to `Unknown`. + +--- + +An `action` block supports the following: + +* `parameter` - (Required) A `parameter` block as defined below. + +* `type` - (Required) The type of the action to be taken. Possible values are `Add`, `Drop`, `Remove`, `Replace` and `Unknown`. + +--- + +A `parameter` block supports the following: + +* `as_path` - (Optional) A list of AS paths. + +* `community` - (Optional) A list of BGP communities. + +* `route_prefix` - (Optional) A list of route prefixes. + +--- + +A `match_criterion` block supports the following: + +* `match_condition` - (Required) The match condition to apply the rule of the Route Map. Possible values are `Contains`, `Equals`, `NotContains`, `NotEquals` and `Unknown`. + +* `as_path` - (Optional) A list of AS paths which this criterion matches. + +* `community` - (Optional) A list of BGP communities which this criterion matches. + +* `route_prefix` - (Optional) A list of route prefixes which this criterion matches. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Route Map. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Route Map. +* `read` - (Defaults to 5 minutes) Used when retrieving the Route Map. +* `update` - (Defaults to 30 minutes) Used when updating the Route Map. +* `delete` - (Defaults to 30 minutes) Used when deleting the Route Map. + +## Import + +Route Maps can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_route_map.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resourceGroup1/providers/Microsoft.Network/virtualHubs/virtualHub1/routeMaps/routeMap1 +``` From 12497f0820e2eb651c696a28dea501902272281d Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Wed, 23 Nov 2022 15:17:51 +0800 Subject: [PATCH 4/7] update code --- .../services/network/route_map_resource.go | 2 +- .../network/validate/route_map_name.go | 23 ++++++++++ .../network/validate/route_map_name_test.go | 43 +++++++++++++++++++ website/docs/r/route_map.html.markdown | 18 ++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 internal/services/network/validate/route_map_name.go create mode 100644 internal/services/network/validate/route_map_name_test.go diff --git a/internal/services/network/route_map_resource.go b/internal/services/network/route_map_resource.go index 90fcb9dc3fb4..8cf8a89d6996 100644 --- a/internal/services/network/route_map_resource.go +++ b/internal/services/network/route_map_resource.go @@ -67,7 +67,7 @@ func (r RouteMapResource) Arguments() map[string]*pluginsdk.Schema { Type: pluginsdk.TypeString, Required: true, ForceNew: true, - ValidateFunc: validation.StringIsNotEmpty, + ValidateFunc: validate.RouteMapName, }, "virtual_hub_id": { diff --git a/internal/services/network/validate/route_map_name.go b/internal/services/network/validate/route_map_name.go new file mode 100644 index 000000000000..230e5e5bc339 --- /dev/null +++ b/internal/services/network/validate/route_map_name.go @@ -0,0 +1,23 @@ +package validate + +import ( + "fmt" + "regexp" +) + +func RouteMapName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + if !regexp.MustCompile(`^[a-zA-Z][a-zA-Z_.-]+[a-zA-Z_]$`).MatchString(value) { + errors = append(errors, fmt.Errorf("The name must begin with a letter, end with a letter or underscore, and may contain only letters, underscores, periods or hyphens. %q: %q", k, value)) + } + + if 1 > len(value) { + errors = append(errors, fmt.Errorf("%q cannot be less than 1 characters: %q", k, value)) + } + + if len(value) > 80 { + errors = append(errors, fmt.Errorf("%q cannot be longer than 80 characters: %q %d", k, value, len(value))) + } + + return warnings, errors +} diff --git a/internal/services/network/validate/route_map_name_test.go b/internal/services/network/validate/route_map_name_test.go new file mode 100644 index 000000000000..41fd92b744a0 --- /dev/null +++ b/internal/services/network/validate/route_map_name_test.go @@ -0,0 +1,43 @@ +package validate + +import ( + "strings" + "testing" +) + +func TestValidateRouteMapName(t *testing.T) { + cases := []struct { + Input string + ExpectError bool + }{ + { + Input: "", + ExpectError: true, + }, + { + Input: "he.l-l_o_", + ExpectError: false, + }, + { + Input: strings.Repeat("s", 79), + ExpectError: false, + }, + { + Input: strings.Repeat("s", 80), + ExpectError: false, + }, + { + Input: strings.Repeat("s", 81), + ExpectError: true, + }, + } + + for _, tc := range cases { + _, errors := RouteMapName(tc.Input, "name") + + hasError := len(errors) > 0 + if tc.ExpectError && !hasError { + t.Fatalf("Expected the Route Map Name to trigger a validation error for '%s'", tc.Input) + } + } +} diff --git a/website/docs/r/route_map.html.markdown b/website/docs/r/route_map.html.markdown index 73cdcd0b71a0..25a879cb580f 100644 --- a/website/docs/r/route_map.html.markdown +++ b/website/docs/r/route_map.html.markdown @@ -35,6 +35,24 @@ resource "azurerm_virtual_hub" "test" { resource "azurerm_route_map" "example" { name = "example-rm" virtual_hub_id = azurerm_virtual_hub.example.id + + rule { + name = "rule1" + next_step_if_matched = "Continue" + + action { + type = "Add" + + parameter { + as_path = ["22334"] + } + } + + match_criterion { + match_condition = "Contains" + route_prefix = ["10.0.0.0/8"] + } + } } ``` From 9b4e826fdf084a6f1c2ec709377f635fed4fc2f5 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Tue, 6 Dec 2022 09:05:17 +0800 Subject: [PATCH 5/7] update code --- internal/services/network/route_map_resource_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/services/network/route_map_resource_test.go b/internal/services/network/route_map_resource_test.go index 24371a312991..65609cb5bc3e 100644 --- a/internal/services/network/route_map_resource_test.go +++ b/internal/services/network/route_map_resource_test.go @@ -121,10 +121,10 @@ func (r RouteMapResource) basic(data acceptance.TestData) string { %s resource "azurerm_route_map" "test" { - name = "acctestrm-%d" + name = "acctestrm-%s" virtual_hub_id = azurerm_virtual_hub.test.id } -`, r.template(data), data.RandomInteger) +`, r.template(data), data.RandomString) } func (r RouteMapResource) complete(data acceptance.TestData) string { @@ -132,7 +132,7 @@ func (r RouteMapResource) complete(data acceptance.TestData) string { %s resource "azurerm_route_map" "test" { - name = "acctestrm-%d" + name = "acctestrm-%s" virtual_hub_id = azurerm_virtual_hub.test.id rule { @@ -153,7 +153,7 @@ resource "azurerm_route_map" "test" { } } } -`, r.template(data), data.RandomInteger) +`, r.template(data), data.RandomString) } func (r RouteMapResource) update(data acceptance.TestData) string { @@ -161,7 +161,7 @@ func (r RouteMapResource) update(data acceptance.TestData) string { %s resource "azurerm_route_map" "test" { - name = "acctestrm-%d" + name = "acctestrm-%s" virtual_hub_id = azurerm_virtual_hub.test.id rule { @@ -182,5 +182,5 @@ resource "azurerm_route_map" "test" { } } } -`, r.template(data), data.RandomInteger) +`, r.template(data), data.RandomString) } From 41e51bb12685b8c11f30377c6bd035ef3ce27f04 Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Tue, 6 Dec 2022 09:40:49 +0800 Subject: [PATCH 6/7] update code --- .../network/route_map_resource_test.go | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/internal/services/network/route_map_resource_test.go b/internal/services/network/route_map_resource_test.go index 65609cb5bc3e..3c0e16ff6866 100644 --- a/internal/services/network/route_map_resource_test.go +++ b/internal/services/network/route_map_resource_test.go @@ -3,6 +3,7 @@ package network_test import ( "context" "fmt" + "math/rand" "testing" "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" @@ -18,10 +19,11 @@ type RouteMapResource struct{} func TestAccRouteMap_basic(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_route_map", "test") r := RouteMapResource{} + nameSuffix := randString() data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.basic(data), + Config: r.basic(data, nameSuffix), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -33,10 +35,11 @@ func TestAccRouteMap_basic(t *testing.T) { func TestAccRouteMap_complete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_route_map", "test") r := RouteMapResource{} + nameSuffix := randString() data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.complete(data), + Config: r.complete(data, nameSuffix), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -48,24 +51,25 @@ func TestAccRouteMap_complete(t *testing.T) { func TestAccRouteMap_update(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_route_map", "test") r := RouteMapResource{} + nameSuffix := randString() data.ResourceTest(t, r, []acceptance.TestStep{ { - Config: r.complete(data), + Config: r.complete(data, nameSuffix), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), { - Config: r.update(data), + Config: r.update(data, nameSuffix), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), }, data.ImportStep(), { - Config: r.complete(data), + Config: r.complete(data, nameSuffix), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), ), @@ -89,6 +93,16 @@ func (r RouteMapResource) Exists(ctx context.Context, clients *clients.Client, s return utils.Bool(resp.ID != nil), nil } +func randString() string { + charSet := "abcdefghijklmnopqrstuvwxyz" + strlen := 5 + result := make([]byte, strlen) + for i := 0; i < strlen; i++ { + result[i] = charSet[rand.Intn(len(charSet))] + } + return string(result) +} + func (r RouteMapResource) template(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { @@ -116,7 +130,7 @@ resource "azurerm_virtual_hub" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) } -func (r RouteMapResource) basic(data acceptance.TestData) string { +func (r RouteMapResource) basic(data acceptance.TestData, nameSuffix string) string { return fmt.Sprintf(` %s @@ -124,10 +138,10 @@ resource "azurerm_route_map" "test" { name = "acctestrm-%s" virtual_hub_id = azurerm_virtual_hub.test.id } -`, r.template(data), data.RandomString) +`, r.template(data), nameSuffix) } -func (r RouteMapResource) complete(data acceptance.TestData) string { +func (r RouteMapResource) complete(data acceptance.TestData, nameSuffix string) string { return fmt.Sprintf(` %s @@ -153,10 +167,10 @@ resource "azurerm_route_map" "test" { } } } -`, r.template(data), data.RandomString) +`, r.template(data), nameSuffix) } -func (r RouteMapResource) update(data acceptance.TestData) string { +func (r RouteMapResource) update(data acceptance.TestData, nameSuffix string) string { return fmt.Sprintf(` %s @@ -182,5 +196,5 @@ resource "azurerm_route_map" "test" { } } } -`, r.template(data), data.RandomString) +`, r.template(data), nameSuffix) } From 7f36ff82005c0e6f298dc9b7ebc9cd9f5c76171a Mon Sep 17 00:00:00 2001 From: neil-yechenwei Date: Thu, 8 Dec 2022 15:39:57 +0800 Subject: [PATCH 7/7] update code --- .../network/route_map_resource_test.go | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/services/network/route_map_resource_test.go b/internal/services/network/route_map_resource_test.go index 3c0e16ff6866..a3eb3f669d23 100644 --- a/internal/services/network/route_map_resource_test.go +++ b/internal/services/network/route_map_resource_test.go @@ -32,6 +32,25 @@ func TestAccRouteMap_basic(t *testing.T) { }) } +func TestAccRouteMap_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_route_map", "test") + r := RouteMapResource{} + nameSuffix := randString() + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, nameSuffix), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data, nameSuffix), + ExpectError: acceptance.RequiresImportError("azurerm_route_map"), + }, + }) +} + func TestAccRouteMap_complete(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_route_map", "test") r := RouteMapResource{} @@ -141,6 +160,17 @@ resource "azurerm_route_map" "test" { `, r.template(data), nameSuffix) } +func (r RouteMapResource) requiresImport(data acceptance.TestData, nameSuffix string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_route_map" "import" { + name = azurerm_route_map.test.name + virtual_hub_id = azurerm_route_map.test.virtual_hub_id +} +`, r.basic(data, nameSuffix)) +} + func (r RouteMapResource) complete(data acceptance.TestData, nameSuffix string) string { return fmt.Sprintf(` %s