• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubeovn / kube-ovn / 22985042443

12 Mar 2026 03:19AM UTC coverage: 23.022% (-0.01%) from 23.036%
22985042443

push

github

oilbeater
fix: use targeted patch in calcSubnetStatusIP to prevent U2O status overwrite (#6350)

calcSubnetStatusIP previously used subnet.Status.Bytes() which serialized
the entire SubnetStatus and patched all fields to etcd. This caused a race
condition where handleUpdateSubnetStatus could overwrite U2OInterconnectionVPC
with stale data from its informer cache, leading to flaky e2e test failures
in "should support underlay to overlay subnet interconnection".

The race condition occurs when:
1. handleAddOrUpdateSubnet sets U2OInterconnectionVPC and patches status
2. handleUpdateSubnetStatus retries (from IP inconsistency requeue), reads
   stale cache without U2OInterconnectionVPC, and calcSubnetStatusIP
   overwrites all status fields including U2OInterconnectionVPC=""

Fix by using a targeted JSON merge patch that only includes the 8 IP-related
fields, leaving non-IP fields like U2OInterconnectionVPC untouched.

Signed-off-by: Mengxin Liu <liumengxinfly@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

0 of 24 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

12501 of 54300 relevant lines covered (23.02%)

0.27 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

5.65
/pkg/controller/subnet_status.go
1
package controller
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "fmt"
7
        "net"
8
        "strconv"
9
        "strings"
10

11
        v1 "k8s.io/api/core/v1"
12
        k8serrors "k8s.io/apimachinery/pkg/api/errors"
13
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14
        "k8s.io/apimachinery/pkg/labels"
15
        "k8s.io/apimachinery/pkg/types"
16
        "k8s.io/klog/v2"
17

18
        kubeovnv1 "github.com/kubeovn/kube-ovn/pkg/apis/kubeovn/v1"
19
        "github.com/kubeovn/kube-ovn/pkg/util"
20
)
21

22
func (c *Controller) updateNatOutgoingPolicyRulesStatus(subnet *kubeovnv1.Subnet) error {
×
23
        if subnet.Spec.NatOutgoing {
×
24
                subnet.Status.NatOutgoingPolicyRules = make([]kubeovnv1.NatOutgoingPolicyRuleStatus, len(subnet.Spec.NatOutgoingPolicyRules))
×
25
                for index, rule := range subnet.Spec.NatOutgoingPolicyRules {
×
26
                        jsonRule, err := json.Marshal(rule)
×
27
                        if err != nil {
×
28
                                klog.Error(err)
×
29
                                return err
×
30
                        }
×
31
                        priority := strconv.Itoa(index)
×
32
                        var retBytes []byte
×
33
                        retBytes = append(retBytes, []byte(subnet.Name)...)
×
34
                        retBytes = append(retBytes, []byte(priority)...)
×
35
                        retBytes = append(retBytes, jsonRule...)
×
36
                        result := util.Sha256Hash(retBytes)
×
37

×
38
                        subnet.Status.NatOutgoingPolicyRules[index].RuleID = result[:util.NatPolicyRuleIDLength]
×
39
                        subnet.Status.NatOutgoingPolicyRules[index].Match = rule.Match
×
40
                        subnet.Status.NatOutgoingPolicyRules[index].Action = rule.Action
×
41
                }
42
        } else {
×
43
                subnet.Status.NatOutgoingPolicyRules = []kubeovnv1.NatOutgoingPolicyRuleStatus{}
×
44
        }
×
45

46
        return nil
×
47
}
48

