Skip to content

Commit 1079c75

Browse files
committed
feat: per-port protocol and certificates
1 parent 7e69452 commit 1079c75

7 files changed

Lines changed: 771 additions & 11 deletions

File tree

docs/load_balancers.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,84 @@ will delete the associated Load Balancer. If the Load Balancer is managed
9494
through Terraform, this causes problems. To disable this, you can enable
9595
deletion protection on the Load Balancer, this way hcloud-cloud-controller-manager
9696
will just skip deleting it when the associated `Service` is deleted.
97+
98+
## Per-Port Protocol and Certificate Configuration
99+
100+
The hcloud-cloud-controller-manager supports configuring different protocols and certificates for different ports of a single service using per-port annotations.
101+
102+
### Per-Port Protocol Configuration
103+
104+
Use the `load-balancer.hetzner.cloud/protocol-ports` annotation to specify different protocols for different ports:
105+
106+
```yaml
107+
apiVersion: v1
108+
kind: Service
109+
metadata:
110+
name: multi-protocol-service
111+
annotations:
112+
load-balancer.hetzner.cloud/protocol-ports: "80:http,443:https,9000:tcp"
113+
spec:
114+
type: LoadBalancer
115+
ports:
116+
- name: http
117+
port: 80
118+
targetPort: 8080
119+
protocol: TCP
120+
- name: https
121+
port: 443
122+
targetPort: 8443
123+
protocol: TCP
124+
- name: tcp
125+
port: 9000
126+
targetPort: 9000
127+
protocol: TCP
128+
selector:
129+
app: my-app
130+
```
131+
132+
### Per-Port Certificate Configuration
133+
134+
Use the `load-balancer.hetzner.cloud/http-certificates-ports` annotation to specify different certificates for different HTTPS ports:
135+
136+
```yaml
137+
apiVersion: v1
138+
kind: Service
139+
metadata:
140+
name: multi-https-service
141+
annotations:
142+
load-balancer.hetzner.cloud/protocol-ports: "443:https,8443:https"
143+
load-balancer.hetzner.cloud/http-certificates-ports: "443:cert1,cert2;8443:cert3"
144+
spec:
145+
type: LoadBalancer
146+
ports:
147+
- name: https-main
148+
port: 443
149+
targetPort: 8443
150+
protocol: TCP
151+
- name: https-alt
152+
port: 8443
153+
targetPort: 8443
154+
protocol: TCP
155+
selector:
156+
app: my-app
157+
```
158+
159+
### Format Specification
160+
161+
**Protocol Ports Format:**
162+
- Format: `"port:protocol,port:protocol,..."`
163+
- Example: `"80:http,443:https,9000:tcp"`
164+
- Supported protocols: `tcp`, `http`, `https`
165+
166+
**Certificate Ports Format:**
167+
- Format: `"port:cert1,cert2;port:cert3,..."`
168+
- Example: `"443:cert1,cert2;8443:cert3"`
169+
- Supports both certificate names and IDs
170+
- Use semicolons (`;`) to separate different ports
171+
- Use commas (`,`) to separate multiple certificates for the same port
172+
173+
### Fallback Behavior
174+
175+
- If per-port configuration is not specified for a port, the global annotation values are used
176+
- Global annotations: `load-balancer.hetzner.cloud/protocol` and `load-balancer.hetzner.cloud/http-certificates`
177+
- If no global annotation is set, defaults to `tcp` protocol

