Skip to content

Commit 1c4a634

Browse files
committed
Allow NodePort Port to be Specified via the PostgresCluster Spec
This update allows a specific NodePort port to be specified for the primary Postgres, pgBouncer and pgAdmin services via the PostgresCluster spec. Note this is used when type is NodePort or LoadBalancer only. Setting this value when using the 'ClusterIP' type will result in an error. The specified value must be also be in-range and not currently in use or the operation will fail. If unspecified, a port will be allocated if this Service requires one as before. Resolves #3008 Issue: [sc-14918]
1 parent 1a4b6bf commit 1c4a634

File tree

11 files changed

+348
-58
lines changed

11 files changed

+348
-58
lines changed

config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10426,6 +10426,14 @@ spec:
1042610426
service:
1042710427
description: Specification of the service that exposes PgBouncer.
1042810428
properties:
10429+
nodePort:
10430+
description: The port on which this service is exposed
10431+
when type is NodePort or LoadBalancer. Value must be
10432+
in-range and not in use or the operation will fail.
10433+
If unspecified, a port will be allocated if this Service
10434+
requires one. - https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
10435+
format: int32
10436+
type: integer
1042910437
type:
1043010438
description: 'More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types'
1043110439
enum:
@@ -10633,6 +10641,13 @@ spec:
1063310641
description: Specification of the service that exposes the PostgreSQL
1063410642
primary instance.
1063510643
properties:
10644+
nodePort:
10645+
description: The port on which this service is exposed when type
10646+
is NodePort or LoadBalancer. Value must be in-range and not
10647+
in use or the operation will fail. If unspecified, a port will
10648+
be allocated if this Service requires one. - https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
10649+
format: int32
10650+
type: integer
1063610651
type:
1063710652
description: 'More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types'
1063810653
enum:
@@ -11795,6 +11810,14 @@ spec:
1179511810
service:
1179611811
description: Specification of the service that exposes pgAdmin.
1179711812
properties:
11813+
nodePort:
11814+
description: The port on which this service is exposed
11815+
when type is NodePort or LoadBalancer. Value must be
11816+
in-range and not in use or the operation will fail.
11817+
If unspecified, a port will be allocated if this Service
11818+
requires one. - https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
11819+
format: int32
11820+
type: integer
1179811821
type:
1179911822
description: 'More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types'
1180011823
enum:

docs/content/references/crd.md

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/content/tutorial/connect-cluster.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,28 @@ All connections are over TLS. PGO provides its own certificate authority (CA) to
4242

4343
### Modifying Service Type
4444

45-
By default, PGO deploys Services with the `ClusterIP` Service type. Based on how you want to expose your database, you may want to modify the Services to use a different [Service type](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types).
45+
By default, PGO deploys Services with the `ClusterIP` Service type. Based on how you want to expose your database,
46+
you may want to modify the Services to use a different
47+
[Service type](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types)
48+
and [NodePort value](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport).
4649

4750
You can modify the Services that PGO manages from the following attributes:
4851

4952
- `spec.service` - this manages the Service for connecting to a Postgres primary.
5053
- `spec.proxy.pgBouncer.service` - this manages the Service for connecting to the PgBouncer connection pooler.
54+
- `spec.userInterface.pgAdmin.service` - this manages the Service for connecting to the pgAdmin management tool.
5155

52-
For example, to set the Postgres primary to use a `NodePort` service, you would add the following to your manifest:
56+
For example, to set the Postgres primary to use a `NodePort` service and specific `nodePort` value, you would add the
57+
following to your manifest:
5358

5459
```yaml
5560
spec:
5661
service:
5762
type: NodePort
63+
nodePort: 32000
5864
```
5965
60-
For our `hippo` cluster, you would see the Service type modification in the . For example:
66+
For our `hippo` cluster, you would see the Service type and nodePort modification. For example:
6167

