diff --git a/internal/services/network/client/client.go b/internal/services/network/client/client.go index 8d2220b6e53e7..5f4ec48a63010 100644 --- a/internal/services/network/client/client.go +++ b/internal/services/network/client/client.go @@ -33,6 +33,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 @@ -162,6 +163,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) @@ -265,6 +269,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 0000000000000..8df9f549cfe9e --- /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 0000000000000..a6359c5b79878 --- /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 a79bb71eff160..326962ca0a287 100644 --- a/internal/services/network/registration.go +++ b/internal/services/network/registration.go @@ -35,6 +35,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ ManagerResource{}, + RouteMapResource{}, } } diff --git a/internal/services/network/resourceids.go b/internal/services/network/resourceids.go index bb6530f0190fa..f883a09aa7174 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 0000000000000..8cf8a89d69969 --- /dev/null +++ b/internal/services/network/route_map_resource.go @@ -0,0 +1,586 @@ +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"` + Rules []Rule `tfschema:"rule"` +} + +type Rule struct { + 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:"parameter"` + 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: validate.RouteMapName, + }, + + "virtual_hub_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.VirtualHubID, + }, + + "rule": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "action": { + Type: pluginsdk.TypeList, + Optional: true, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "parameter": { + Type: pluginsdk.TypeList, + Required: 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, + Required: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.RouteMapActionTypeAdd), + string(network.RouteMapActionTypeDrop), + string(network.RouteMapActionTypeRemove), + string(network.RouteMapActionTypeReplace), + string(network.RouteMapActionTypeUnknown), + }, false), + }, + }, + }, + }, + + "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, + 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, + }, + }, + }, + }, + }, + + "next_step_if_matched": { + Type: pluginsdk.TypeString, + Optional: true, + Default: string(network.NextStepUnknown), + ValidateFunc: validation.StringInSlice([]string{ + string(network.NextStepContinue), + string(network.NextStepTerminate), + string(network.NextStepUnknown), + }, 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{}, + } + + 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("rule") { + 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 { + 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{ + Name: utils.String(v.Name), + } + + actions, err := expandActions(v.Actions) + if err != nil { + return nil, err + } + rule.Actions = actions + + matchCriteria, err := expandCriteria(v.MatchCriteria) + if err != nil { + return nil, err + } + rule.MatchCriteria = matchCriteria + + if v.NextStepIfMatched != "" { + rule.NextStepIfMatched = v.NextStepIfMatched + } + + 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 parameters []network.Parameter + + 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 + } + + parameters = append(parameters, parameter) + } + + return ¶meters, nil +} + +func expandCriteria(input []Criterion) (*[]network.Criterion, error) { + var criteria []network.Criterion + + for _, item := range input { + v := item + criterion := network.Criterion{ + MatchCondition: v.MatchCondition, + } + + 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) + } + + return &criteria, 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 := flattenCriteria(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 flattenCriteria(input *[]network.Criterion) ([]Criterion, error) { + var criteria []Criterion + if input == nil { + return criteria, 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 + } + + criteria = append(criteria, criterion) + } + + return criteria, nil +} 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 0000000000000..a3eb3f669d23b --- /dev/null +++ b/internal/services/network/route_map_resource_test.go @@ -0,0 +1,230 @@ +package network_test + +import ( + "context" + "fmt" + "math/rand" + "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{} + nameSuffix := randString() + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data, nameSuffix), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +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{} + nameSuffix := randString() + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data, nameSuffix), + 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{} + nameSuffix := randString() + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data, nameSuffix), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.update(data, nameSuffix), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data, nameSuffix), + 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 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" { + 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, nameSuffix string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_route_map" "test" { + name = "acctestrm-%s" + virtual_hub_id = azurerm_virtual_hub.test.id +} +`, 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 + +resource "azurerm_route_map" "test" { + name = "acctestrm-%s" + 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), nameSuffix) +} + +func (r RouteMapResource) update(data acceptance.TestData, nameSuffix string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_route_map" "test" { + name = "acctestrm-%s" + 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), nameSuffix) +} 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 0000000000000..c3a2bcd7d93cb --- /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 0000000000000..11fe480fb09a2 --- /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) + } + } +} 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 0000000000000..230e5e5bc339d --- /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 0000000000000..41fd92b744a0c --- /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 new file mode 100644 index 0000000000000..25a879cb580f9 --- /dev/null +++ b/website/docs/r/route_map.html.markdown @@ -0,0 +1,132 @@ +--- +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 + + 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"] + } + } +} +``` + +## 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 +```