49
func (c *Controller) patchSubnetStatus(subnet *kubeovnv1.Subnet, reason, errStr string) error {
1✔
50
        if errStr != "" {
2✔
51
                subnet.Status.SetError(reason, errStr)
1✔
52
                if reason == "ValidateLogicalSwitchFailed" {
1✔
53
                        subnet.Status.NotValidated(reason, errStr)
×
54
                } else {
1✔
55
                        subnet.Status.Validated(reason, "")
1✔
56
                }
1✔
57
                subnet.Status.NotReady(reason, errStr)
1✔
58
                c.recorder.Eventf(subnet, v1.EventTypeWarning, reason, errStr)
1✔
59
        } else {
×
60
                subnet.Status.Validated(reason, "")
×
61
                c.recorder.Eventf(subnet, v1.EventTypeNormal, reason, errStr)
×
62
                if reason == "SetPrivateLogicalSwitchSuccess" ||
×
63
                        reason == "ResetLogicalSwitchAclSuccess" ||
×
64
                        reason == "ReconcileCentralizedGatewaySuccess" ||
×
65
                        reason == "SetNonOvnSubnetSuccess" {
×
66
                        subnet.Status.Ready(reason, "")
×
67
                }
×
68
        }
69

70
        bytes, err := subnet.Status.Bytes()
1✔
71
        if err != nil {
1✔
72
                klog.Error(err)
×
73
                return err
×
74
        }
×
75
        if _, err := c.config.KubeOvnClient.KubeovnV1().Subnets().Patch(context.Background(), subnet.Name, types.MergePatchType, bytes, metav1.PatchOptions{}, "status"); err != nil {
1✔
76
                klog.Errorf("failed to patch status for subnet %s, %v", subnet.Name, err)
×
77
                return err
×
78
        }
×
79
        return nil
1✔
80
}
81

82
func (c *Controller) handleUpdateSubnetStatus(key string) error {
×
83
        c.subnetKeyMutex.LockKey(key)
×
84
        defer func() { _ = c.subnetKeyMutex.UnlockKey(key) }()
×
85

86
        cachedSubnet, err := c.subnetsLister.Get(key)
×
87
        subnet := cachedSubnet.DeepCopy()
×
88
        if err != nil {
×
89
                if k8serrors.IsNotFound(err) {
×
90
                        return nil
×
91
                }
×
92
                klog.Error(err)
×
93
                return err
×
94
        }
95

96
        ippools, err := c.ippoolLister.List(labels.Everything())
×
97
        if err != nil {
×
98
                klog.Errorf("failed to list ippool: %v", err)
×
99
                return err
×
100
        }
×
101
        for _, p := range ippools {
×
102
                if p.Spec.Subnet == subnet.Name {
×
103
                        c.updateIPPoolStatusQueue.Add(p.Name)
×
104
                }
×
105
        }
106

107
        if _, err = c.calcSubnetStatusIP(subnet); err != nil {
×
108
                klog.Error(err)
×
109
                return err
×
110
        }
×
111

112
        if err := c.checkSubnetUsingIPs(subnet); err != nil {
×
113
                klog.Errorf("inconsistency detected in status of subnet %s : %v", subnet.Name, err)
×
114
                return err
×
115
        }
×
116
        return nil
×
117
}
118

119
func filterNonGatewayExcludeIPs(subnet *kubeovnv1.Subnet) []string {
×
120
        noGWExcludeIPs := []string{}
×
121
        v4gw, v6gw := util.SplitStringIP(subnet.Spec.Gateway)
×
122
        for _, excludeIP := range subnet.Spec.ExcludeIps {
×
123
                if v4gw == excludeIP || v6gw == excludeIP {
×
124
                        continue
×
125
                }
126
                noGWExcludeIPs = append(noGWExcludeIPs, excludeIP)
×
127
        }
128
        return noGWExcludeIPs
×
129
}
130

131
func (c *Controller) calculateUsingIPs(subnet *kubeovnv1.Subnet, podUsedIPs []*kubeovnv1.IP, noGWExcludeIPs []string) (float64, error) {
×
132
        usingIPNums := len(podUsedIPs)
×
133

×
134
        if len(noGWExcludeIPs) > 0 {
×
135
                for _, podUsedIP := range podUsedIPs {
×
136
                        for _, excludeIP := range noGWExcludeIPs {
×
137
                                if util.ContainsIPs(excludeIP, podUsedIP.Spec.V4IPAddress) || util.ContainsIPs(excludeIP, podUsedIP.Spec.V6IPAddress) {
×
138
                                        usingIPNums--
×
139
                                        break
×
140
                                }
141
                        }
142
                }
143
        }
144

145
        usingIPs := float64(usingIPNums)
×
146

×
147
        vips, err := c.virtualIpsLister.List(labels.SelectorFromSet(labels.Set{
×
148
                util.SubnetNameLabel: subnet.Name,
×
149
                util.IPReservedLabel: "",
×
150
        }))
×
151
        if err != nil {
×
152
                return 0, err
×
153
        }
×
154
        usingIPs += float64(len(vips))
×
155

×
156
        eips, err := c.iptablesEipsLister.List(
×
157
                labels.SelectorFromSet(labels.Set{util.SubnetNameLabel: subnet.Name}))
×
158
        if err != nil {
×
159
                return 0, err
×
160
        }
×
161
        usingIPs += float64(len(eips))
×
162

×
163
        ovnEips, err := c.ovnEipsLister.List(labels.SelectorFromSet(labels.Set{
×
164
                util.SubnetNameLabel: subnet.Name,
×
165
        }))
×
166
        if err != nil {
×
167
                return 0, err
×
168
        }
×
169
        usingIPs += float64(len(ovnEips))
×
170

×
171
        return usingIPs, nil
×
172
}
173

