Skip to content

Commit d5b5961

Browse files
authored
fix(destination): return retry-able error on ip conflict (#15627)
When encountering a cluster ip conflict (multiple services mapping onto a single IP) return 'Unavailable' versus 'FailedPrecondition.' The FailedPrecondiction error code is used to indicate the error state is terminal, and the client should not retry without an external change. The Unavailable error code is used when the client can retry the call. The conflict is the result of rescheduling a node, combined with the underlying IPAM implementation re-provisioning the same address. This change will result in the caller (proxy) issuing a retry w/ backoff.
1 parent f178e2d commit d5b5961

2 files changed

Lines changed: 56 additions & 1 deletion

File tree

‎controller/api/destination/server.go‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ import (
2828
// updates buffered per stream before the stream is closed.
2929
const DefaultStreamQueueCapacity = 100
3030

31+
var (
32+
// errIPConflict is a sentinel error returned when multiple services are
33+
// indexed by the same underlying IP address (clusterIP).
34+
errIPConflict = status.Error(codes.Unavailable, "found multiple services with conflicting cluster IP")
35+
)
36+
3137
type (
3238
Config struct {
3339
ControllerNS,
@@ -642,7 +648,7 @@ func getSvcID(k8sAPI *k8s.API, clusterIP string, log *logging.Entry) (*watcher.S
642648
conflictingServices = append(conflictingServices, fmt.Sprintf("%s:%s", service.Namespace, service.Name))
643649
}
644650
log.Warnf("found conflicting %s cluster IP: %s", clusterIP, strings.Join(conflictingServices, ","))
645-
return nil, status.Errorf(codes.FailedPrecondition, "found %d services with conflicting cluster IP %s", len(services), clusterIP)
651+
return nil, errIPConflict
646652
}
647653
if len(services) == 0 {
648654
return nil, nil

‎controller/api/destination/server_test.go‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1277,6 +1277,55 @@ spec:
12771277
t.Fatalf("Expected not to find service mapped to [%s]", badClusterIP)
12781278
}
12791279
})
1280+
1281+
t.Run("Return Unavailable when services conflict on the same cluster IP", func(t *testing.T) {
1282+
conflictingClusterIP := "10.245.0.99"
1283+
k8sConfigs := []string{`
1284+
apiVersion: v1
1285+
kind: Service
1286+
metadata:
1287+
name: service-a
1288+
namespace: test
1289+
spec:
1290+
type: ClusterIP
1291+
clusterIP: 10.245.0.99
1292+
clusterIPs:
1293+
- 10.245.0.99
1294+
ports:
1295+
- port: 1234`, `
1296+
apiVersion: v1
1297+
kind: Service
1298+
metadata:
1299+
name: service-b
1300+
namespace: test
1301+
spec:
1302+
type: ClusterIP
1303+
clusterIP: 10.245.0.99
1304+
clusterIPs:
1305+
- 10.245.0.99
1306+
ports:
1307+
- port: 1234`}
1308+
k8sAPI, err := k8s.NewFakeAPI(k8sConfigs...)
1309+
if err != nil {
1310+
t.Fatalf("NewFakeAPI returned an error: %s", err)
1311+
}
1312+
err = watcher.InitializeIndexers(k8sAPI)
1313+
if err != nil {
1314+
t.Fatalf("InitializeIndexers returned an error: %s", err)
1315+
}
1316+
k8sAPI.Sync(nil)
1317+
1318+
svc, err := getSvcID(k8sAPI, conflictingClusterIP, logging.WithFields(nil))
1319+
if svc != nil {
1320+
t.Fatalf("Expected no service to be returned for conflicting cluster IP [%s]", conflictingClusterIP)
1321+
}
1322+
if err == nil {
1323+
t.Fatalf("Expected an error for conflicting cluster IP [%s]", conflictingClusterIP)
1324+
}
1325+
if code := status.Code(err); code != codes.Unavailable {
1326+
t.Fatalf("Expected error code %s, but got %s: %s", codes.Unavailable, code, err)
1327+
}
1328+
})
12801329
}
12811330

12821331
func testReturnEndpoints(t *testing.T, fqdn, ip string, port uint32) {

0 commit comments

Comments
 (0)