Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ var ErrInvalidVXCAEndPartnerConfig = errors.New("invalid vxc a-end partner confi

// ErrInvalidVXCBEndPartnerConfig is returned when an invalid VXC B-End partner config is provided
var ErrInvalidVXCBEndPartnerConfig = errors.New("invalid vxc b-end partner config")

// ErrMCRCancelLaterNotAllowed is returned when attempting to schedule MCR deletion for later (only CANCEL_NOW is allowed)
var ErrMCRCancelLaterNotAllowed = errors.New("MCR products do not support scheduled deletion (cancel later). Only immediate deletion (CANCEL_NOW) is allowed per product lifecycle requirements")

// ErrTransitVXCCancelLaterNotAllowed is returned when attempting to schedule Transit VXC deletion for later (only CANCEL_NOW is allowed)
var ErrTransitVXCCancelLaterNotAllowed = errors.New("transit VXC (Megaport Internet) does not support scheduled deletion (cancel later). Only immediate deletion (CANCEL_NOW) is allowed per product lifecycle requirements")
10 changes: 9 additions & 1 deletion mcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,18 @@ func (svc *MCRServiceOp) ModifyMCRPrefixFilterList(ctx context.Context, mcrID st
}

// DeleteMCR deletes an MCR in the Megaport MCR API.
// Note: MCR products only support immediate deletion (CANCEL_NOW). Per API requirements,
// the DeleteNow flag will be automatically set to true. Attempting to schedule deletion
// for later (DeleteNow=false) will return an error.
func (svc *MCRServiceOp) DeleteMCR(ctx context.Context, req *DeleteMCRRequest) (*DeleteMCRResponse, error) {
// Enforce MCR lifecycle restriction: only CANCEL_NOW is allowed
if !req.DeleteNow {
return nil, ErrMCRCancelLaterNotAllowed
}

_, err := svc.Client.ProductService.DeleteProduct(ctx, &DeleteProductRequest{
ProductID: req.MCRID,
DeleteNow: req.DeleteNow,
DeleteNow: true, // Always use CANCEL_NOW for MCR
SafeDelete: req.SafeDelete,
})
if err != nil {
Expand Down
28 changes: 8 additions & 20 deletions mcr_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,30 +107,18 @@ func (suite *MCRIntegrationTestSuite) TestMCRLifecycle() {
}
suite.EqualValues(newMCRName, mcr.Name)

// Testing MCR Cancel
logger.InfoContext(ctx, "Scheduling MCR for deletion (30 days).", slog.String("mcr_id", mcrId))
// Testing MCR Cancel - MCR products do not support cancel later (only CANCEL_NOW)
logger.InfoContext(ctx, "Attempting to schedule MCR for deletion (cancel later) - this should fail.", slog.String("mcr_id", mcrId))

// This is a soft Delete
softDeleteRes, deleteErr := mcrSvc.DeleteMCR(ctx, &DeleteMCRRequest{
// Attempt soft delete - this should now fail because MCR only supports CANCEL_NOW
_, deleteErr := mcrSvc.DeleteMCR(ctx, &DeleteMCRRequest{
MCRID: mcrId,
DeleteNow: false,
})
if deleteErr != nil {
suite.FailNowf("could not soft delete mcr", "could not soft delete mcr %v", deleteErr)
}
suite.True(softDeleteRes.IsDeleting, true)

mcrCancelInfo, getErr := mcrSvc.GetMCR(ctx, mcrId)
if getErr != nil {
suite.FailNowf("could not get mcr", "could not get mcr %v", getErr)
}
suite.EqualValues(STATUS_CANCELLED, mcrCancelInfo.ProvisioningStatus)
logger.DebugContext(ctx, "MCR Canceled", slog.String("provisioning_status", mcrCancelInfo.ProvisioningStatus))
restoreRes, restoreErr := mcrSvc.RestoreMCR(ctx, mcrId)
if restoreErr != nil {
suite.FailNowf("could not restore mcr", "could not restore mcr %v", getErr)
}
suite.True(restoreRes.IsRestored)
// Expect error since MCR does not support cancel later
suite.Error(deleteErr, "expected error when attempting to cancel MCR later")
suite.ErrorIs(deleteErr, ErrMCRCancelLaterNotAllowed, "expected ErrMCRCancelLaterNotAllowed error")
logger.DebugContext(ctx, "MCR cancel later correctly rejected", slog.String("error", deleteErr.Error()))

// Testing MCR Delete
logger.InfoContext(ctx, "Deleting MCR now.")
Expand Down
30 changes: 29 additions & 1 deletion vxc.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,37 @@ func (svc *VXCServiceOp) ValidateVXCOrder(ctx context.Context, req *BuyVXCReques
return svc.Client.ProductService.ValidateProductOrder(ctx, buyOrder)
}

// isTransitVXC checks if a VXC is a Transit VXC (Megaport Internet) by examining the partner configuration.
// A VXC is considered a Transit VXC if either A-End or B-End has connectType "TRANSIT".
func isTransitVXC(vxc *VXC) bool {
if vxc == nil || vxc.Resources == nil || vxc.Resources.CSPConnection == nil {
return false
}

for _, csp := range vxc.Resources.CSPConnection.CSPConnection {
if transitCSP, ok := csp.(CSPConnectionTransit); ok && transitCSP.ConnectType == "TRANSIT" {
return true
}
}
return false
}

// DeleteVXC deletes a VXC in the Megaport VXC API.
// Note: Transit VXCs (Megaport Internet) only support immediate deletion (CANCEL_NOW).
// Attempting to schedule deletion (DeleteNow=false) for Transit VXCs will return an error.
func (svc *VXCServiceOp) DeleteVXC(ctx context.Context, id string, req *DeleteVXCRequest) error {
_, err := svc.Client.ProductService.DeleteProduct(ctx, &DeleteProductRequest{
// Check if this is a Transit VXC that requires immediate deletion
vxc, err := svc.GetVXC(ctx, id)
if err != nil {
return err
}

// Enforce Transit VXC lifecycle restriction: only CANCEL_NOW is allowed
if isTransitVXC(vxc) && !req.DeleteNow {
return ErrTransitVXCCancelLaterNotAllowed
}

_, err = svc.Client.ProductService.DeleteProduct(ctx, &DeleteProductRequest{
ProductID: id,
DeleteNow: req.DeleteNow,
})
Expand Down
218 changes: 214 additions & 4 deletions vxc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1077,23 +1077,233 @@ func (suite *VXCClientTestSuite) TestDeleteVXC() {
DeleteNow: true,
}

jblob := `{
// Mock GetVXC call (needed to check if VXC is Transit)
getVxcBlob := `{
"message": "Found Product 36b3f68e-2f54-4331-bf94-f8984449365f",
"terms": "This data is subject to the Acceptable Use Policy https://www.megaport.com/legal/acceptable-use-policy",
"data": {
"productId": 1,
"productUid": "36b3f68e-2f54-4331-bf94-f8984449365f",
"productName": "test-vxc",
"productType": "VXC",
"rateLimit": 50,
"provisioningStatus": "LIVE",
"resources": {
"csp_connection": {
"connectType": "AWS",
"resource_name": "csp_connection",
"resource_type": "csp_connection"
}
}
}
}`

getPath := "/v2/product/" + productUid
suite.mux.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) {
suite.testMethod(r, http.MethodGet)
fmt.Fprint(w, getVxcBlob)
})

deleteBlob := `{
"message": "Action [CANCEL_NOW Service 36b3f68e-2f54-4331-bf94-f8984449365f] has been done.",
"terms": "This data is subject to the Acceptable Use Policy https://www.megaport.com/legal/acceptable-use-policy"
}`

path := "/v3/product/" + productUid + "/action/CANCEL_NOW"
deletePath := "/v3/product/" + productUid + "/action/CANCEL_NOW"

suite.mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
suite.mux.HandleFunc(deletePath, func(w http.ResponseWriter, r *http.Request) {
suite.testMethod(r, http.MethodPost)
fmt.Fprint(w, jblob)
fmt.Fprint(w, deleteBlob)
})

err := vxcSvc.DeleteVXC(ctx, productUid, req)

suite.NoError(err)
}

// TestDeleteTransitVXCWithCancelLater tests that attempting to schedule deletion (cancel later) for a Transit VXC returns an error
func (suite *VXCClientTestSuite) TestDeleteTransitVXCWithCancelLater() {
ctx := context.Background()

vxcSvc := suite.client.VXCService
productUid := "36b3f68e-2f54-4331-bf94-f8984449365f"

req := &DeleteVXCRequest{
DeleteNow: false, // Attempt to schedule deletion
}

// Mock GetVXC call returning a Transit VXC
getVxcBlob := `{
"message": "Found Product 36b3f68e-2f54-4331-bf94-f8984449365f",
"terms": "This data is subject to the Acceptable Use Policy https://www.megaport.com/legal/acceptable-use-policy",
"data": {
"productId": 1,
"productUid": "36b3f68e-2f54-4331-bf94-f8984449365f",
"productName": "test-transit-vxc",
"productType": "VXC",
"rateLimit": 50,
"provisioningStatus": "LIVE",
"resources": {
"csp_connection": {
"connectType": "TRANSIT",
"resource_name": "csp_connection",
"resource_type": "csp_connection",
"customer_ip4_address": "203.0.113.1/30",
"ipv4_gateway_address": "203.0.113.2"
}
}
}
}`

getPath := "/v2/product/" + productUid
suite.mux.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) {
suite.testMethod(r, http.MethodGet)
fmt.Fprint(w, getVxcBlob)
})

err := vxcSvc.DeleteVXC(ctx, productUid, req)

suite.Error(err, "expected error when attempting to cancel Transit VXC later")
suite.ErrorIs(err, ErrTransitVXCCancelLaterNotAllowed, "expected ErrTransitVXCCancelLaterNotAllowed")
}

// TestDeleteTransitVXCWithDeleteNow tests that immediate deletion of a Transit VXC succeeds
func (suite *VXCClientTestSuite) TestDeleteTransitVXCWithDeleteNow() {
ctx := context.Background()

vxcSvc := suite.client.VXCService
productUid := "36b3f68e-2f54-4331-bf94-f8984449365f"

req := &DeleteVXCRequest{
DeleteNow: true, // Immediate deletion
}

// Mock GetVXC call returning a Transit VXC
getVxcBlob := `{
"message": "Found Product 36b3f68e-2f54-4331-bf94-f8984449365f",
"terms": "This data is subject to the Acceptable Use Policy https://www.megaport.com/legal/acceptable-use-policy",
"data": {
"productId": 1,
"productUid": "36b3f68e-2f54-4331-bf94-f8984449365f",
"productName": "test-transit-vxc",
"productType": "VXC",
"rateLimit": 50,
"provisioningStatus": "LIVE",
"resources": {
"csp_connection": {
"connectType": "TRANSIT",
"resource_name": "csp_connection",
"resource_type": "csp_connection",
"customer_ip4_address": "203.0.113.1/30",
"ipv4_gateway_address": "203.0.113.2"
}
}
}
}`

getPath := "/v2/product/" + productUid
suite.mux.HandleFunc(getPath, func(w http.ResponseWriter, r *http.Request) {
suite.testMethod(r, http.MethodGet)
fmt.Fprint(w, getVxcBlob)
})

deleteBlob := `{
"message": "Action [CANCEL_NOW Service 36b3f68e-2f54-4331-bf94-f8984449365f] has been done.",
"terms": "This data is subject to the Acceptable Use Policy https://www.megaport.com/legal/acceptable-use-policy"
}`

deletePath := "/v3/product/" + productUid + "/action/CANCEL_NOW"

suite.mux.HandleFunc(deletePath, func(w http.ResponseWriter, r *http.Request) {
suite.testMethod(r, http.MethodPost)
fmt.Fprint(w, deleteBlob)
})

err := vxcSvc.DeleteVXC(ctx, productUid, req)

suite.NoError(err, "expected no error when deleting Transit VXC with DeleteNow=true")
}

// TestIsTransitVXC tests the isTransitVXC helper function with various VXC types
func (suite *VXCClientTestSuite) TestIsTransitVXC() {
// Test nil VXC
suite.False(isTransitVXC(nil), "nil VXC should return false")

// Test VXC with no resources
vxc := &VXC{}
suite.False(isTransitVXC(vxc), "VXC with no resources should return false")

// Test VXC with no CSP connection
vxc.Resources = &VXCResources{}
suite.False(isTransitVXC(vxc), "VXC with no CSP connection should return false")

// Test Transit VXC
transitVXC := &VXC{
Resources: &VXCResources{
CSPConnection: &CSPConnection{
CSPConnection: []CSPConnectionConfig{
CSPConnectionTransit{
ConnectType: "TRANSIT",
ResourceName: "csp_connection",
ResourceType: "csp_connection",
CustomerIP4Address: "203.0.113.1/30",
IPv4GatewayAddress: "203.0.113.2",
},
},
},
},
}
suite.True(isTransitVXC(transitVXC), "VXC with TRANSIT connectType should return true")

// Test AWS VXC (non-Transit)
awsVXC := &VXC{
Resources: &VXCResources{
CSPConnection: &CSPConnection{
CSPConnection: []CSPConnectionConfig{
CSPConnectionAWS{
ConnectType: "AWS",
ResourceName: "csp_connection",
ResourceType: "csp_connection",
},
},
},
},
}
suite.False(isTransitVXC(awsVXC), "VXC with AWS connectType should return false")

// Test Azure VXC (non-Transit)
azureVXC := &VXC{
Resources: &VXCResources{
CSPConnection: &CSPConnection{
CSPConnection: []CSPConnectionConfig{
CSPConnectionAzure{
ConnectType: "AZURE",
ResourceName: "csp_connection",
ResourceType: "csp_connection",
},
},
},
},
}
suite.False(isTransitVXC(azureVXC), "VXC with AZURE connectType should return false")

// Test Google VXC (non-Transit)
googleVXC := &VXC{
Resources: &VXCResources{
CSPConnection: &CSPConnection{
CSPConnection: []CSPConnectionConfig{
CSPConnectionGoogle{
ConnectType: "GOOGLE",
ResourceName: "csp_connection",
ResourceType: "csp_connection",
},
},
},
},
}
suite.False(isTransitVXC(googleVXC), "VXC with GOOGLE connectType should return false")
}

// TestDeleteVXC tests to see if the custom unmarshalling works for decommed VXCs.
func (suite *VXCClientTestSuite) TestDecomissionedVXCMarshal() {
ctx := context.Background()
Expand Down