174
func (c *Controller) calcSubnetStatusIP(subnet *kubeovnv1.Subnet) (*kubeovnv1.Subnet, error) {
×
175
        if err := util.CheckCidrs(subnet.Spec.CIDRBlock); err != nil {
×
176
                return nil, err
×
177
        }
×
178

179
        podUsedIPs, err := c.ipsLister.List(labels.SelectorFromSet(labels.Set{subnet.Name: ""}))
×
180
        if err != nil {
×
181
                klog.Error(err)
×
182
                return nil, err
×
183
        }
×
184

185
        noGWExcludeIPs := filterNonGatewayExcludeIPs(subnet)
×
186
        usingIPs, err := c.calculateUsingIPs(subnet, podUsedIPs, noGWExcludeIPs)
×
187
        if err != nil {
×
188
                klog.Error(err)
×
189
                return nil, err
×
190
        }
×
191

192
        var v4availableIPs, v6availableIPs float64
×
193
        v4UsingIPStr, v6UsingIPStr, v4AvailableIPStr, v6AvailableIPStr := c.ipam.GetSubnetIPRangeString(subnet.Name, subnet.Spec.ExcludeIps)
×
194

×
195
        switch subnet.Spec.Protocol {
×
196
        case kubeovnv1.ProtocolDual:
×
197
                v4ExcludeIPs, v6ExcludeIPs := util.SplitIpsByProtocol(subnet.Spec.ExcludeIps)
×
198
                cidrBlocks := strings.Split(subnet.Spec.CIDRBlock, ",")
×
199
                v4toSubIPs := util.ExpandExcludeIPs(v4ExcludeIPs, cidrBlocks[0])
×
200
                v6toSubIPs := util.ExpandExcludeIPs(v6ExcludeIPs, cidrBlocks[1])
×
201
                _, v4CIDR, _ := net.ParseCIDR(cidrBlocks[0])
×
202
                _, v6CIDR, _ := net.ParseCIDR(cidrBlocks[1])
×
203
                v4availableIPs = util.AddressCount(v4CIDR) - util.CountIPNums(v4toSubIPs) - usingIPs
×
204
                v6availableIPs = util.AddressCount(v6CIDR) - util.CountIPNums(v6toSubIPs) - usingIPs
×
205
        case kubeovnv1.ProtocolIPv4:
×
206
                _, cidr, _ := net.ParseCIDR(subnet.Spec.CIDRBlock)
×
207
                toSubIPs := util.ExpandExcludeIPs(subnet.Spec.ExcludeIps, subnet.Spec.CIDRBlock)
×
208
                v4availableIPs = util.AddressCount(cidr) - util.CountIPNums(toSubIPs) - usingIPs
×
209
        case kubeovnv1.ProtocolIPv6:
×
210
                _, cidr, _ := net.ParseCIDR(subnet.Spec.CIDRBlock)
×
211
                toSubIPs := util.ExpandExcludeIPs(subnet.Spec.ExcludeIps, subnet.Spec.CIDRBlock)
×
212
                v6availableIPs = util.AddressCount(cidr) - util.CountIPNums(toSubIPs) - usingIPs
×
213
        }
214

215
        v4availableIPs = max(v4availableIPs, 0)
×
216
        v6availableIPs = max(v6availableIPs, 0)
×
217

×
218
        v4UsingIPs, v6UsingIPs := usingIPs, usingIPs
×
219
        switch subnet.Spec.Protocol {
×
220
        case kubeovnv1.ProtocolIPv4:
×
221
                v6UsingIPs = 0
×
222
        case kubeovnv1.ProtocolIPv6:
×
223
                v4UsingIPs = 0
×
224
        }
225

226
        if subnet.Status.V4AvailableIPs == v4availableIPs &&
×
227
                subnet.Status.V6AvailableIPs == v6availableIPs &&
×
228
                subnet.Status.V4UsingIPs == v4UsingIPs &&
×
229
                subnet.Status.V6UsingIPs == v6UsingIPs &&
×
230
                subnet.Status.V4UsingIPRange == v4UsingIPStr &&
×
231
                subnet.Status.V6UsingIPRange == v6UsingIPStr &&
×
232
                subnet.Status.V4AvailableIPRange == v4AvailableIPStr &&
×
233
                subnet.Status.V6AvailableIPRange == v6AvailableIPStr {
×
234
                return subnet, nil
×
235
        }
×
236

237
        subnet.Status.V4AvailableIPs = v4availableIPs
×
238
        subnet.Status.V6AvailableIPs = v6availableIPs
×
239
        subnet.Status.V4UsingIPs = v4UsingIPs
×
240
        subnet.Status.V6UsingIPs = v6UsingIPs
×
241
        subnet.Status.V4UsingIPRange = v4UsingIPStr
×
242
        subnet.Status.V6UsingIPRange = v6UsingIPStr
×
243
        subnet.Status.V4AvailableIPRange = v4AvailableIPStr
×
244
        subnet.Status.V6AvailableIPRange = v6AvailableIPStr
×
NEW
245

×
NEW
246
        // Use a targeted patch with only IP-related fields to avoid overwriting
×
NEW
247
        // non-IP status fields (e.g., U2OInterconnectionVPC) set by other handlers.
×
NEW
248
        ipStatusPatch := struct {
×
NEW
249
                Status struct {
×
NEW
250
                        V4AvailableIPs     float64 `json:"v4availableIPs"`
×
NEW
251
                        V4AvailableIPRange string  `json:"v4availableIPrange"`
×
NEW
252
                        V4UsingIPs         float64 `json:"v4usingIPs"`
×
NEW
253
                        V4UsingIPRange     string  `json:"v4usingIPrange"`
×
NEW
254
                        V6AvailableIPs     float64 `json:"v6availableIPs"`
×
NEW
255
                        V6AvailableIPRange string  `json:"v6availableIPrange"`
×
NEW
256
                        V6UsingIPs         float64 `json:"v6usingIPs"`
×
NEW
257
                        V6UsingIPRange     string  `json:"v6usingIPrange"`
×
NEW
258
                } `json:"status"`
×
NEW
259
        }{}
×
NEW
260
        ipStatusPatch.Status.V4AvailableIPs = v4availableIPs
×
NEW
261
        ipStatusPatch.Status.V4AvailableIPRange = v4AvailableIPStr
×
NEW
262
        ipStatusPatch.Status.V4UsingIPs = v4UsingIPs
×
NEW
263
        ipStatusPatch.Status.V4UsingIPRange = v4UsingIPStr
×
NEW
264
        ipStatusPatch.Status.V6AvailableIPs = v6availableIPs
×
NEW
265
        ipStatusPatch.Status.V6AvailableIPRange = v6AvailableIPStr
×
NEW
266
        ipStatusPatch.Status.V6UsingIPs = v6UsingIPs
×
NEW
267
        ipStatusPatch.Status.V6UsingIPRange = v6UsingIPStr
×
NEW
268
        bytes, err := json.Marshal(ipStatusPatch)
×
269
        if err != nil {
×
270
                klog.Error(err)
×
271
                return nil, err
×
272
        }
×
273
        newSubnet, err := c.config.KubeOvnClient.KubeovnV1().Subnets().Patch(context.Background(), subnet.Name, types.MergePatchType, bytes, metav1.PatchOptions{}, "status")
×
274
        return newSubnet, err
×
275
}
276

277
func (c *Controller) checkSubnetUsingIPs(subnet *kubeovnv1.Subnet) error {
×
278
        if subnet.Status.V4UsingIPs != 0 && subnet.Status.V4UsingIPRange == "" {
×
279
                err := fmt.Errorf("subnet %s has %.0f v4 ip in use, while the v4 using ip range is empty", subnet.Name, subnet.Status.V4UsingIPs)
×
280
                klog.Error(err)
×
281
                return err
×
282
        }
×
283
        if subnet.Status.V6UsingIPs != 0 && subnet.Status.V6UsingIPRange == "" {
×
284
                err := fmt.Errorf("subnet %s has %.0f v6 ip in use, while the v6 using ip range is empty", subnet.Name, subnet.Status.V6UsingIPs)
×
285
                klog.Error(err)
×
286
                return err
×
287
        }
×
288
        return nil
×
289
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc