Skip to content

Commit b776b4d

Browse files
committed
Add leader election to PGO, including necessary RBAC for leases and tests. Return error if PGO_CONTROLLER_LEASE_NAME is invalid.
1 parent c7a885d commit b776b4d

File tree

6 files changed

+135
-4
lines changed

6 files changed

+135
-4
lines changed

cmd/postgres-operator/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func main() {
9191
// deprecation warnings when using an older version of a resource for backwards compatibility).
9292
rest.SetDefaultWarningHandler(rest.NoWarnings{})
9393

94-
mgr, err := runtime.CreateRuntimeManager(os.Getenv("PGO_TARGET_NAMESPACE"), cfg, false)
94+
mgr, err := runtime.CreateRuntimeManager(ctx, os.Getenv("PGO_TARGET_NAMESPACE"), cfg, false)
9595
assertNoError(err)
9696

9797
openshift := isOpenshift(cfg)

config/rbac/cluster/role.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ rules:
8888
- list
8989
- patch
9090
- watch
91+
- apiGroups:
92+
- coordination.k8s.io
93+
resources:
94+
- leases
95+
verbs:
96+
- create
97+
- get
98+
- update
99+
- watch
91100
- apiGroups:
92101
- policy
93102
resources:

config/rbac/namespace/role.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,15 @@ rules:
8888
- list
8989
- patch
9090
- watch
91+
- apiGroups:
92+
- coordination.k8s.io
93+
resources:
94+
- leases
95+
verbs:
96+
- create
97+
- get
98+
- update
99+
- watch
91100
- apiGroups:
92101
- policy
93102
resources:

internal/controller/postgrescluster/helpers_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,15 @@ func testCluster() *v1beta1.PostgresCluster {
158158
// setupManager creates the runtime manager used during controller testing
159159
func setupManager(t *testing.T, cfg *rest.Config,
160160
controllerSetup func(mgr manager.Manager)) (context.Context, context.CancelFunc) {
161+
ctx, cancel := context.WithCancel(context.Background())
161162

162-
mgr, err := runtime.CreateRuntimeManager("", cfg, true)
163+
mgr, err := runtime.CreateRuntimeManager(ctx, "", cfg, true)
163164
if err != nil {
164165
t.Fatal(err)
165166
}
166167

167168
controllerSetup(mgr)
168169

169-
ctx, cancel := context.WithCancel(context.Background())
170170
go func() {
171171
if err := mgr.Start(ctx); err != nil {
172172
t.Error(err)

internal/controller/runtime/runtime.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,19 @@ limitations under the License.
1616
package runtime
1717

1818
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
"os"
1923
"time"
2024

2125
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/util/validation"
2227
"k8s.io/client-go/kubernetes/scheme"
2328
"k8s.io/client-go/rest"
2429
"sigs.k8s.io/controller-runtime/pkg/cache"
2530
"sigs.k8s.io/controller-runtime/pkg/client/config"
31+
"sigs.k8s.io/controller-runtime/pkg/log"
2632
"sigs.k8s.io/controller-runtime/pkg/manager"
2733

2834
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
@@ -48,8 +54,12 @@ var refreshInterval = 60 * time.Minute
4854
// controllers that will be responsible for managing PostgreSQL clusters using the
4955
// 'postgrescluster' custom resource. Additionally, the manager will only watch for resources in
5056
// the namespace specified, with an empty string resulting in the manager watching all namespaces.
51-
func CreateRuntimeManager(namespace string, config *rest.Config,
57+
58+
// +kubebuilder:rbac:groups="coordination.k8s.io",resources="leases",verbs={get,create,update}
59+
60+
func CreateRuntimeManager(ctx context.Context, namespace string, config *rest.Config,
5261
disableMetrics bool) (manager.Manager, error) {
62+
log := log.FromContext(ctx)
5363

5464
// Watch all namespaces by default
5565
options := manager.Options{
@@ -70,6 +80,14 @@ func CreateRuntimeManager(namespace string, config *rest.Config,
7080
options.Metrics.BindAddress = "0"
7181
}
7282

83+
// Add leader election options
84+
options, err := addLeaderElectionOptions(options)
85+
if err != nil {
86+
return nil, err
87+
} else {
88+
log.Info("Leader election enabled.")
89+
}
90+
7391
// create controller runtime manager
7492
mgr, err := manager.New(config, options)
7593
if err != nil {
@@ -81,3 +99,33 @@ func CreateRuntimeManager(namespace string, config *rest.Config,
8199

82100
// GetConfig creates a *rest.Config for talking to a Kubernetes API server.
83101
func GetConfig() (*rest.Config, error) { return config.GetConfig() }
102+
103+
// addLeaderElectionOptions takes the manager.Options as an argument and will
104+
// add leader election options if PGO_CONTROLLER_LEASE_NAME is set and valid.
105+
// If PGO_CONTROLLER_LEASE_NAME is not valid, the function will return the
106+
// original options and an error. If PGO_CONTROLLER_LEASE_NAME is not set at all,
107+
// the function will return the original options.
108+
func addLeaderElectionOptions(opts manager.Options) (manager.Options, error) {
109+
errs := []error{}
110+
111+
leaderLeaseName := os.Getenv("PGO_CONTROLLER_LEASE_NAME")
112+
if len(leaderLeaseName) > 0 {
113+
// If no errors are returned by IsDNS1123Subdomain(), turn on leader election,
114+
// otherwise, return the errors
115+
dnsSubdomainErrors := validation.IsDNS1123Subdomain(leaderLeaseName)
116+
if len(dnsSubdomainErrors) == 0 {
117+
opts.LeaderElection = true
118+
opts.LeaderElectionNamespace = os.Getenv("PGO_NAMESPACE")
119+
opts.LeaderElectionID = leaderLeaseName
120+
} else {
121+
for _, errString := range dnsSubdomainErrors {
122+
err := errors.New(errString)
123+
errs = append(errs, err)
124+
}
125+
126+
return opts, fmt.Errorf("value for PGO_CONTROLLER_LEASE_NAME is invalid: %v", errs)
127+
}
128+
}
129+
130+
return opts, nil
131+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
Copyright 2021 - 2024 Crunchy Data Solutions, Inc.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package runtime
17+
18+
import (
19+
"testing"
20+
21+
"gotest.tools/v3/assert"
22+
"sigs.k8s.io/controller-runtime/pkg/manager"
23+
)
24+
25+
func TestAddLeaderElectionOptions(t *testing.T) {
26+
t.Setenv("PGO_NAMESPACE", "test-namespace")
27+
28+
t.Run("PGO_CONTROLLER_LEASE_NAME is not set", func(t *testing.T) {
29+
opts := manager.Options{HealthProbeBindAddress: "0"}
30+
31+
opts, err := addLeaderElectionOptions(opts)
32+
33+
assert.NilError(t, err)
34+
assert.Assert(t, opts.HealthProbeBindAddress == "0")
35+
assert.Assert(t, !opts.LeaderElection)
36+
assert.Assert(t, opts.LeaderElectionNamespace == "")
37+
assert.Assert(t, opts.LeaderElectionID == "")
38+
})
39+
40+
t.Run("PGO_CONTROLLER_LEASE_NAME is invalid", func(t *testing.T) {
41+
t.Setenv("PGO_CONTROLLER_LEASE_NAME", "INVALID_NAME")
42+
opts := manager.Options{HealthProbeBindAddress: "0"}
43+
44+
opts, err := addLeaderElectionOptions(opts)
45+
46+
assert.ErrorContains(t, err, "value for PGO_CONTROLLER_LEASE_NAME is invalid:")
47+
assert.Assert(t, opts.HealthProbeBindAddress == "0")
48+
assert.Assert(t, !opts.LeaderElection)
49+
assert.Assert(t, opts.LeaderElectionNamespace == "")
50+
assert.Assert(t, opts.LeaderElectionID == "")
51+
})
52+
53+
t.Run("PGO_CONTROLLER_LEASE_NAME is valid", func(t *testing.T) {
54+
t.Setenv("PGO_CONTROLLER_LEASE_NAME", "valid-name")
55+
opts := manager.Options{HealthProbeBindAddress: "0"}
56+
57+
opts, err := addLeaderElectionOptions(opts)
58+
59+
assert.NilError(t, err)
60+
assert.Assert(t, opts.HealthProbeBindAddress == "0")
61+
assert.Assert(t, opts.LeaderElection)
62+
assert.Assert(t, opts.LeaderElectionNamespace == "test-namespace")
63+
assert.Assert(t, opts.LeaderElectionID == "valid-name")
64+
})
65+
}

0 commit comments

Comments
 (0)