Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions docs/load_balancers.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,84 @@ will delete the associated Load Balancer. If the Load Balancer is managed
through Terraform, this causes problems. To disable this, you can enable
deletion protection on the Load Balancer, this way hcloud-cloud-controller-manager
will just skip deleting it when the associated `Service` is deleted.

## Per-Port Protocol and Certificate Configuration

The hcloud-cloud-controller-manager supports configuring different protocols and certificates for different ports of a single service using per-port annotations.

### Per-Port Protocol Configuration

Use the `load-balancer.hetzner.cloud/protocol-ports` annotation to specify different protocols for different ports:

```yaml
apiVersion: v1
kind: Service
metadata:
name: multi-protocol-service
annotations:
load-balancer.hetzner.cloud/protocol-ports: "80:http,443:https,9000:tcp"
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
- name: https
port: 443
targetPort: 8443
protocol: TCP
- name: tcp
port: 9000
targetPort: 9000
protocol: TCP
selector:
app: my-app
```

### Per-Port Certificate Configuration

Use the `load-balancer.hetzner.cloud/http-certificates-ports` annotation to specify different certificates for different HTTPS ports:

```yaml
apiVersion: v1
kind: Service
metadata:
name: multi-https-service
annotations:
load-balancer.hetzner.cloud/protocol-ports: "443:https,8443:https"
load-balancer.hetzner.cloud/http-certificates-ports: "443:cert1,cert2;8443:cert3"
spec:
type: LoadBalancer
ports:
- name: https-main
port: 443
targetPort: 8443
protocol: TCP
- name: https-alt
port: 8443
targetPort: 8443
protocol: TCP
selector:
app: my-app
```

### Format Specification

**Protocol Ports Format:**
- Format: `"port:protocol,port:protocol,..."`
- Example: `"80:http,443:https,9000:tcp"`
- Supported protocols: `tcp`, `http`, `https`

**Certificate Ports Format:**
- Format: `"port:cert1,cert2;port:cert3,..."`
- Example: `"443:cert1,cert2;8443:cert3"`
- Supports both certificate names and IDs
- Use semicolons (`;`) to separate different ports
- Use commas (`,`) to separate multiple certificates for the same port

### Fallback Behavior

- If per-port configuration is not specified for a port, the global annotation values are used
- Global annotations: `load-balancer.hetzner.cloud/protocol` and `load-balancer.hetzner.cloud/http-certificates`
- If no global annotation is set, defaults to `tcp` protocol
12 changes: 12 additions & 0 deletions internal/annotation/load_balancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ const (
// values: tcp, http, https
LBSvcProtocol Name = "load-balancer.hetzner.cloud/protocol"

// LBSvcProtocolPorts specifies the protocol per port for the service. This allows
// different ports to use different protocols. Format: "80:http,443:https,9000:tcp"
// If set, this takes precedence over LBSvcProtocol for the specified ports.
LBSvcProtocolPorts Name = "load-balancer.hetzner.cloud/protocol-ports"

// LBAlgorithmType specifies the algorithm type of the Load Balancer.
//
// Possible values: round_robin, least_connections
Expand Down Expand Up @@ -129,6 +134,13 @@ const (
// HTTPS only.
LBSvcHTTPCertificates Name = "load-balancer.hetzner.cloud/http-certificates"

// LBSvcHTTPCertificatesPorts specifies certificates per port for HTTPS services.
// Format: "443:cert1,cert2;8443:cert3,cert4"
// If set, this takes precedence over LBSvcHTTPCertificates for the specified ports.
//
// HTTPS only.
LBSvcHTTPCertificatesPorts Name = "load-balancer.hetzner.cloud/http-certificates-ports"

// LBSvcHTTPManagedCertificateName contains the names of the managed
// certificate to create by the Cloud Controller manager. Ignored if
// LBSvcHTTPCertificateType is missing or set to "uploaded". Optional.
Expand Down
106 changes: 106 additions & 0 deletions internal/annotation/name.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,112 @@ func (s Name) CertificatesFromService(svc *corev1.Service) ([]*hcloud.Certificat
return cs, err
}

// ProtocolPortsFromService retrieves the protocol configuration per port from svc.
// The annotation format is "port:protocol,port:protocol" (e.g. "80:http,443:https,9000:tcp")
//
// Returns a map of port -> protocol. Returns an empty map if the annotation was not set.
func (s Name) ProtocolPortsFromService(svc *corev1.Service) (map[int]hcloud.LoadBalancerServiceProtocol, error) {
const op = "annotation/Name.ProtocolPortsFromService"
metrics.OperationCalled.WithLabelValues(op).Inc()

result := make(map[int]hcloud.LoadBalancerServiceProtocol)

v, ok := s.StringFromService(svc)
if !ok {
return result, nil // Return empty map if not set
}

if strings.TrimSpace(v) == "" {
return result, nil
}

pairs := strings.Split(v, ",")
for _, pair := range pairs {
parts := strings.Split(strings.TrimSpace(pair), ":")
if len(parts) != 2 {
return nil, fmt.Errorf("%s: invalid format for port:protocol pair: %s", op, pair)
}

port, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return nil, fmt.Errorf("%s: invalid port number: %s", op, parts[0])
}

protocol, err := validateServiceProtocol(strings.TrimSpace(parts[1]))
if err != nil {
return nil, fmt.Errorf("%s: %w", op, err)
}

result[port] = protocol
}

return result, nil
}

// CertificatePortsFromService retrieves the certificate configuration per port from svc.
// The annotation format is "port:cert1,cert2;port:cert3,cert4" (e.g. "443:cert1,cert2;8443:cert3")
//
// Returns a map of port -> certificates. Returns an empty map if the annotation was not set.
func (s Name) CertificatePortsFromService(svc *corev1.Service) (map[int][]*hcloud.Certificate, error) {
const op = "annotation/Name.CertificatePortsFromService"
metrics.OperationCalled.WithLabelValues(op).Inc()

result := make(map[int][]*hcloud.Certificate)

v, ok := s.StringFromService(svc)
if !ok {
return result, nil // Return empty map if not set
}

if strings.TrimSpace(v) == "" {
return result, nil
}

// Split by semicolon to get port configurations
portConfigs := strings.Split(v, ";")
for _, portConfig := range portConfigs {
portConfig = strings.TrimSpace(portConfig)
if portConfig == "" {
continue
}

// Split by colon to get port and certificates
parts := strings.Split(portConfig, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("%s: invalid format for port:certificates pair: %s", op, portConfig)
}

port, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return nil, fmt.Errorf("%s: invalid port number: %s", op, parts[0])
}

// Parse certificates (same logic as CertificatesFromService)
certStrings := strings.Split(strings.TrimSpace(parts[1]), ",")
certificates := make([]*hcloud.Certificate, len(certStrings))

for i, certString := range certStrings {
certString = strings.TrimSpace(certString)
if certString == "" {
return nil, fmt.Errorf("%s: empty certificate reference", op)
}

id, err := strconv.ParseInt(certString, 10, 64)
if err != nil {
// If we could not parse the string as an integer we assume it
// is a name not an id.
certificates[i] = &hcloud.Certificate{Name: certString}
} else {
certificates[i] = &hcloud.Certificate{ID: id}
}
}

result[port] = certificates
}

return result, nil
}

// CertificateTypeFromService retrieves the hcloud.CertificateType value
// belonging to the annotation from svc.
//
Expand Down
Loading
Loading