internal/annotation/load_balancer.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ const (
5454
// values: tcp, http, https
5555
LBSvcProtocol Name = "load-balancer.hetzner.cloud/protocol"
5656

57+
// LBSvcProtocolPorts specifies the protocol per port for the service. This allows
58+
// different ports to use different protocols. Format: "80:http,443:https,9000:tcp"
59+
// If set, this takes precedence over LBSvcProtocol for the specified ports.
60+
LBSvcProtocolPorts Name = "load-balancer.hetzner.cloud/protocol-ports"
61+
5762
// LBAlgorithmType specifies the algorithm type of the Load Balancer.
5863
//
5964
// Possible values: round_robin, least_connections
@@ -129,6 +134,13 @@ const (
129134
// HTTPS only.
130135
LBSvcHTTPCertificates Name = "load-balancer.hetzner.cloud/http-certificates"
131136

137+
// LBSvcHTTPCertificatesPorts specifies certificates per port for HTTPS services.
138+
// Format: "443:cert1,cert2;8443:cert3,cert4"
139+
// If set, this takes precedence over LBSvcHTTPCertificates for the specified ports.
140+
//
141+
// HTTPS only.
142+
LBSvcHTTPCertificatesPorts Name = "load-balancer.hetzner.cloud/http-certificates-ports"
143+
132144
// LBSvcHTTPManagedCertificateName contains the names of the managed
133145
// certificate to create by the Cloud Controller manager. Ignored if
134146
// LBSvcHTTPCertificateType is missing or set to "uploaded". Optional.

internal/annotation/name.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,112 @@ func (s Name) CertificatesFromService(svc *corev1.Service) ([]*hcloud.Certificat
259259
return cs, err
260260
}
261261

262+
// ProtocolPortsFromService retrieves the protocol configuration per port from svc.
263+
// The annotation format is "port:protocol,port:protocol" (e.g. "80:http,443:https,9000:tcp")
264+
//
265+
// Returns a map of port -> protocol. Returns an empty map if the annotation was not set.
266+
func (s Name) ProtocolPortsFromService(svc *corev1.Service) (map[int]hcloud.LoadBalancerServiceProtocol, error) {
267+
const op = "annotation/Name.ProtocolPortsFromService"
268+
metrics.OperationCalled.WithLabelValues(op).Inc()
269+
270+
result := make(map[int]hcloud.LoadBalancerServiceProtocol)
271+
272+
v, ok := s.StringFromService(svc)
273+
if !ok {
274+
return result, nil // Return empty map if not set
275+
}
276+
277+
if strings.TrimSpace(v) == "" {
278+
return result, nil
279+
}
280+
281+
pairs := strings.Split(v, ",")
282+
for _, pair := range pairs {
283+
parts := strings.Split(strings.TrimSpace(pair), ":")
284+
if len(parts) != 2 {
285+
return nil, fmt.Errorf("%s: invalid format for port:protocol pair: %s", op, pair)
286+
}
287+
288+
port, err := strconv.Atoi(strings.TrimSpace(parts[0]))
289+
if err != nil {
290+
return nil, fmt.Errorf("%s: invalid port number: %s", op, parts[0])
291+
}
292+
293+
protocol, err := validateServiceProtocol(strings.TrimSpace(parts[1]))
294+
if err != nil {
295+
return nil, fmt.Errorf("%s: %w", op, err)
296+
}
297+
298+
result[port] = protocol
299+
}
300+
301+
return result, nil
302+
}
303+
304+
// CertificatePortsFromService retrieves the certificate configuration per port from svc.
305+
// The annotation format is "port:cert1,cert2;port:cert3,cert4" (e.g. "443:cert1,cert2;8443:cert3")
306+
//
307+
// Returns a map of port -> certificates. Returns an empty map if the annotation was not set.
308+
func (s Name) CertificatePortsFromService(svc *corev1.Service) (map[int][]*hcloud.Certificate, error) {
309+
const op = "annotation/Name.CertificatePortsFromService"
310+
metrics.OperationCalled.WithLabelValues(op).Inc()
311+
312+
result := make(map[int][]*hcloud.Certificate)
313+
314+
v, ok := s.StringFromService(svc)
315+
if !ok {
316+
return result, nil // Return empty map if not set
317+
}
318+
319+
if strings.TrimSpace(v) == "" {
320+
return result, nil
321+
}
322+
323+
// Split by semicolon to get port configurations
324+
portConfigs := strings.Split(v, ";")
325+
for _, portConfig := range portConfigs {
326+
portConfig = strings.TrimSpace(portConfig)
327+
if portConfig == "" {
328+
continue
329+
}
330+
331+
// Split by colon to get port and certificates
332+
parts := strings.Split(portConfig, ":")
333+
if len(parts) != 2 {
334+
return nil, fmt.Errorf("%s: invalid format for port:certificates pair: %s", op, portConfig)
335+
}
336+
337+
port, err := strconv.Atoi(strings.TrimSpace(parts[0]))
338+
if err != nil {
339+
return nil, fmt.Errorf("%s: invalid port number: %s", op, parts[0])
340+
}
341+
342+
// Parse certificates (same logic as CertificatesFromService)
343+
certStrings := strings.Split(strings.TrimSpace(parts[1]), ",")
344+
certificates := make([]*hcloud.Certificate, len(certStrings))
345+
346+
for i, certString := range certStrings {
347+
certString = strings.TrimSpace(certString)
348+
if certString == "" {
349+
return nil, fmt.Errorf("%s: empty certificate reference", op)
350+
}
351+
352+
id, err := strconv.ParseInt(certString, 10, 64)
353+
if err != nil {
354+
// If we could not parse the string as an integer we assume it
355+
// is a name not an id.
356+
certificates[i] = &hcloud.Certificate{Name: certString}
357+
} else {
358+
certificates[i] = &hcloud.Certificate{ID: id}
359+
}
360+
}
361+
362+
result[port] = certificates
363+
}
364+
365+
return result, nil
366+
}
367+
262368
// CertificateTypeFromService retrieves the hcloud.CertificateType value
263369
// belonging to the annotation from svc.
264370
//

0 commit comments

Comments
 (0)