Skip to content

Commit 048431f

Browse files
authored
feat(lb): support host headers on http healthchecks (#151)
* feat(lb): support host headers on http healthchecks * fix(lb): backend diff now includes healthcheck diff * chore(lb): add test coverage
1 parent 7e8b78a commit 048431f

File tree

3 files changed

+130
-72
lines changed

3 files changed

+130
-72
lines changed

docs/loadbalancer-annotations.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ The default value is `5`.
6767

6868
### `service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri`
6969
This is the annotation to set the URI that is used by the `http` health check.
70-
It is possible to set the uri per port, like `80:/;443,8443:/healthz`.
70+
It is possible to set the uri per port, like `80:/;443,8443:mydomain.tld/healthz`.
7171
NB: Required when setting service.beta.kubernetes.io/scw-loadbalancer-health-check-type to `http` or `https`.
7272

7373
### `service.beta.kubernetes.io/scw-loadbalancer-health-check-http-method`

scaleway/loadbalancers.go

Lines changed: 35 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"net"
23+
"net/url"
2324
"os"
2425
"reflect"
2526
"strconv"
@@ -79,7 +80,7 @@ const (
7980
serviceAnnotationLoadBalancerHealthCheckMaxRetries = "service.beta.kubernetes.io/scw-loadbalancer-health-check-max-retries"
8081

8182
// serviceAnnotationLoadBalancerHealthCheckHTTPURI is the URI that is used by the "http" health check
82-
// It is possible to set the uri per port, like "80:/;443,8443:/healthz"
83+
// It is possible to set the uri per port, like "80:/;443,8443:mydomain.tld/healthz"
8384
// NB: Required when setting service.beta.kubernetes.io/scw-loadbalancer-health-check-type to "http" or "https"
8485
serviceAnnotationLoadBalancerHealthCheckHTTPURI = "service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri"
8586

@@ -1510,19 +1511,29 @@ func getHTTPHealthCheck(service *v1.Service, nodePort int32) (*scwlb.HealthCheck
15101511
if err != nil {
15111512
return nil, err
15121513
}
1513-
uri, err := getHTTPHealthCheckURI(service, nodePort)
1514+
1515+
uriStr, err := getHTTPHealthCheckURI(service, nodePort)
1516+
if err != nil {
1517+
return nil, err
1518+
}
1519+
uri, err := url.Parse(fmt.Sprintf("http://%s", uriStr))
15141520
if err != nil {
15151521
return nil, err
15161522
}
1523+
if uri.Path == "" {
1524+
uri.Path = "/"
1525+
}
1526+
15171527
method, err := getHTTPHealthCheckMethod(service, nodePort)
15181528
if err != nil {
15191529
return nil, err
15201530
}
15211531

15221532
return &scwlb.HealthCheckHTTPConfig{
1523-
Method: method,
1524-
Code: &code,
1525-
URI: uri,
1533+
Method: method,
1534+
Code: &code,
1535+
URI: uri.Path,
1536+
HostHeader: uri.Host,
15261537
}, nil
15271538
}
15281539

@@ -1531,19 +1542,30 @@ func getHTTPSHealthCheck(service *v1.Service, nodePort int32) (*scwlb.HealthChec
15311542
if err != nil {
15321543
return nil, err
15331544
}
1534-
uri, err := getHTTPHealthCheckURI(service, nodePort)
1545+
1546+
uriStr, err := getHTTPHealthCheckURI(service, nodePort)
15351547
if err != nil {
15361548
return nil, err
15371549
}
1550+
uri, err := url.Parse(fmt.Sprintf("https://%s", uriStr))
1551+
if err != nil {
1552+
return nil, err
1553+
}
1554+
if uri.Path == "" {
1555+
uri.Path = "/"
1556+
}
1557+
15381558
method, err := getHTTPHealthCheckMethod(service, nodePort)
15391559
if err != nil {
15401560
return nil, err
15411561
}
15421562

15431563
return &scwlb.HealthCheckHTTPSConfig{
1544-
Method: method,
1545-
Code: &code,
1546-
URI: uri,
1564+
Method: method,
1565+
Code: &code,
1566+
URI: uri.Path,
1567+
HostHeader: uri.Host,
1568+
Sni: uri.Host,
15471569
}, nil
15481570
}
15491571

@@ -1905,65 +1927,9 @@ func backendEquals(got, want *scwlb.Backend) bool {
19051927
return false
19061928
}
19071929

1908-
// TODO
1909-
if got.HealthCheck != want.HealthCheck {
1910-
if got.HealthCheck == nil || want.HealthCheck == nil {
1911-
klog.V(3).Infof("backend.HealthCheck: %s - %s", got.HealthCheck, want.HealthCheck)
1912-
return false
1913-
}
1914-
1915-
if got.HealthCheck.Port != want.HealthCheck.Port {
1916-
klog.V(3).Infof("backend.HealthCheck.Port: %s - %s", got.HealthCheck.Port, want.HealthCheck.Port)
1917-
return false
1918-
}
1919-
if !durationPtrEqual(got.HealthCheck.CheckDelay, want.HealthCheck.CheckDelay) {
1920-
klog.V(3).Infof("backend.HealthCheck.CheckDelay: %s - %s", got.HealthCheck.CheckDelay, want.HealthCheck.CheckDelay)
1921-
return false
1922-
}
1923-
if !durationPtrEqual(got.HealthCheck.CheckTimeout, want.HealthCheck.CheckTimeout) {
1924-
klog.V(3).Infof("backend.HealthCheck.CheckTimeout: %s - %s", got.HealthCheck.CheckTimeout, want.HealthCheck.CheckTimeout)
1925-
return false
1926-
}
1927-
if got.HealthCheck.CheckMaxRetries != want.HealthCheck.CheckMaxRetries {
1928-
klog.V(3).Infof("backend.HealthCheck.CheckMaxRetries: %s - %s", got.HealthCheck.CheckMaxRetries, want.HealthCheck.CheckMaxRetries)
1929-
return false
1930-
}
1931-
if got.HealthCheck.CheckSendProxy != want.HealthCheck.CheckSendProxy {
1932-
klog.V(3).Infof("backend.HealthCheck.CheckSendProxy: %s - %s", got.HealthCheck.CheckSendProxy, want.HealthCheck.CheckSendProxy)
1933-
return false
1934-
}
1935-
if (got.HealthCheck.TCPConfig == nil) != (want.HealthCheck.TCPConfig == nil) {
1936-
klog.V(3).Infof("backend.HealthCheck.TCPConfig: %s - %s", got.HealthCheck.TCPConfig, want.HealthCheck.TCPConfig)
1937-
return false
1938-
}
1939-
if (got.HealthCheck.MysqlConfig == nil) != (want.HealthCheck.MysqlConfig == nil) {
1940-
klog.V(3).Infof("backend.HealthCheck.MysqlConfig: %s - %s", got.HealthCheck.MysqlConfig, want.HealthCheck.MysqlConfig)
1941-
return false
1942-
}
1943-
if (got.HealthCheck.PgsqlConfig == nil) != (want.HealthCheck.PgsqlConfig == nil) {
1944-
klog.V(3).Infof("backend.HealthCheck.PgsqlConfig: %s - %s", got.HealthCheck.PgsqlConfig, want.HealthCheck.PgsqlConfig)
1945-
return false
1946-
}
1947-
if (got.HealthCheck.LdapConfig == nil) != (want.HealthCheck.LdapConfig == nil) {
1948-
klog.V(3).Infof("backend.HealthCheck.LdapConfig: %s - %s", got.HealthCheck.LdapConfig, want.HealthCheck.LdapConfig)
1949-
return false
1950-
}
1951-
if (got.HealthCheck.RedisConfig == nil) != (want.HealthCheck.RedisConfig == nil) {
1952-
klog.V(3).Infof("backend.HealthCheck.RedisConfig: %s - %s", got.HealthCheck.RedisConfig, want.HealthCheck.RedisConfig)
1953-
return false
1954-
}
1955-
if (got.HealthCheck.HTTPConfig == nil) != (want.HealthCheck.HTTPConfig == nil) {
1956-
klog.V(3).Infof("backend.HealthCheck.HTTPConfig: %s - %s", got.HealthCheck.HTTPConfig, want.HealthCheck.HTTPConfig)
1957-
return false
1958-
}
1959-
if (got.HealthCheck.HTTPSConfig == nil) != (want.HealthCheck.HTTPSConfig == nil) {
1960-
klog.V(3).Infof("backend.HealthCheck.HTTPSConfig: %s - %s", got.HealthCheck.HTTPSConfig, want.HealthCheck.HTTPSConfig)
1961-
return false
1962-
}
1963-
if !scwDurationPtrEqual(got.HealthCheck.TransientCheckDelay, want.HealthCheck.TransientCheckDelay) {
1964-
klog.V(3).Infof("backend.HealthCheck.TransientCheckDelay: %s - %s", got.HealthCheck.TransientCheckDelay, want.HealthCheck.TransientCheckDelay)
1965-
return false
1966-
}
1930+
if !reflect.DeepEqual(got.HealthCheck, want.HealthCheck) {
1931+
klog.V(3).Infof("backend.HealthCheck: %s - %s", got.HealthCheck, want.HealthCheck)
1932+
return false
19671933
}
19681934

19691935
return true
@@ -2074,7 +2040,7 @@ func aclsEquals(got []*scwlb.ACL, want []*scwlb.ACLSpec) bool {
20742040

20752041
slices.SortStableFunc(got, func(a, b *scwlb.ACL) bool { return a.Index < b.Index })
20762042
slices.SortStableFunc(want, func(a, b *scwlb.ACLSpec) bool { return a.Index < b.Index })
2077-
for idx, _ := range want {
2043+
for idx := range want {
20782044
if want[idx].Name != got[idx].Name {
20792045
return false
20802046
}
@@ -2102,7 +2068,6 @@ func aclsEquals(got []*scwlb.ACL, want []*scwlb.ACLSpec) bool {
21022068
}
21032069

21042070
func (l *loadbalancers) createBackend(service *v1.Service, loadbalancer *scwlb.LB, backend *scwlb.Backend) (*scwlb.Backend, error) {
2105-
// TODO: implement createBackend
21062071
b, err := l.api.CreateBackend(&scwlb.ZonedAPICreateBackendRequest{
21072072
Zone: getLoadBalancerZone(service),
21082073
LBID: loadbalancer.ID,
@@ -2130,7 +2095,6 @@ func (l *loadbalancers) createBackend(service *v1.Service, loadbalancer *scwlb.L
21302095
}
21312096

21322097
func (l *loadbalancers) updateBackend(service *v1.Service, loadbalancer *scwlb.LB, backend *scwlb.Backend) (*scwlb.Backend, error) {
2133-
// TODO: implement updateBackend
21342098
b, err := l.api.UpdateBackend(&scwlb.ZonedAPIUpdateBackendRequest{
21352099
Zone: getLoadBalancerZone(service),
21362100
BackendID: backend.ID,

scaleway/loadbalancers_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,30 @@ func TestBackendEquals(t *testing.T) {
941941
want bool
942942
}{"with a different TimeoutTunnel", reference, diff, false})
943943

944+
httpRef := deepCloneBackend(reference)
945+
httpRef.HealthCheck.TCPConfig = nil
946+
httpRef.HealthCheck.HTTPConfig = &scwlb.HealthCheckHTTPConfig{
947+
URI: "/",
948+
Method: "POST",
949+
Code: scw.Int32Ptr(200),
950+
}
951+
httpDiff := deepCloneBackend(httpRef)
952+
matrix = append(matrix, struct {
953+
Name string
954+
a *scwlb.Backend
955+
b *scwlb.Backend
956+
want bool
957+
}{"with same HTTP healthchecks", httpRef, httpDiff, true})
958+
959+
httpDiff = deepCloneBackend(httpRef)
960+
httpDiff.HealthCheck.HTTPConfig.Code = scw.Int32Ptr(404)
961+
matrix = append(matrix, struct {
962+
Name string
963+
a *scwlb.Backend
964+
b *scwlb.Backend
965+
want bool
966+
}{"with same HTTP healthchecks", httpRef, httpDiff, false})
967+
944968
for _, tt := range matrix {
945969
t.Run(tt.Name, func(t *testing.T) {
946970
got := backendEquals(tt.a, tt.b)
@@ -1137,6 +1161,76 @@ func TestMakeACLPrefix(t *testing.T) {
11371161
}
11381162
}
11391163

1164+
func TestGetHTTPHealthCheck(t *testing.T) {
1165+
matrix := []struct {
1166+
name string
1167+
svc *v1.Service
1168+
want *scwlb.HealthCheckHTTPConfig
1169+
}{
1170+
{"with empty config", &v1.Service{
1171+
ObjectMeta: metav1.ObjectMeta{
1172+
Annotations: map[string]string{
1173+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-type": "http",
1174+
},
1175+
},
1176+
}, &scwlb.HealthCheckHTTPConfig{URI: "/", Method: "GET", Code: scw.Int32Ptr(200)}},
1177+
1178+
{"with just a domain", &v1.Service{
1179+
ObjectMeta: metav1.ObjectMeta{
1180+
Annotations: map[string]string{
1181+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-type": "http",
1182+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri": "domain.tld",
1183+
},
1184+
},
1185+
}, &scwlb.HealthCheckHTTPConfig{URI: "/", Method: "GET", Code: scw.Int32Ptr(200), HostHeader: "domain.tld"}},
1186+
1187+
{"with a domain and path", &v1.Service{
1188+
ObjectMeta: metav1.ObjectMeta{
1189+
Annotations: map[string]string{
1190+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-type": "http",
1191+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri": "domain.tld/path",
1192+
},
1193+
},
1194+
}, &scwlb.HealthCheckHTTPConfig{URI: "/path", Method: "GET", Code: scw.Int32Ptr(200), HostHeader: "domain.tld"}},
1195+
1196+
{"with just a path", &v1.Service{
1197+
ObjectMeta: metav1.ObjectMeta{
1198+
Annotations: map[string]string{
1199+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-type": "http",
1200+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-http-uri": "/path",
1201+
},
1202+
},
1203+
}, &scwlb.HealthCheckHTTPConfig{URI: "/path", Method: "GET", Code: scw.Int32Ptr(200)}},
1204+
1205+
{"with a specific code", &v1.Service{
1206+
ObjectMeta: metav1.ObjectMeta{
1207+
Annotations: map[string]string{
1208+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-type": "http",
1209+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-http-code": "404",
1210+
},
1211+
},
1212+
}, &scwlb.HealthCheckHTTPConfig{URI: "/", Method: "GET", Code: scw.Int32Ptr(404)}},
1213+
1214+
{"with a specific method", &v1.Service{
1215+
ObjectMeta: metav1.ObjectMeta{
1216+
Annotations: map[string]string{
1217+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-type": "http",
1218+
"service.beta.kubernetes.io/scw-loadbalancer-health-check-http-method": "POST",
1219+
},
1220+
},
1221+
}, &scwlb.HealthCheckHTTPConfig{URI: "/", Method: "POST", Code: scw.Int32Ptr(200)}},
1222+
}
1223+
1224+
for _, tt := range matrix {
1225+
t.Run(tt.name, func(t *testing.T) {
1226+
got, _ := getHTTPHealthCheck(tt.svc, int32(80))
1227+
if !reflect.DeepEqual(got, tt.want) {
1228+
t.Errorf("want: %v, got: %v", got, tt.want)
1229+
}
1230+
})
1231+
}
1232+
}
1233+
11401234
func deepCloneBackend(original *scwlb.Backend) *scwlb.Backend {
11411235
originalJSON, err := json.Marshal(original)
11421236
if err != nil {

0 commit comments

Comments
 (0)