Skip to content

Commit 6024c79

Browse files
Make NetcfgSubnetRangeOutsideVnet error retryable for VirtualNetworksSubnet (#4931)
* Initial plan * Implement ErrorClassifier for VirtualNetworksSubnet to make NetcfgSubnetRangeOutsideVnet retryable Co-authored-by: theunrepentantgeek <[email protected]> * Format code for copilot * Simplify testing --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: theunrepentantgeek <[email protected]> Co-authored-by: Bevan Arps <[email protected]>
1 parent 702dd69 commit 6024c79

File tree

2 files changed

+116
-1
lines changed

2 files changed

+116
-1
lines changed

v2/api/network/customizations/virtual_network_subnet_extensions.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ import (
1313
"github.com/Azure/azure-service-operator/v2/internal/genericarmclient"
1414
"github.com/Azure/azure-service-operator/v2/internal/resolver"
1515
"github.com/Azure/azure-service-operator/v2/pkg/genruntime"
16+
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
1617
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/extensions"
1718
)
1819

19-
var _ extensions.PostReconciliationChecker = &PrivateEndpointExtension{}
20+
var (
21+
_ extensions.PostReconciliationChecker = &VirtualNetworksSubnetExtension{}
22+
_ extensions.ErrorClassifier = &VirtualNetworksSubnetExtension{}
23+
)
2024

2125
func (extension *VirtualNetworksSubnetExtension) PostReconcileCheck(
2226
_ context.Context,
@@ -50,3 +54,36 @@ func (extension *VirtualNetworksSubnetExtension) PostReconcileCheck(
5054

5155
return extensions.PostReconcileCheckResultSuccess(), nil
5256
}
57+
58+
func (extension *VirtualNetworksSubnetExtension) ClassifyError(
59+
cloudError *genericarmclient.CloudError,
60+
apiVersion string,
61+
log logr.Logger,
62+
next extensions.ErrorClassifierFunc,
63+
) (core.CloudErrorDetails, error) {
64+
details, err := next(cloudError)
65+
if err != nil {
66+
return core.CloudErrorDetails{}, err
67+
}
68+
69+
if isRetryableSubnetError(cloudError) {
70+
details.Classification = core.ErrorRetryable
71+
}
72+
73+
return details, nil
74+
}
75+
76+
func isRetryableSubnetError(err *genericarmclient.CloudError) bool {
77+
if err == nil {
78+
return false
79+
}
80+
81+
// NetcfgSubnetRangeOutsideVnet occurs when a subnet's IP range is outside the VNet's allowed ranges.
82+
// This can happen during simultaneous VNet and Subnet changes when the subnet is reconciled before the VNet.
83+
// This should be retryable as the VNet may be updated soon to include the required range.
84+
if err.Code() == "NetcfgSubnetRangeOutsideVnet" {
85+
return true
86+
}
87+
88+
return false
89+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright (c) Microsoft Corporation.
3+
Licensed under the MIT license.
4+
*/
5+
6+
package customizations
7+
8+
import (
9+
"testing"
10+
11+
. "github.com/onsi/gomega"
12+
13+
"github.com/go-logr/logr"
14+
15+
"github.com/Azure/azure-service-operator/v2/internal/genericarmclient"
16+
"github.com/Azure/azure-service-operator/v2/pkg/genruntime/core"
17+
)
18+
19+
func Test_VirtualNetworksSubnetExtension_ClassifyError_NetcfgSubnetRangeOutsideVnet_IsRetryable(t *testing.T) {
20+
t.Parallel()
21+
22+
cases := map[string]struct {
23+
errorCode string
24+
errorMessage string
25+
mockReturns core.ErrorClassification
26+
expected core.ErrorClassification
27+
}{
28+
"Fatal error is still fatal": {
29+
errorCode: "BadRequest",
30+
errorMessage: "Invalid parameter value",
31+
mockReturns: core.ErrorFatal,
32+
expected: core.ErrorFatal,
33+
},
34+
"Other error is still retryable": {
35+
errorCode: "OtherError",
36+
errorMessage: "Some other error occurred",
37+
mockReturns: core.ErrorRetryable,
38+
expected: core.ErrorRetryable,
39+
},
40+
"SubnetRangeOutsideVnet error is retryable": {
41+
errorCode: "NetcfgSubnetRangeOutsideVnet",
42+
errorMessage: "Subnet 'testsubnet' is not valid because its IP address range is outside the IP address range of virtual network 'testvnet'.",
43+
mockReturns: core.ErrorRetryable,
44+
expected: core.ErrorRetryable,
45+
},
46+
}
47+
48+
for name, c := range cases {
49+
t.Run(name, func(t *testing.T) {
50+
t.Parallel()
51+
g := NewGomegaWithT(t)
52+
53+
extension := &VirtualNetworksSubnetExtension{}
54+
55+
// Create a test error with the specific code
56+
err := genericarmclient.NewTestCloudError(c.errorCode, c.errorMessage)
57+
58+
// Mock the next classifier function - this would normally classify as fatal
59+
mockCalled := false
60+
next := func(cloudError *genericarmclient.CloudError) (core.CloudErrorDetails, error) {
61+
mockCalled = true
62+
return core.CloudErrorDetails{
63+
Classification: c.mockReturns,
64+
Code: cloudError.Code(),
65+
Message: cloudError.Message(),
66+
}, nil
67+
}
68+
69+
details, classifyErr := extension.ClassifyError(err, "2024-03-01", logr.Discard(), next)
70+
71+
g.Expect(classifyErr).ToNot(HaveOccurred())
72+
g.Expect(details.Classification).To(Equal(c.expected))
73+
g.Expect(details.Code).To(Equal(c.errorCode))
74+
g.Expect(details.Message).To(ContainSubstring(c.errorMessage))
75+
g.Expect(mockCalled).To(BeTrue())
76+
})
77+
}
78+
}

0 commit comments

Comments
 (0)