6268
```
6369
kubectl -n postgres-operator get svc --selector=postgres-operator.crunchydata.com/cluster=hippo
@@ -66,15 +72,18 @@ kubectl -n postgres-operator get svc --selector=postgres-operator.crunchydata.co
6672
will yield something similar to:
6773
6874
```
69-
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
70-
hippo-ha NodePort 10.96.17.210 <none> 5432:32751/TCP 2m37s
71-
hippo-ha-config ClusterIP None <none> <none> 2m37s
72-
hippo-pods ClusterIP None <none> <none> 2m37s
73-
hippo-primary ClusterIP None <none> 5432/TCP 2m37s
74-
hippo-replicas ClusterIP 10.96.151.53 <none> 5432/TCP 2m37s
75+
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
76+
hippo-ha NodePort 10.105.57.191 <none> 5432:32000/TCP 48s
77+
hippo-ha-config ClusterIP None <none> <none> 48s
78+
hippo-pods ClusterIP None <none> <none> 48s
79+
hippo-primary ClusterIP None <none> 5432/TCP 48s
80+
hippo-replicas ClusterIP 10.106.18.99 <none> 5432/TCP 48s
7581
```
7682
77-
(Note that if you are exposing your Services externally and are relying on TLS verification, you will need to use the [custom TLS]({{< relref "tutorial/customize-cluster.md" >}}#customize-tls) features of PGO).
83+
Note that setting the `nodePort` value is not allowed when using the `ClusterIP` type, and it must be in-range and
84+
not otherwise in use or the operation will fail. Also, if you are exposing your Services externally and are relying on TLS
85+
verification, you will need to use the [custom TLS]({{< relref "tutorial/customize-cluster.md" >}}#customize-tls)
86+
features of PGO).
7887
7988
## Connect an Application
8089

internal/controller/postgrescluster/patroni.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package postgrescluster
1717

1818
import (
1919
"context"
20+
"fmt"
2021
"io"
2122
"time"
2223

@@ -246,21 +247,37 @@ func (r *Reconciler) generatePatroniLeaderLeaseService(
246247
// Patroni will ensure that they always route to the elected leader.
247248
// - https://docs.k8s.io/concepts/services-networking/service/#services-without-selectors
248249
service.Spec.Selector = nil
249-
if cluster.Spec.Service != nil {
250-
service.Spec.Type = corev1.ServiceType(cluster.Spec.Service.Type)
251-
} else {
252-
service.Spec.Type = corev1.ServiceTypeClusterIP
253-
}
254250

255251
// The TargetPort must be the name (not the number) of the PostgreSQL
256252
// ContainerPort. This name allows the port number to differ between
257253
// instances, which can happen during a rolling update.
258-
service.Spec.Ports = []corev1.ServicePort{{
254+
servicePort := corev1.ServicePort{
259255
Name: naming.PortPostgreSQL,
260256
Port: *cluster.Spec.Port,
261257
Protocol: corev1.ProtocolTCP,
262258
TargetPort: intstr.FromString(naming.PortPostgreSQL),
263-
}}
259+
}
260+
261+
if spec := cluster.Spec.Service; spec == nil {
262+
service.Spec.Type = corev1.ServiceTypeClusterIP
263+
} else {
264+
service.Spec.Type = corev1.ServiceType(spec.Type)
265+
if spec.NodePort != nil {
266+
if service.Spec.Type == corev1.ServiceTypeClusterIP {
267+
// The NodePort can only be set when the Service type is NodePort or
268+
// LoadBalancer. However, due to a known issue prior to Kubernetes
269+
// 1.20, we clear these errors during our apply. To preserve the
270+
// appropriate behavior, we log an Event and return an error.
271+
// TODO(tjmoore4): Once Validation Rules are available, this check
272+
// and event could potentially be removed in favor of that validation
273+
r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "MisconfiguredClusterIP",
274+
"NodePort cannot be set with type ClusterIP on Service %q", service.Name)
275+
return nil, fmt.Errorf("NodePort cannot be set with type ClusterIP on Service %q", service.Name)
276+
}
277+
servicePort.NodePort = *spec.NodePort
278+
}
279+
}
280+
service.Spec.Ports = []corev1.ServicePort{servicePort}
264281

265282
err := errors.WithStack(r.setControllerReference(cluster, service))
266283
return service, err

internal/controller/postgrescluster/patroni_test.go

Lines changed: 66 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
apierrors "k8s.io/apimachinery/pkg/api/errors"
3636
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3737
"k8s.io/apimachinery/pkg/types"
38+
"k8s.io/client-go/tools/record"
3839
"sigs.k8s.io/controller-runtime/pkg/client"
3940
"sigs.k8s.io/controller-runtime/pkg/reconcile"
4041

@@ -48,7 +49,10 @@ func TestGeneratePatroniLeaderLeaseService(t *testing.T) {
4849
_, cc := setupKubernetes(t)
4950
require.ParallelCapacity(t, 0)
5051

51-
reconciler := &Reconciler{Client: cc}
52+
reconciler := &Reconciler{
53+
Client: cc,
54+
Recorder: new(record.FakeRecorder),
55+
}
5256

5357
cluster := &v1beta1.PostgresCluster{}
5458
cluster.Namespace = "ns1"
@@ -75,12 +79,6 @@ ownerReferences:
7579
name: pg2
7680
uid: ""
7781
`))
78-
assert.Assert(t, marshalMatches(service.Spec.Ports, `
79-
- name: postgres
80-
port: 9876
81-
protocol: TCP
82-
targetPort: postgres
83-
`))
8482

