Skip to content

Commit 50bccd8

Browse files
authored
Merge pull request #305 from nicktate/ntate/feature/health-check-port-annotation
Add support for configurable health-check via annotation
2 parents a1637c4 + 832e2a6 commit 50bccd8

File tree

5 files changed

+201
-5
lines changed

5 files changed

+201
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Fixed
66

77
* Maintain default protocol when secure protocol override is applied (@timoreimann)
8+
* Add `service.beta.kubernetes.io/do-loadbalancer-healthcheck-port` annotation to customize DO LB health-check port (@ntate)
89

910
## v0.1.22 (beta) - Jan 15th 2020
1011

cloud-controller-manager/do/loadbalancers.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ const (
4949
// for DO load balancers. Defaults to '/'.
5050
annDOHealthCheckPath = "service.beta.kubernetes.io/do-loadbalancer-healthcheck-path"
5151

52+
// annDOHealthCheckPort is the annotation used to specify the health check port
53+
// for DO load balancers. Defaults to the first Service Port
54+
annDOHealthCheckPort = "service.beta.kubernetes.io/do-loadbalancer-healthcheck-port"
55+
5256
// annDOHealthCheckProtocol is the annotation used to specify the health check protocol
5357
// for DO load balancers. Defaults to the protocol used in
5458
// 'service.beta.kubernetes.io/do-loadbalancer-protocol'.
@@ -587,11 +591,12 @@ func (l *loadBalancers) buildLoadBalancerRequest(ctx context.Context, service *v
587591
}
588592

589593
// buildHealthChecks returns a godo.HealthCheck for service.
590-
//
591-
// Although a Kubernetes Service can have many node ports, DigitalOcean Load
592-
// Balancers can only take one node port so we choose the first node port for
593-
// health checking.
594594
func buildHealthCheck(service *v1.Service) (*godo.HealthCheck, error) {
595+
healthCheckPort, err := healthCheckPort(service)
596+
if err != nil {
597+
return nil, err
598+
}
599+
595600
healthCheckProtocol, err := healthCheckProtocol(service)
596601
if err != nil {
597602
return nil, err
@@ -618,7 +623,7 @@ func buildHealthCheck(service *v1.Service) (*godo.HealthCheck, error) {
618623

619624
return &godo.HealthCheck{
620625
Protocol: healthCheckProtocol,
621-
Port: int(service.Spec.Ports[0].NodePort),
626+
Port: healthCheckPort,
622627
Path: healthCheckPath,
623628
CheckIntervalSeconds: checkIntervalSecs,
624629
ResponseTimeoutSeconds: responseTimeoutSecs,
@@ -781,6 +786,30 @@ func getHostname(service *v1.Service) string {
781786
return strings.ToLower(service.Annotations[annDOHostname])
782787
}
783788

789+
// healthCheckPort returns the health check port specified, defaulting
790+
// to the first port in the service otherwise.
791+
func healthCheckPort(service *v1.Service) (int, error) {
792+
ports, err := getPorts(service, annDOHealthCheckPort)
793+
if err != nil {
794+
return 0, fmt.Errorf("failed to get health check port: %v", err)
795+
}
796+
797+
if len(ports) > 1 {
798+
return 0, fmt.Errorf("annotation %s only supports a single port, but found multiple: %v", annDOHealthCheckPort, ports)
799+
}
800+
801+
if len(ports) == 1 {
802+
for _, servicePort := range service.Spec.Ports {
803+
if int(servicePort.Port) == ports[0] {
804+
return int(servicePort.NodePort), nil
805+
}
806+
}
807+
return 0, fmt.Errorf("specified health check port %d does not exist on service %s/%s", ports[0], service.Namespace, service.Name)
808+
}
809+
810+
return int(service.Spec.Ports[0].NodePort), nil
811+
}
812+
784813
// healthCheckProtocol returns the health check protocol as specified in the service,
785814
// falling back to TCP if not specified.
786815
func healthCheckProtocol(service *v1.Service) (string, error) {

cloud-controller-manager/do/loadbalancers_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,6 +1991,125 @@ func Test_buildHealthCheck(t *testing.T) {
19911991
},
19921992
errMsgPrefix: fmt.Sprintf("invalid protocol: %q specified in annotation: %q", "invalid", annDOProtocol),
19931993
},
1994+
{
1995+
name: "health check with custom port",
1996+
service: &v1.Service{
1997+
ObjectMeta: metav1.ObjectMeta{
1998+
Name: "test",
1999+
UID: "abc123",
2000+
Annotations: map[string]string{
2001+
annDOProtocol: "http",
2002+
annDOHealthCheckPath: "/health",
2003+
annDOHealthCheckPort: "636",
2004+
},
2005+
},
2006+
Spec: v1.ServiceSpec{
2007+
Ports: []v1.ServicePort{
2008+
{
2009+
Name: "test",
2010+
Protocol: "TCP",
2011+
Port: int32(80),
2012+
NodePort: int32(30000),
2013+
},
2014+
{
2015+
Name: "test",
2016+
Protocol: "TCP",
2017+
Port: int32(636),
2018+
NodePort: int32(32000),
2019+
},
2020+
},
2021+
},
2022+
},
2023+
healthcheck: defaultHealthCheck("http", 32000, "/health"),
2024+
},
2025+
{
2026+
name: "invalid health check using port override with non-existent port",
2027+
service: &v1.Service{
2028+
ObjectMeta: metav1.ObjectMeta{
2029+
Name: "test",
2030+
Namespace: "default",
2031+
UID: "abc123",
2032+
Annotations: map[string]string{
2033+
annDOHealthCheckPort: "9999",
2034+
},
2035+
},
2036+
Spec: v1.ServiceSpec{
2037+
Ports: []v1.ServicePort{
2038+
{
2039+
Name: "test",
2040+
Protocol: "TCP",
2041+
Port: int32(636),
2042+
NodePort: int32(30000),
2043+
},
2044+
{
2045+
Name: "test",
2046+
Protocol: "TCP",
2047+
Port: int32(332),
2048+
NodePort: int32(32000),
2049+
},
2050+
},
2051+
},
2052+
},
2053+
errMsgPrefix: fmt.Sprintf("specified health check port %d does not exist on service default/test", 9999),
2054+
},
2055+
{
2056+
name: "invalid health check using port override with non-numeric port",
2057+
service: &v1.Service{
2058+
ObjectMeta: metav1.ObjectMeta{
2059+
Name: "test",
2060+
UID: "abc123",
2061+
Annotations: map[string]string{
2062+
annDOHealthCheckPort: "invalid",
2063+
},
2064+
},
2065+
Spec: v1.ServiceSpec{
2066+
Ports: []v1.ServicePort{
2067+
{
2068+
Name: "test",
2069+
Protocol: "TCP",
2070+
Port: int32(636),
2071+
NodePort: int32(30000),
2072+
},
2073+
{
2074+
Name: "test",
2075+
Protocol: "TCP",
2076+
Port: int32(332),
2077+
NodePort: int32(32000),
2078+
},
2079+
},
2080+
},
2081+
},
2082+
errMsgPrefix: "failed to get health check port: strconv.Atoi: parsing \"invalid\": invalid syntax",
2083+
},
2084+
{
2085+
name: "invalid health check using port override with multiple ports",
2086+
service: &v1.Service{
2087+
ObjectMeta: metav1.ObjectMeta{
2088+
Name: "test",
2089+
UID: "abc123",
2090+
Annotations: map[string]string{
2091+
annDOHealthCheckPort: "636,332",
2092+
},
2093+
},
2094+
Spec: v1.ServiceSpec{
2095+
Ports: []v1.ServicePort{
2096+
{
2097+
Name: "test",
2098+
Protocol: "TCP",
2099+
Port: int32(636),
2100+
NodePort: int32(30000),
2101+
},
2102+
{
2103+
Name: "test",
2104+
Protocol: "TCP",
2105+
Port: int32(332),
2106+
NodePort: int32(32000),
2107+
},
2108+
},
2109+
},
2110+
},
2111+
errMsgPrefix: fmt.Sprintf("annotation %s only supports a single port, but found multiple: [636 332]", annDOHealthCheckPort),
2112+
},
19942113
{
19952114
name: "default numeric parameters",
19962115
service: &v1.Service{

docs/controllers/services/annotations.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ Certain annotations may override the default protocol. See the more specific des
1212

1313
If `https` or `http2` is specified, then either `service.beta.kubernetes.io/do-loadbalancer-certificate-id` or `service.beta.kubernetes.io/do-loadbalancer-tls-passthrough` must be specified as well.
1414

15+
## service.beta.kubernetes.io/do-loadbalancer-healthcheck-port
16+
17+
The port used to check if a backend droplet is healthy. Defaults to the first port in a service.
18+
1519
## service.beta.kubernetes.io/do-loadbalancer-healthcheck-path
1620

1721
The path used to check if a backend droplet is healthy. Defaults to "/".
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
kind: Service
3+
apiVersion: v1
4+
metadata:
5+
name: http-lb
6+
annotations:
7+
service.beta.kubernetes.io/do-loadbalancer-protocol: "http"
8+
service.beta.kubernetes.io/do-loadbalancer-healthcheck-port: "55"
9+
spec:
10+
type: LoadBalancer
11+
selector:
12+
app: nginx-example
13+
ports:
14+
- name: http
15+
protocol: TCP
16+
port: 80
17+
targetPort: 80
18+
- name: health-check
19+
protocol: TCP
20+
port: 55
21+
targetPort: 80
22+
23+
---
24+
apiVersion: apps/v1
25+
kind: Deployment
26+
metadata:
27+
name: nginx-example
28+
spec:
29+
replicas: 2
30+
selector:
31+
matchLabels:
32+
app: nginx-example
33+
template:
34+
metadata:
35+
labels:
36+
app: nginx-example
37+
spec:
38+
containers:
39+
- name: nginx
40+
image: nginx
41+
ports:
42+
- containerPort: 80
43+
protocol: TCP

0 commit comments

Comments
 (0)