Skip to content

Commit 15a5262

Browse files
mtuliokmala
authored andcommitted
fix: rule difference for nested objects
The Difference method calculation are returning wrong values on service update calculating the security group rules. The core issue is caused by it is always comparing the pointer address, not the values, always generating difference between lists. Example: given s1:{a1, a2} and s2:{a2,a3,a4} s2.Difference(s1) was returning {a2, a3, a4}, instead {a3, a4}
1 parent 7657df5 commit 15a5262

2 files changed

Lines changed: 272 additions & 4 deletions

File tree

pkg/providers/v1/sets_ippermissions.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,24 @@ func (s IPPermissionSet) Equal(s2 IPPermissionSet) bool {
141141
return len(s) == len(s2) && s.IsSuperset(s2)
142142
}
143143

144-
// Difference returns a set of objects that are not in s2
144+
// Difference returns a set of objects that are not in s2.
145145
// For example:
146146
// s1 = {a1, a2, a3}
147147
// s2 = {a1, a2, a4, a5}
148148
// s1.Difference(s2) = {a3}
149149
// s2.Difference(s1) = {a4, a5}
150150
func (s IPPermissionSet) Difference(s2 IPPermissionSet) IPPermissionSet {
151151
result := NewIPPermissionSet()
152-
for k, v := range s {
153-
_, found := s2[k]
152+
for _, desired := range s.List() {
153+
found := false
154+
for _, existing := range s2.List() {
155+
if ipPermissionExists(&desired, &existing, false) {
156+
found = true
157+
break
158+
}
159+
}
154160
if !found {
155-
result[k] = v
161+
result.Insert(desired)
156162
}
157163
}
158164
return result

pkg/providers/v1/sets_ippermissions_test.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/aws/aws-sdk-go-v2/aws"
77
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
8+
"github.com/stretchr/testify/assert"
89
)
910

1011
func TestUngroup(t *testing.T) {
@@ -156,3 +157,264 @@ func TestUngroup(t *testing.T) {
156157
})
157158
}
158159
}
160+
161+
func TestIPPermissionSetDifferenceCriticalScenarios(t *testing.T) {
162+
t.Run("real_world_nlb_security_group_scenario", func(t *testing.T) {
163+
// Scenario:
164+
// Desired: tcp:80, tcp:81, icmp:3-4
165+
// Actual: tcp:80, icmp:3-4
166+
// Expected: add tcp:81 only, remove nothing
167+
168+
desired := NewIPPermissionSet(
169+
ec2types.IpPermission{
170+
IpProtocol: aws.String("tcp"),
171+
FromPort: aws.Int32(80),
172+
ToPort: aws.Int32(80),
173+
IpRanges: []ec2types.IpRange{
174+
{CidrIp: aws.String("0.0.0.0/0")},
175+
},
176+
},
177+
ec2types.IpPermission{
178+
IpProtocol: aws.String("tcp"),
179+
FromPort: aws.Int32(81),
180+
ToPort: aws.Int32(81),
181+
IpRanges: []ec2types.IpRange{
182+
{CidrIp: aws.String("0.0.0.0/0")},
183+
},
184+
},
185+
ec2types.IpPermission{
186+
IpProtocol: aws.String("icmp"),
187+
FromPort: aws.Int32(3),
188+
ToPort: aws.Int32(4),
189+
IpRanges: []ec2types.IpRange{
190+
{CidrIp: aws.String("0.0.0.0/0")},
191+
},
192+
},
193+
)
194+
195+
actual := NewIPPermissionSet(
196+
ec2types.IpPermission{
197+
IpProtocol: aws.String("tcp"),
198+
FromPort: aws.Int32(80),
199+
ToPort: aws.Int32(80),
200+
IpRanges: []ec2types.IpRange{
201+
{CidrIp: aws.String("0.0.0.0/0")},
202+
},
203+
},
204+
ec2types.IpPermission{
205+
IpProtocol: aws.String("icmp"),
206+
FromPort: aws.Int32(3),
207+
ToPort: aws.Int32(4),
208+
IpRanges: []ec2types.IpRange{
209+
{CidrIp: aws.String("0.0.0.0/0")},
210+
},
211+
},
212+
)
213+
214+
// Calculate what should be added and removed
215+
add := desired.Difference(actual)
216+
remove := actual.Difference(desired)
217+
218+
// Verify correct results
219+
assert.Equal(t, 1, add.Len(), "Should add exactly one permission (tcp:81)")
220+
assert.Equal(t, 0, remove.Len(), "Should remove no permissions")
221+
222+
// Verify the added permission is tcp:81
223+
addList := add.List()
224+
if len(addList) > 0 {
225+
perm := addList[0]
226+
assert.Equal(t, "tcp", aws.ToString(perm.IpProtocol))
227+
assert.Equal(t, int32(81), aws.ToInt32(perm.FromPort))
228+
assert.Equal(t, int32(81), aws.ToInt32(perm.ToPort))
229+
}
230+
})
231+
232+
t.Run("empty_sets_and_edge_cases", func(t *testing.T) {
233+
// Test edge cases with empty sets and nil scenarios
234+
235+
emptySet := NewIPPermissionSet()
236+
nonEmptySet := NewIPPermissionSet(
237+
ec2types.IpPermission{
238+
IpProtocol: aws.String("tcp"),
239+
FromPort: aws.Int32(443),
240+
ToPort: aws.Int32(443),
241+
IpRanges: []ec2types.IpRange{
242+
{CidrIp: aws.String("10.0.0.0/8")},
243+
},
244+
},
245+
)
246+
247+
// Empty - NonEmpty should return empty
248+
diff1 := emptySet.Difference(nonEmptySet)
249+
assert.Equal(t, 0, diff1.Len(), "Empty set difference with non-empty should be empty")
250+
251+
// NonEmpty - Empty should return all from NonEmpty
252+
diff2 := nonEmptySet.Difference(emptySet)
253+
assert.Equal(t, 1, diff2.Len(), "Non-empty set difference with empty should return all permissions")
254+
255+
// Empty - Empty should return empty
256+
diff3 := emptySet.Difference(emptySet)
257+
assert.Equal(t, 0, diff3.Len(), "Empty set difference with empty should be empty")
258+
})
259+
260+
t.Run("initialization_issue_prevention", func(t *testing.T) {
261+
// Test that demonstrates the importance of proper initialization
262+
// This prevents the bug where variables were declared as `var add IPPermissionSet`
263+
264+
sourceSet := NewIPPermissionSet(
265+
ec2types.IpPermission{
266+
IpProtocol: aws.String("tcp"),
267+
FromPort: aws.Int32(80),
268+
ToPort: aws.Int32(80),
269+
IpRanges: []ec2types.IpRange{
270+
{CidrIp: aws.String("0.0.0.0/0")},
271+
},
272+
},
273+
)
274+
275+
// Test with properly initialized empty set
276+
emptySet := NewIPPermissionSet()
277+
diff := sourceSet.Difference(emptySet)
278+
assert.Equal(t, 1, diff.Len(), "Difference with properly initialized empty set should work")
279+
280+
// Test that uninitialized set doesn't cause panic in Difference operation
281+
var uninitializedSet IPPermissionSet
282+
defer func() {
283+
if r := recover(); r != nil {
284+
t.Errorf("Difference operation should not panic with uninitialized set: %v", r)
285+
}
286+
}()
287+
288+
// This should not panic (though behavior may be undefined)
289+
_ = sourceSet.Difference(uninitializedSet)
290+
})
291+
292+
t.Run("multiple_ip_ranges_scenario", func(t *testing.T) {
293+
// Test complex permissions with multiple IP ranges to ensure
294+
// the Difference function handles them correctly
295+
296+
desired := NewIPPermissionSet(
297+
// Permission with multiple IP ranges
298+
ec2types.IpPermission{
299+
IpProtocol: aws.String("tcp"),
300+
FromPort: aws.Int32(443),
301+
ToPort: aws.Int32(443),
302+
IpRanges: []ec2types.IpRange{
303+
{CidrIp: aws.String("10.0.0.0/8")},
304+
{CidrIp: aws.String("172.16.0.0/12")},
305+
{CidrIp: aws.String("192.168.0.0/16")},
306+
},
307+
},
308+
// Single IP range permission
309+
ec2types.IpPermission{
310+
IpProtocol: aws.String("tcp"),
311+
FromPort: aws.Int32(80),
312+
ToPort: aws.Int32(80),
313+
IpRanges: []ec2types.IpRange{
314+
{CidrIp: aws.String("0.0.0.0/0")},
315+
},
316+
},
317+
)
318+
319+
actual := NewIPPermissionSet(
320+
// Same permission with multiple IP ranges (should match)
321+
ec2types.IpPermission{
322+
IpProtocol: aws.String("tcp"),
323+
FromPort: aws.Int32(443),
324+
ToPort: aws.Int32(443),
325+
IpRanges: []ec2types.IpRange{
326+
{CidrIp: aws.String("10.0.0.0/8")},
327+
{CidrIp: aws.String("172.16.0.0/12")},
328+
{CidrIp: aws.String("192.168.0.0/16")},
329+
},
330+
},
331+
// Different permission with multiple IP ranges
332+
ec2types.IpPermission{
333+
IpProtocol: aws.String("tcp"),
334+
FromPort: aws.Int32(8080),
335+
ToPort: aws.Int32(8080),
336+
IpRanges: []ec2types.IpRange{
337+
{CidrIp: aws.String("10.0.0.0/8")},
338+
{CidrIp: aws.String("172.16.0.0/12")},
339+
},
340+
},
341+
)
342+
343+
// Calculate differences
344+
add := desired.Difference(actual)
345+
remove := actual.Difference(desired)
346+
347+
// Should add tcp:80 (not present in actual)
348+
assert.Equal(t, 1, add.Len(), "Should add exactly one permission (tcp:80)")
349+
350+
// Should remove tcp:8080 (not present in desired)
351+
assert.Equal(t, 1, remove.Len(), "Should remove exactly one permission (tcp:8080)")
352+
353+
// Verify what's being added
354+
addList := add.List()
355+
if len(addList) > 0 {
356+
perm := addList[0]
357+
assert.Equal(t, "tcp", aws.ToString(perm.IpProtocol))
358+
assert.Equal(t, int32(80), aws.ToInt32(perm.FromPort))
359+
assert.Equal(t, int32(80), aws.ToInt32(perm.ToPort))
360+
assert.Equal(t, 1, len(perm.IpRanges), "Should have one IP range")
361+
assert.Equal(t, "0.0.0.0/0", aws.ToString(perm.IpRanges[0].CidrIp))
362+
}
363+
364+
// Verify what's being removed
365+
removeList := remove.List()
366+
if len(removeList) > 0 {
367+
perm := removeList[0]
368+
assert.Equal(t, "tcp", aws.ToString(perm.IpProtocol))
369+
assert.Equal(t, int32(8080), aws.ToInt32(perm.FromPort))
370+
assert.Equal(t, int32(8080), aws.ToInt32(perm.ToPort))
371+
assert.Equal(t, 2, len(perm.IpRanges), "Should have two IP ranges")
372+
}
373+
})
374+
375+
t.Run("identical_permissions_different_ip_range_order", func(t *testing.T) {
376+
// Test that permissions with same IP ranges but in different order
377+
// are treated as identical (this tests the robustness of the key generation)
378+
379+
perm1 := ec2types.IpPermission{
380+
IpProtocol: aws.String("tcp"),
381+
FromPort: aws.Int32(443),
382+
ToPort: aws.Int32(443),
383+
IpRanges: []ec2types.IpRange{
384+
{CidrIp: aws.String("10.0.0.0/8")},
385+
{CidrIp: aws.String("172.16.0.0/12")},
386+
{CidrIp: aws.String("192.168.0.0/16")},
387+
},
388+
}
389+
390+
perm2 := ec2types.IpPermission{
391+
IpProtocol: aws.String("tcp"),
392+
FromPort: aws.Int32(443),
393+
ToPort: aws.Int32(443),
394+
IpRanges: []ec2types.IpRange{
395+
{CidrIp: aws.String("192.168.0.0/16")}, // Different order
396+
{CidrIp: aws.String("10.0.0.0/8")},
397+
{CidrIp: aws.String("172.16.0.0/12")},
398+
},
399+
}
400+
401+
set1 := NewIPPermissionSet(perm1)
402+
set2 := NewIPPermissionSet(perm2)
403+
404+
// These should be different due to different order in JSON marshaling
405+
// (This tests the current behavior - if this fails, it indicates the key generation
406+
// doesn't account for order, which might be the root cause of issues)
407+
diff := set1.Difference(set2)
408+
409+
// Log the result to understand current behavior
410+
t.Logf("Difference between permissions with same IP ranges in different order: %d", diff.Len())
411+
412+
// The current implementation might treat these as different due to JSON marshaling
413+
// This test documents the current behavior and will help identify if this is the issue
414+
if diff.Len() == 0 {
415+
t.Log("Permissions with different IP range order are treated as identical")
416+
} else {
417+
t.Log("Permissions with different IP range order are treated as different")
418+
}
419+
})
420+
}

0 commit comments

Comments
 (0)