8583
// Always gets a ClusterIP (never None).
8684
assert.Equal(t, service.Spec.ClusterIP, "")
@@ -92,9 +90,14 @@ ownerReferences:
9290
service, err := reconciler.generatePatroniLeaderLeaseService(cluster)
9391
assert.NilError(t, err)
9492
alwaysExpect(t, service)
95-
9693
// Defaults to ClusterIP.
9794
assert.Equal(t, service.Spec.Type, corev1.ServiceTypeClusterIP)
95+
assert.Assert(t, marshalMatches(service.Spec.Ports, `
96+
- name: postgres
97+
port: 9876
98+
protocol: TCP
99+
targetPort: postgres
100+
`))
98101
})
99102

100103
t.Run("AnnotationsLabels", func(t *testing.T) {
@@ -148,6 +151,61 @@ ownerReferences:
148151
assert.NilError(t, err)
149152
alwaysExpect(t, service)
150153
test.Expect(t, service)
154+
assert.Assert(t, marshalMatches(service.Spec.Ports, `
155+
- name: postgres
156+
port: 9876
157+
protocol: TCP
158+
targetPort: postgres
159+
`))
160+
})
161+
}
162+
163+
typesAndPort := []struct {
164+
Description string
165+
Type string
166+
NodePort *int32
167+
Expect func(testing.TB, *corev1.Service, error)
168+
}{
169+
{Description: "ClusterIP with Port 32000", Type: "ClusterIP",
170+
NodePort: initialize.Int32(32000), Expect: func(t testing.TB, service *corev1.Service, err error) {
171+
assert.ErrorContains(t, err, "NodePort cannot be set with type ClusterIP on Service \"pg2-ha\"")
172+
assert.Assert(t, service == nil)
173+
}},
174+
{Description: "NodePort with Port 32001", Type: "NodePort",
175+
NodePort: initialize.Int32(32001), Expect: func(t testing.TB, service *corev1.Service, err error) {
176+
assert.NilError(t, err)
177+
alwaysExpect(t, service)
178+
assert.Equal(t, service.Spec.Type, corev1.ServiceTypeNodePort)
179+
assert.Assert(t, marshalMatches(service.Spec.Ports, `
180+
- name: postgres
181+
nodePort: 32001
182+
port: 9876
183+
protocol: TCP
184+
targetPort: postgres
185+
`))
186+
}},
187+
{Description: "LoadBalancer with Port 32002", Type: "LoadBalancer",
188+
NodePort: initialize.Int32(32002), Expect: func(t testing.TB, service *corev1.Service, err error) {
189+
assert.Equal(t, service.Spec.Type, corev1.ServiceTypeLoadBalancer)
190+
assert.NilError(t, err)
191+
alwaysExpect(t, service)
192+
assert.Assert(t, marshalMatches(service.Spec.Ports, `
193+
- name: postgres
194+
nodePort: 32002
195+
port: 9876
196+
protocol: TCP
197+
targetPort: postgres
198+
`))
199+
}},
200+
}
201+
202+
for _, test := range typesAndPort {
203+
t.Run(test.Description, func(t *testing.T) {
204+
cluster := cluster.DeepCopy()
205+
cluster.Spec.Service = &v1beta1.ServiceSpec{Type: test.Type, NodePort: test.NodePort}
206+
207+
service, err := reconciler.generatePatroniLeaderLeaseService(cluster)
208+
test.Expect(t, service, err)
151209
})
152210
}
153211
}

