Skip to content

Commit 86d243a

Browse files
committed
Avoid unnecesary LB creation if UUID is missing
When the k8s.cloudscale.ch/loadbalancer-uuid annotation was present on a service, but the UUID could not be found in via API, the CCM would start creating LBs in a loop until it was killed. The cause of this was the LBs were looked up. The CCM aassumed that if a UUID could not be found, that the LB must not exist, and tried to create one. This would happen in a loop without any way for the CCM to detect that it should stop. This assumption has now been changed. We now check for the LB both by name and UUID, and expect to find at most one. If the service name and the UUID both match different LBs (as might happen if one manipulates the service annotations manually), the CCM will return an error. Ultimately, the CCM has no state it can call its own and infers state from the APIs it talks to (i.e., Kubernetes and the cloudscale.ch API). So if someone were to change both the UUID and the name of the LB using annotations, the CCM would have no choice but to create a new LB. Manipulating the UUID was at one point thought to be a possible feature: One could bring an existing LB into the managament of the CCM. Though there is no code that would prevent this, the docs no longer mention this option, as this has the potential to cause havoc. Changing the name is already discouraged. It is probably savest to not touch the name, nor the UUID, during the lifetime of a service.
1 parent f81b6a0 commit 86d243a

File tree

4 files changed

+96
-8
lines changed

4 files changed

+96
-8
lines changed

pkg/cloudscale_ccm/lb_mapper.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,19 @@ func (l *lbMapper) findByServiceInfo(
2020
serviceInfo *serviceInfo,
2121
) *limiter.Limiter[cloudscale.LoadBalancer] {
2222

23+
// If we have a UUID, look for both the service and the UUID. Usually
24+
// we expect to only see one, but it is possible for the UUID to point
25+
// to another LB than the service name, in which case we return both
26+
// so the caller can decide if that is sane or not.
2327
if uuid := serviceInfo.annotation(LoadBalancerUUID); uuid != "" {
24-
return l.getByUUID(ctx, uuid)
28+
return limiter.Join(
29+
l.getByUUID(ctx, uuid),
30+
l.findByName(ctx, serviceInfo.annotation(LoadBalancerName)),
31+
).Unique(
32+
func(a *cloudscale.LoadBalancer, b *cloudscale.LoadBalancer) bool {
33+
return a.UUID == b.UUID
34+
},
35+
)
2536
}
2637

2738
return l.findByName(ctx, serviceInfo.annotation(LoadBalancerName))

pkg/cloudscale_ccm/lb_mapper_test.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,42 @@ func TestFindLoadBalancer(t *testing.T) {
3737
lb, err := lbs.One()
3838
assert.NoError(t, err)
3939
assert.Equal(t, "foo", lb.Name)
40+
assert.Equal(t, 1, lbs.Count())
4041

4142
// Using an ambiguous name
4243
s.Annotations[LoadBalancerName] = "clone"
4344

4445
lbs = mapper.findByServiceInfo(t.Context(), i)
4546
_, err = lbs.One()
4647
assert.Error(t, err)
48+
assert.Equal(t, 2, lbs.Count())
4749

48-
// Using a uuid
50+
// Using a unique name and a mismatched uuid
51+
s.Annotations[LoadBalancerName] = "foo"
4952
s.Annotations[LoadBalancerUUID] = "85dffa20-8097-4d75-afa6-9e4372047ce6"
5053

54+
lbs = mapper.findByServiceInfo(t.Context(), i)
55+
_, err = lbs.One()
56+
assert.Error(t, err)
57+
assert.Equal(t, 2, lbs.Count())
58+
59+
// Using a unique name and a missing uuid
60+
s.Annotations[LoadBalancerName] = "foo"
61+
s.Annotations[LoadBalancerUUID] = "00000000-0000-0000-0000-000000000000"
62+
5163
lbs = mapper.findByServiceInfo(t.Context(), i)
5264
lb, err = lbs.One()
5365
assert.NoError(t, err)
54-
assert.Equal(t, "clone", lb.Name)
66+
assert.Equal(t, "foo", lb.Name)
67+
assert.Equal(t, 1, lbs.Count())
68+
69+
// Using a unique name and a matching uuid
70+
s.Annotations[LoadBalancerName] = "foo"
71+
s.Annotations[LoadBalancerUUID] = "c2e4aabd-8c91-46da-b069-71e01f439806"
72+
73+
lbs = mapper.findByServiceInfo(t.Context(), i)
74+
lb, err = lbs.One()
75+
assert.NoError(t, err)
76+
assert.Equal(t, "foo", lb.Name)
77+
assert.Equal(t, 1, lbs.Count())
5578
}

pkg/cloudscale_ccm/loadbalancer.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,11 @@ import (
2121
// them, unless you know what you are doing.
2222
const (
2323
// LoadBalancerUUID uniquely identifes the loadbalancer. This annotation
24-
// should not be provided by the customer, unless the adoption of an
25-
// existing load balancer is desired.
24+
// should not be provided by the customer.
2625
//
27-
// In all other cases, this value is set by the CCM after creating the
28-
// load balancer, to ensure that we track it with a proper ID and not
29-
// a name that might change without our knowledge.
26+
// Instead, this value is set by the CCM after creating the load balancer,
27+
// to ensure that we track it with a proper ID and not a name that might
28+
// change without our knowledge.
3029
LoadBalancerUUID = "k8s.cloudscale.ch/loadbalancer-uuid"
3130

3231
// LoadBalancerConfigVersion is set by the CCM when it first handles a

pkg/internal/limiter/limiter.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,52 @@ func New[T any](err error, elements ...T) *Limiter[T] {
1818
}
1919
}
2020

21+
func Join[T any](limiters ...*Limiter[T]) *Limiter[T] {
22+
joined := &Limiter[T]{
23+
Error: nil,
24+
elements: []T{},
25+
}
26+
27+
for _, limiter := range limiters {
28+
29+
// Keep the first error
30+
if joined.Error != nil && limiter.Error != nil {
31+
joined.Error = limiter.Error
32+
}
33+
34+
// Append the elements
35+
joined.elements = append(joined.elements, limiter.elements...)
36+
}
37+
38+
return joined
39+
}
40+
41+
// Unique excludes duplicate elements, according to a comparison function.
42+
// This is meant for small lists as the complexity is O(n^2).
43+
func (t *Limiter[T]) Unique(eq func(a *T, b *T) bool) *Limiter[T] {
44+
result := []T{}
45+
46+
for _, a := range t.elements {
47+
add := true
48+
49+
for _, b := range result {
50+
if eq(&a, &b) {
51+
add = false
52+
53+
break
54+
}
55+
}
56+
57+
if add {
58+
result = append(result, a)
59+
}
60+
}
61+
62+
t.elements = result
63+
64+
return t
65+
}
66+
2167
// All returns the full set of answers.
2268
func (t *Limiter[T]) All() ([]T, error) {
2369
if t.Error != nil {
@@ -68,3 +114,12 @@ func (t *Limiter[T]) AtMostOne() (*T, error) {
68114

69115
return &t.elements[0], nil
70116
}
117+
118+
// Count returns the number of elements, if there is no error.
119+
func (t *Limiter[T]) Count() int {
120+
if t.Error != nil {
121+
return 0
122+
}
123+
124+
return len(t.elements)
125+
}

0 commit comments

Comments
 (0)