Repository: AviatrixSystems/terraform-provider-aviatrix
Pull request #2181: AVX-62274 Updating the sort order for interfaces in EAT
Author: login: sairane10
Created: 2025-03-10T22:00:58Z | Merged: 2025-03-18T16:24:48Z

- Fixing the sort order of interfaces using the order provided by the user
- The older code for updating the eip_map was sending the data as form string. For Megaport, we require eip_map to be provided as a json objects instead. In this PR, I have updated the eip_map code to use the API v2 version which accepts json object. This ensures that the eip_map gets updated correctly and the original object structure is passed directly for the update.


Diff:
diff --git a/aviatrix/resource_aviatrix_transit_gateway.go b/aviatrix/resource_aviatrix_transit_gateway.go
index e2b53578b..92f9aea4c 100644
--- a/aviatrix/resource_aviatrix_transit_gateway.go
+++ b/aviatrix/resource_aviatrix_transit_gateway.go
@@ -1873,7 +1873,16 @@ func resourceAviatrixTransitGatewayRead(d *schema.ResourceData, meta interface{}
 		}
 		// set interfaces
 		if len(gw.Interfaces) != 0 {
-			interfaces := setInterfaceDetails(gw.Interfaces)
+			// get the order of interfaces
+			userInterfaces, ok := d.Get("interfaces").([]interface{})
+			if !ok {
+				return fmt.Errorf("could not get user interfaces")
+			}
+			userInterfaceOrder, err := getUserInterfaceOrder(userInterfaces)
+			if err != nil {
+				return fmt.Errorf("could not get user interface order: %w", err)
+			}
+			interfaces := setInterfaceDetails(gw.Interfaces, userInterfaceOrder)
 			if err = d.Set("interfaces", interfaces); err != nil {
 				return fmt.Errorf("could not set interfaces into state: %w", err)
 			}
@@ -1950,8 +1959,16 @@ func resourceAviatrixTransitGatewayRead(d *schema.ResourceData, meta interface{}
 		}
 		// set ha interfaces
 		if len(gw.HaGw.Interfaces) != 0 {
-			ha_interfaces := setInterfaceDetails(gw.HaGw.Interfaces)
-			if err = d.Set("ha_interfaces", ha_interfaces); err != nil {
+			userInterfaces, ok := d.Get("ha_interfaces").([]interface{})
+			if !ok {
+				return fmt.Errorf("could not get user interfaces")
+			}
+			userInterfaceOrder, err := getUserInterfaceOrder(userInterfaces)
+			if err != nil {
+				return fmt.Errorf("could not get user interface order: %w", err)
+			}
+			haInterfaces := setInterfaceDetails(gw.HaGw.Interfaces, userInterfaceOrder)
+			if err = d.Set("ha_interfaces", haInterfaces); err != nil {
 				return fmt.Errorf("could not set interfaces into state: %w", err)
 			}
 		}
@@ -2940,14 +2957,25 @@ func resourceAviatrixTransitGatewayUpdate(d *schema.ResourceData, meta interface
 					// print eip map for edge mega port
 					log.Printf("[INFO] EIP Map for Edge Mega Port: %#v", eipMapList)
 					gateway.LogicalEipMap = eipMapList
+					gateway.CloudType = cloudType
+					ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+					defer cancel()
+					err = client.UpdateEdgeGatewayV2(ctx, gateway)
+					if err != nil {
+						return fmt.Errorf("failed to update logical eip map for edge gateway: %w", err)
+					}
 				} else {
 					eipMapJSON, err := json.Marshal(eipMapList)
 					if err != nil {
 						return fmt.Errorf("failed to marshal eip_map to JSON: %w", err)
 					}
 					gateway.EipMap = string(eipMapJSON)
+					err = client.UpdateEdgeGateway(gateway)
+					if err != nil {
+						return fmt.Errorf("failed to update eip map for edge gateway: %w", err)
+					}
 				}
-				err = client.UpdateEdgeGateway(gateway)
+
 				if err != nil {
 					return fmt.Errorf("failed to update edge gateway: %s", err)
 				}
@@ -4004,17 +4032,24 @@ func createEdgeTransitGateway(d *schema.ResourceData, client *goaviatrix.Client,
 			// print eip map for edge mega port
 			log.Printf("[INFO] EIP Map for Edge Mega Port: %#v", eipMapList)
 			gateway.LogicalEipMap = eipMapList
+			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+			defer cancel()
+			// update logical eip map
+			err = client.UpdateEdgeGatewayV2(ctx, gateway)
+			if err != nil {
+				return fmt.Errorf("failed to update logical eip map for edge gateway: %w", err)
+			}
 		} else {
 			eipMapJSON, err := json.Marshal(eipMapList)
 			if err != nil {
 				return fmt.Errorf("failed to marshal eip_map to JSON: %w", err)
 			}
 			gateway.EipMap = string(eipMapJSON)
-		}
-		// update EIP map
-		err = client.UpdateEdgeGateway(gateway)
-		if err != nil {
-			return fmt.Errorf("failed to update edge gateway: %s", err)
+			// update EIP map
+			err = client.UpdateEdgeGateway(gateway)
+			if err != nil {
+				return fmt.Errorf("failed to update edge gateway: %w", err)
+			}
 		}
 	}
 
@@ -4474,9 +4509,9 @@ func getStringListAttribute(data map[string]interface{}, key string) ([]string,
 	return result, nil
 }
 
-func setInterfaceDetails(interfaces []goaviatrix.EdgeTransitInterface) []map[string]interface{} {
+func setInterfaceDetails(interfaces []goaviatrix.EdgeTransitInterface, interfaceOrder []string) []map[string]interface{} {
 	interfaceList := make([]map[string]interface{}, 0)
-	sortedInterfaces := sortInterfacesByCustomOrder(interfaces)
+	sortedInterfaces := sortInterfacesByCustomOrder(interfaces, interfaceOrder)
 	for _, intf := range sortedInterfaces {
 		interfaceDict := make(map[string]interface{})
 		interfaceDict["logical_ifname"] = intf.LogicalIfName
@@ -4616,3 +4651,20 @@ func setGatewayResourceData(d *schema.ResourceData, gw *goaviatrix.Gateway) erro
 
 	return nil
 }
+
+// get the user provided interface order
+func getUserInterfaceOrder(interfaces []interface{}) ([]string, error) {
+	userInterfaceOrder := make([]string, 0)
+	for _, iface := range interfaces {
+		ifaceMap, ok := iface.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("interface is not a map[string]interface{}")
+		}
+		logicalIfName, ok := ifaceMap["logical_ifname"].(string)
+		if !ok {
+			return nil, fmt.Errorf("logical interface name must be a string")
+		}
+		userInterfaceOrder = append(userInterfaceOrder, logicalIfName)
+	}
+	return userInterfaceOrder, nil
+}
diff --git a/aviatrix/resource_aviatrix_transit_gateway_test.go b/aviatrix/resource_aviatrix_transit_gateway_test.go
index 3465ec927..024cdf0fb 100644
--- a/aviatrix/resource_aviatrix_transit_gateway_test.go
+++ b/aviatrix/resource_aviatrix_transit_gateway_test.go
@@ -1108,6 +1108,9 @@ func TestSetInterfaceMappingDetails(t *testing.T) {
 }
 
 func TestSetInterfaceDetails(t *testing.T) {
+	// Define the interface order
+	interfaceOrder := []string{"wan0", "wan1", "mgmt0", "wan2", "wan3"}
+
 	// Define test cases
 	tests := []struct {
 		name       string
@@ -1132,16 +1135,16 @@ func TestSetInterfaceDetails(t *testing.T) {
 		{
 			name: "Multiple WAN and MANAGEMENT interfaces",
 			interfaces: []goaviatrix.EdgeTransitInterface{
-				{LogicalIfName: "wan0", IpAddress: "10.0.0.2"},
-				{LogicalIfName: "wan1", IpAddress: "10.0.0.3"},
 				{LogicalIfName: "wan2", GatewayIp: "192.168.1.1"},
 				{LogicalIfName: "mgmt0", Dhcp: true},
+				{LogicalIfName: "wan0", IpAddress: "10.0.0.2"},
+				{LogicalIfName: "wan1", IpAddress: "10.0.0.3"},
 			},
 			expected: []map[string]interface{}{
 				{"logical_ifname": "wan0", "ip_address": "10.0.0.2"},
 				{"logical_ifname": "wan1", "ip_address": "10.0.0.3"},
-				{"logical_ifname": "wan2", "gateway_ip": "192.168.1.1"},
 				{"logical_ifname": "mgmt0", "dhcp": true},
+				{"logical_ifname": "wan2", "gateway_ip": "192.168.1.1"},
 			},
 		},
 		{
@@ -1183,7 +1186,7 @@ func TestSetInterfaceDetails(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			result := setInterfaceDetails(tt.interfaces)
+			result := setInterfaceDetails(tt.interfaces, interfaceOrder)
 			assert.Equal(t, tt.expected, result)
 		})
 	}
@@ -1435,3 +1438,130 @@ func TestParseInterface(t *testing.T) {
 		})
 	}
 }
+
+func TestGetUserInterfaceOrder(t *testing.T) {
+	tests := []struct {
+		name        string
+		interfaces  []interface{}
+		expected    []string
+		expectError bool
+	}{
+		{
+			name: "Valid interface list",
+			interfaces: []interface{}{
+				map[string]interface{}{"logical_ifname": "wan0"},
+				map[string]interface{}{"logical_ifname": "wan1"},
+				map[string]interface{}{"logical_ifname": "mgmt0"},
+			},
+			expected:    []string{"wan0", "wan1", "mgmt0"},
+			expectError: false,
+		},
+		{
+			name:        "Empty interface list",
+			interfaces:  []interface{}{},
+			expected:    []string{},
+			expectError: false,
+		},
+		{
+			name: "Interface is not a map[string]interface{}",
+			interfaces: []interface{}{
+				"invalid_entry",
+			},
+			expected:    nil,
+			expectError: true,
+		},
+		{
+			name: "Missing logical_ifname key",
+			interfaces: []interface{}{
+				map[string]interface{}{"interface_type": "WAN"},
+			},
+			expected:    nil,
+			expectError: true,
+		},
+		{
+			name: "Logical_ifname is not a string",
+			interfaces: []interface{}{
+				map[string]interface{}{"logical_ifname": 12345},
+			},
+			expected:    nil,
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result, err := getUserInterfaceOrder(tt.interfaces)
+			if tt.expectError {
+				assert.Error(t, err, "Expected an error but got nil")
+			} else {
+				assert.NoError(t, err, "Expected no error but got one")
+				assert.Equal(t, tt.expected, result)
+			}
+		})
+	}
+}
+
+func TestSortInterfacesByCustomOrder(t *testing.T) {
+	tests := []struct {
+		name               string
+		interfaces         []goaviatrix.EdgeTransitInterface
+		userInterfaceOrder []string
+		expected           []goaviatrix.EdgeTransitInterface
+	}{
+		{
+			name: "Sort interfaces based on custom order",
+			interfaces: []goaviatrix.EdgeTransitInterface{
+				{LogicalIfName: "wan2"},
+				{LogicalIfName: "wan0"},
+				{LogicalIfName: "mgmt0"},
+				{LogicalIfName: "wan1"},
+			},
+			userInterfaceOrder: []string{"wan0", "wan1", "mgmt0", "wan2"},
+			expected: []goaviatrix.EdgeTransitInterface{
+				{LogicalIfName: "wan0"},
+				{LogicalIfName: "wan1"},
+				{LogicalIfName: "mgmt0"},
+				{LogicalIfName: "wan2"},
+			},
+		},
+		{
+			name: "Handles interfaces not in custom order",
+			interfaces: []goaviatrix.EdgeTransitInterface{
+				{LogicalIfName: "extra0"},
+				{LogicalIfName: "wan1"},
+				{LogicalIfName: "mgmt0"},
+			},
+			userInterfaceOrder: []string{"wan1", "mgmt0"},
+			expected: []goaviatrix.EdgeTransitInterface{
+				{LogicalIfName: "wan1"},
+				{LogicalIfName: "mgmt0"},
+				{LogicalIfName: "extra0"},
+			},
+		},
+		{
+			name: "Handles empty custom order",
+			interfaces: []goaviatrix.EdgeTransitInterface{
+				{LogicalIfName: "wan2"},
+				{LogicalIfName: "wan0"},
+			},
+			userInterfaceOrder: []string{},
+			expected: []goaviatrix.EdgeTransitInterface{
+				{LogicalIfName: "wan2"},
+				{LogicalIfName: "wan0"},
+			},
+		},
+		{
+			name:               "Handles empty interface list",
+			interfaces:         []goaviatrix.EdgeTransitInterface{},
+			userInterfaceOrder: []string{"wan0", "wan1"},
+			expected:           []goaviatrix.EdgeTransitInterface{},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := sortInterfacesByCustomOrder(tt.interfaces, tt.userInterfaceOrder)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
diff --git a/aviatrix/utils.go b/aviatrix/utils.go
index 363cde0e1..f12120b47 100644
--- a/aviatrix/utils.go
+++ b/aviatrix/utils.go
@@ -377,11 +377,11 @@ func createOrderMap(order []string) map[string]int {
 }
 
 // Sorting function that uses the interface order
-func sortInterfacesByCustomOrder(interfaces []goaviatrix.EdgeTransitInterface) []goaviatrix.EdgeTransitInterface {
-	orderMap := createOrderMap(interfaceOrder)
+func sortInterfacesByCustomOrder(interfaces []goaviatrix.EdgeTransitInterface, userInterfaceOrder []string) []goaviatrix.EdgeTransitInterface {
+	orderMap := createOrderMap(userInterfaceOrder)
 	sort.SliceStable(interfaces, func(i, j int) bool {
-		iIndex, iExists := orderMap[interfaces[i].Name]
-		jIndex, jExists := orderMap[interfaces[j].Name]
+		iIndex, iExists := orderMap[interfaces[i].LogicalIfName]
+		jIndex, jExists := orderMap[interfaces[j].LogicalIfName]
 		if !iExists {
 			iIndex = len(orderMap)
 		}
diff --git a/goaviatrix/transit_vpc.go b/goaviatrix/transit_vpc.go
index 9f36ba5d2..0adb9ba7b 100644
--- a/goaviatrix/transit_vpc.go
+++ b/goaviatrix/transit_vpc.go
@@ -2,6 +2,7 @@ package goaviatrix
 
 import (
 	"bytes"
+	"context"
 	"encoding/json"
 	"fmt"
 	"log"
@@ -13,11 +14,11 @@ import (
 // Gateway simple struct to hold gateway details
 type TransitVpc struct {
 	AccountName                  string `form:"account_name,omitempty" json:"account_name,omitempty"`
-	Action                       string `form:"action,omitempty"`
-	CID                          string `form:"CID,omitempty"`
+	Action                       string `form:"action,omitempty" json:"action,omitempty"`
+	CID                          string `form:"CID,omitempty" json:"CID,omitempty"`
 	CloudType                    int    `form:"cloud_type,omitempty" json:"cloud_type,omitempty"`
 	DnsServer                    string `form:"dns_server,omitempty" json:"dns_server,omitempty"`
-	GwName                       string `form:"gw_name,omitempty" json:"vpc_name,omitempty"`
+	GwName                       string `form:"gw_name,omitempty" json:"name,omitempty"`
 	GwSize                       string `form:"gw_size,omitempty"`
 	VpcID                        string `form:"vpc_id,omitempty" json:"vpc_id,omitempty"`
 	VNetNameResourceGroup        string `form:"vnet_and_resource_group_names,omitempty"`
@@ -292,6 +293,12 @@ func (c *Client) UpdateEdgeGateway(gateway *TransitVpc) error {
 	return c.PostAPI(action, form, BasicCheck)
 }
 
+func (c *Client) UpdateEdgeGatewayV2(ctx context.Context, gateway *TransitVpc) error {
+	gateway.CID = c.CID
+	gateway.Action = "update_edge_gateway"
+	return c.PostAPIContext2(ctx, nil, gateway.Action, gateway, BasicCheck)
+}
+
 func (c *Client) DeleteEdgeGateway(gateway *Gateway) error {
 	form := map[string]string{
 		"CID":          c.CID,


Reviews:
user: login: tommy-carver
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-18T15:58:29Z
user: login: tommy-carver
state: APPROVED
author_association: NONE
submitted_at: 2025-03-18T16:23:46Z

Review comments:
diff_hunk: @@ -292,6 +293,12 @@ func (c *Client) UpdateEdgeGateway(gateway *TransitVpc) error {
 	return c.PostAPI(action, form, BasicCheck)
 }
 
+func (c *Client) UpdateTransitGateway(ctx context.Context, gateway *TransitVpc) error {
path: goaviatrix/transit_vpc.go
user: login: tommy-carver
body: This function is called update transit gateway, but calls update edge gateway. Since the change you mentioned switched us to using API v2, can we call this relevantly, UpdateEdgeGatewayV2?
created_at: 2025-03-18T15:58:04Z
author_association: NONE
diff_hunk: @@ -13,11 +14,11 @@ import (
 // Gateway simple struct to hold gateway details
 type TransitVpc struct {
 	AccountName                  string `form:"account_name,omitempty" json:"account_name,omitempty"`
-	Action                       string `form:"action,omitempty"`
-	CID                          string `form:"CID,omitempty"`
+	Action                       string `form:"action,omitempty" json:"action,omitempty"`
+	CID                          string `form:"CID,omitempty" json:"CID,omitempty"`
 	CloudType                    int    `form:"cloud_type,omitempty" json:"cloud_type,omitempty"`
 	DnsServer                    string `form:"dns_server,omitempty" json:"dns_server,omitempty"`
-	GwName                       string `form:"gw_name,omitempty" json:"vpc_name,omitempty"`
path: goaviatrix/transit_vpc.go
user: login: tommy-carver
body: Are there ramifications towards removing this?
created_at: 2025-03-18T15:58:18Z
author_association: NONE

PR comments:
user: login: tommy-carver
created_at: 2025-03-14T22:57:20Z
body: Can you elaborate more on what this PR is doing? I understand the update of the sort order for the interface, but the other changes I don't understand the purpose and "Updating the logical eip map update using the api version 2. The new version works with json object." confuses me slightly.
author_association: NONE
user: login: sairane10
created_at: 2025-03-17T15:10:38Z
body: @tommy-carver, the older code for updating the eip_map was sending the data as form string. For Megaport, we require eip_map to be provided as a json objects instead. In this PR, I have updated the eip_map code to use the API v2 version which accepts json object. This ensures that the eip_map gets updated correctly and the original object structure is passed directly for the update.
author_association: CONTRIBUTOR