internal/controller/postgrescluster/pgadmin.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,24 +149,40 @@ func (r *Reconciler) generatePGAdminService(
149149
naming.LabelCluster: cluster.Name,
150150
naming.LabelRole: naming.RolePGAdmin,
151151
}
152-
if spec := cluster.Spec.UserInterface.PGAdmin.Service; spec != nil {
153-
service.Spec.Type = corev1.ServiceType(spec.Type)
154-
} else {
155-
service.Spec.Type = corev1.ServiceTypeClusterIP
156-
}
157152

158153
// The TargetPort must be the name (not the number) of the pgAdmin
159154
// ContainerPort. This name allows the port number to differ between Pods,
160155
// which can happen during a rolling update.
161156
//
162157
// TODO(tjmoore4): A custom service port is not currently supported as this
163158
// requires updates to the pgAdmin service configuration.
164-
service.Spec.Ports = []corev1.ServicePort{{
159+
servicePort := corev1.ServicePort{
165160
Name: naming.PortPGAdmin,
166161
Port: *initialize.Int32(5050),
167162
Protocol: corev1.ProtocolTCP,
168163
TargetPort: intstr.FromString(naming.PortPGAdmin),
169-
}}
164+
}
165+
166+
if spec := cluster.Spec.UserInterface.PGAdmin.Service; spec == nil {
167+
service.Spec.Type = corev1.ServiceTypeClusterIP
168+
} else {
169+
service.Spec.Type = corev1.ServiceType(spec.Type)
170+
if spec.NodePort != nil {
171+
if service.Spec.Type == corev1.ServiceTypeClusterIP {
172+
// The NodePort can only be set when the Service type is NodePort or
173+
// LoadBalancer. However, due to a known issue prior to Kubernetes
174+
// 1.20, we clear these errors during our apply. To preserve the
175+
// appropriate behavior, we log an Event and return an error.
176+
// TODO(tjmoore4): Once Validation Rules are available, this check
177+
// and event could potentially be removed in favor of that validation
178+
r.Recorder.Eventf(cluster, corev1.EventTypeWarning, "MisconfiguredClusterIP",
179+
"NodePort cannot be set with type ClusterIP on Service %q", service.Name)
180+
return nil, true, fmt.Errorf("NodePort cannot be set with type ClusterIP on Service %q", service.Name)
181+
}
182+
servicePort.NodePort = *spec.NodePort
183+
}
184+
}
185+
service.Spec.Ports = []corev1.ServicePort{servicePort}
170186

171187
err := errors.WithStack(r.setControllerReference(cluster, service))
172188

0 commit comments

Comments
 (0)