Skip to content

Commit 87c5a0e

Browse files
committed
feat: allow to set patroni pre_promote and before_stop
1 parent 5148e9a commit 87c5a0e

6 files changed

Lines changed: 222 additions & 17 deletions

File tree

stackgres-k8s/src/cluster-controller/src/main/java/io/stackgres/cluster/controller/PatroniReconciliator.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public class PatroniReconciliator extends SafeReconciliator<StackGresClusterCont
7070
private static final Pattern TAGS_LINE_PATTERN = Pattern.compile("^tags:.*$");
7171
private static final Pattern PG_CTL_TIMEOUT_LINE_PATTERN = Pattern.compile("^ pg_ctl_timeout:.*$");
7272
private static final Pattern CALLBACKS_LINE_PATTERN = Pattern.compile("^ callbacks:.*$");
73+
private static final Pattern PRE_PROMOTE_LINE_PATTERN = Pattern.compile("^ pre_promote:.*$");
74+
private static final Pattern BEFORE_STOP_LINE_PATTERN = Pattern.compile("^ before_stop:.*$");
7375

7476
private static final String NOLOADBALANCE_TAG = PatroniUtil.NOLOADBALANCE_TAG;
7577
private static final String NOFAILOVER_TAG = PatroniUtil.NOFAILOVER_TAG;
@@ -192,6 +194,20 @@ private boolean reconcilePatroni(KubernetesClient client, StackGresClusterContex
192194
if (callbacksNeedsUpdate) {
193195
addOrReplacePatroniPostgresqlCallbacks(callbacks);
194196
}
197+
final String prePromote = getPrePromoteAsYamlString(cluster);
198+
boolean prePromoteNeedsUpdate = Seq.seq(Files.readAllLines(PATRONI_CONFIG_PATH))
199+
.filter(line -> PRE_PROMOTE_LINE_PATTERN.matcher(line).matches())
200+
.noneMatch(prePromote::equals);
201+
if (prePromoteNeedsUpdate) {
202+
addOrReplacePatroniPostgresqlPrePromote(prePromote);
203+
}
204+
final String beforeStop = getBeforeStopAsYamlString(cluster);
205+
boolean beforeStopNeedsUpdate = Seq.seq(Files.readAllLines(PATRONI_CONFIG_PATH))
206+
.filter(line -> BEFORE_STOP_LINE_PATTERN.matcher(line).matches())
207+
.noneMatch(beforeStop::equals);
208+
if (beforeStopNeedsUpdate) {
209+
addOrReplacePatroniPostgresqlBeforeStop(beforeStop);
210+
}
195211
if (configChanged(PATRONI_CONFIG_PATH, LAST_PATRONI_CONFIG_PATH)) {
196212
PatroniCommandUtil.reloadPatroniConfig();
197213
setPatroniTagsAsPodLabels(client, cluster, tagsMap);
@@ -342,6 +358,64 @@ private void addOrReplacePatroniPostgresqlCallbacks(final String callbacks) {
342358
}
343359
}
344360

361+
private String getPrePromoteAsYamlString(final StackGresCluster cluster) {
362+
return String.format(" pre_promote: %s",
363+
Optional.of(cluster.getSpec())
364+
.map(StackGresClusterSpec::getConfigurations)
365+
.map(StackGresClusterConfigurations::getPatroni)
366+
.map(StackGresClusterPatroni::getInitialConfig)
367+
.flatMap(StackGresClusterPatroniConfig::getPrePromote)
368+
.map(String::valueOf)
369+
.orElse(""));
370+
}
371+
372+
private void addOrReplacePatroniPostgresqlPrePromote(final String prePromote) {
373+
var hasPrePromote =
374+
FluentProcess.start("grep", "-q", "^ *pre_promote:.*$",
375+
PATRONI_CONFIG_PATH.toString()).tryGet();
376+
String escapedPrePromote = prePromote
377+
.replace("\\", "\\\\")
378+
.replace("/", "\\/");
379+
if (hasPrePromote.exception().isEmpty()) {
380+
FluentProcess.start("sed", "-i",
381+
String.format("s/^ *pre_promote:.*$/%s/", escapedPrePromote),
382+
PATRONI_CONFIG_PATH.toString()).join();
383+
} else {
384+
FluentProcess.start("sed", "-i",
385+
String.format("s/^postgresql:$/postgresql:\\n%s/", escapedPrePromote),
386+
PATRONI_CONFIG_PATH.toString()).join();
387+
}
388+
}
389+
390+
private String getBeforeStopAsYamlString(final StackGresCluster cluster) {
391+
return String.format(" before_stop: %s",
392+
Optional.of(cluster.getSpec())
393+
.map(StackGresClusterSpec::getConfigurations)
394+
.map(StackGresClusterConfigurations::getPatroni)
395+
.map(StackGresClusterPatroni::getInitialConfig)
396+
.flatMap(StackGresClusterPatroniConfig::getBeforeStop)
397+
.map(String::valueOf)
398+
.orElse(""));
399+
}
400+
401+
private void addOrReplacePatroniPostgresqlBeforeStop(final String beforeStop) {
402+
var hasBeforeStop =
403+
FluentProcess.start("grep", "-q", "^ *before_stop:.*$",
404+
PATRONI_CONFIG_PATH.toString()).tryGet();
405+
String escapedBeforeStop = beforeStop
406+
.replace("\\", "\\\\")
407+
.replace("/", "\\/");
408+
if (hasBeforeStop.exception().isEmpty()) {
409+
FluentProcess.start("sed", "-i",
410+
String.format("s/^ *before_stop:.*$/%s/", escapedBeforeStop),
411+
PATRONI_CONFIG_PATH.toString()).join();
412+
} else {
413+
FluentProcess.start("sed", "-i",
414+
String.format("s/^postgresql:$/postgresql:\\n%s/", escapedBeforeStop),
415+
PATRONI_CONFIG_PATH.toString()).join();
416+
}
417+
}
418+
345419
private void setPatroniTagsAsPodLabels(KubernetesClient client, final StackGresCluster cluster,
346420
final Map<String, String> tagsMap) {
347421
KubernetesClientUtil.retryOnConflict(() -> {

stackgres-k8s/src/common/src/main/java/io/stackgres/common/crd/sgcluster/StackGresClusterPatroniConfig.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ public void setCallbacks(Map<String, Object> callbacks) {
7777
.ifPresent(postgresql -> postgresql.put("callbacks", new JsonObject(callbacks)));
7878
}
7979

80+
@JsonIgnore
81+
public Optional<String> getPrePromote() {
82+
return getPostgresql()
83+
.map(postgresql -> postgresql.get("pre_promote"))
84+
.filter(String.class::isInstance)
85+
.map(String.class::cast);
86+
}
87+
88+
@JsonIgnore
89+
public void setPrePromote(String prePromote) {
90+
getWritablePostgresql()
91+
.ifPresent(postgresql -> postgresql.put("pre_promote", prePromote));
92+
}
93+
94+
@JsonIgnore
95+
public Optional<String> getBeforeStop() {
96+
return getPostgresql()
97+
.map(postgresql -> postgresql.get("before_stop"))
98+
.filter(String.class::isInstance)
99+
.map(String.class::cast);
100+
}
101+
102+
@JsonIgnore
103+
public void setBeforeStop(String beforeStop) {
104+
getWritablePostgresql()
105+
.ifPresent(postgresql -> postgresql.put("before_stop", beforeStop));
106+
}
107+
80108
private Optional<JsonObject> getWritablePostgresql() {
81109
return Optional.of(this)
82110
.filter(config -> config.hasWritableObject("postgresql"))

stackgres-k8s/src/common/src/main/resources/crds/SGCluster.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1223,7 +1223,7 @@ spec:
12231223
* log
12241224
* bootstrap
12251225
* citus
1226-
* postgresql # with the exception of callbacks and postgresql.pg_ctl_timeout
1226+
* postgresql # with the exception of postgresql.callbacks, postgresql.pre_promote, postgresql.before_stop and postgresql.pg_ctl_timeout
12271227
* restapi
12281228
* ctl
12291229
* watchdog

stackgres-k8s/src/common/src/main/resources/crds/SGShardedCluster.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1445,7 +1445,7 @@ spec:
14451445
* log
14461446
* bootstrap
14471447
* citus
1448-
* postgresql # with the exception of callbacks and postgresql.pg_ctl_timeout
1448+
* postgresql # with the exception of postgresql.callbacks, postgresql.pre_promote, postgresql.before_stop and postgresql.pg_ctl_timeout
14491449
* restapi
14501450
* ctl
14511451
* watchdog

stackgres-k8s/src/operator/src/main/java/io/stackgres/operator/validation/cluster/PatroniInitialConfigValidator.java

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,31 @@ public void validate(StackGresClusterReview review) throws ValidationFailed {
4848
.flatMap(StackGresClusterPatroniConfig::getPgCtlTimeout);
4949
var oldPgCtlTimeout = oldPatroniInitialConfig
5050
.flatMap(StackGresClusterPatroniConfig::getPgCtlTimeout);
51-
51+
5252
var callbacks = patroniInitialConfig
5353
.flatMap(StackGresClusterPatroniConfig::getCallbacks);
5454
var oldCallbacks = oldPatroniInitialConfig
5555
.flatMap(StackGresClusterPatroniConfig::getCallbacks);
5656

57+
var prePromote = patroniInitialConfig
58+
.flatMap(StackGresClusterPatroniConfig::getPrePromote);
59+
var oldPrePromote = oldPatroniInitialConfig
60+
.flatMap(StackGresClusterPatroniConfig::getPrePromote);
61+
62+
var beforeStop = patroniInitialConfig
63+
.flatMap(StackGresClusterPatroniConfig::getBeforeStop);
64+
var oldBeforeStop = oldPatroniInitialConfig
65+
.flatMap(StackGresClusterPatroniConfig::getBeforeStop);
66+
5767
if (!Objects.equals(pgCtlTimeout, oldPgCtlTimeout)
58-
|| !Objects.equals(callbacks, oldCallbacks)) {
59-
if ((pgCtlTimeout.isPresent() || callbacks.isPresent()) && oldPatroniInitialConfig.isEmpty()) {
68+
|| !Objects.equals(callbacks, oldCallbacks)
69+
|| !Objects.equals(prePromote, oldPrePromote)
70+
|| !Objects.equals(beforeStop, oldBeforeStop)) {
71+
if ((pgCtlTimeout.isPresent()
72+
|| callbacks.isPresent()
73+
|| prePromote.isPresent()
74+
|| beforeStop.isPresent())
75+
&& oldPatroniInitialConfig.isEmpty()) {
6076
if (oldSpec.getConfigurations() == null) {
6177
oldSpec.setConfigurations(new StackGresClusterConfigurations());
6278
}
@@ -72,13 +88,18 @@ public void validate(StackGresClusterReview review) throws ValidationFailed {
7288
.map(StackGresClusterPatroni::getInitialConfig);
7389
}
7490
final Optional<StackGresClusterPatroniConfig> modifiableOldPatroniInitialConfig = oldPatroniInitialConfig;
75-
pgCtlTimeout
76-
.ifPresent(
77-
value -> modifiableOldPatroniInitialConfig.ifPresent(config -> config.setPgCtlTimeout(value)));
78-
callbacks
79-
.ifPresent(
80-
value -> modifiableOldPatroniInitialConfig.ifPresent(config -> config.setCallbacks(value)));
81-
if (pgCtlTimeout.isEmpty() && callbacks.isEmpty()) {
91+
pgCtlTimeout.ifPresent(
92+
value -> modifiableOldPatroniInitialConfig.ifPresent(config -> config.setPgCtlTimeout(value)));
93+
callbacks.ifPresent(
94+
value -> modifiableOldPatroniInitialConfig.ifPresent(config -> config.setCallbacks(value)));
95+
prePromote.ifPresent(
96+
value -> modifiableOldPatroniInitialConfig.ifPresent(config -> config.setPrePromote(value)));
97+
beforeStop.ifPresent(
98+
value -> modifiableOldPatroniInitialConfig.ifPresent(config -> config.setBeforeStop(value)));
99+
if (pgCtlTimeout.isEmpty()
100+
&& callbacks.isEmpty()
101+
&& prePromote.isEmpty()
102+
&& beforeStop.isEmpty()) {
82103
modifiableOldPatroniInitialConfig.ifPresent(config -> config.removePostgresql());
83104
}
84105
}

stackgres-k8s/src/operator/src/test/java/io/stackgres/operator/validation/cluster/PatroniInitialConfigValidatorTest.java

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,10 +133,10 @@ void givenAnUpdateWithPatroniInitialConfigWithCallbacksChanged_shouldPass() thro
133133
final StackGresClusterReview review = getUpdateReview();
134134
review.getRequest().getOldObject().getSpec().getConfigurations()
135135
.getPatroni().getInitialConfig().put(
136-
"postgresql", Map.of("callbacks", Map.of("on_role_change", "sh -c 'echo \"on_role_change: $*\"'")));
136+
"postgresql", Map.of("callbacks", Map.of("on_role_change", "/usr/local/bin/on_role_change")));
137137
review.getRequest().getObject().getSpec().getConfigurations()
138138
.getPatroni().getInitialConfig().put(
139-
"postgresql", Map.of("callbacks", Map.of("on_start", "sh -c 'echo \"on_start: $*\"'")));
139+
"postgresql", Map.of("callbacks", Map.of("on_start", "/usr/local/bin/on_start")));
140140

141141
validator.validate(review);
142142
}
@@ -146,7 +146,7 @@ void givenAnUpdateWithPatroniInitialConfigWithCallbacksAdded_shouldPass() throws
146146
final StackGresClusterReview review = getUpdateReview();
147147
review.getRequest().getObject().getSpec().getConfigurations()
148148
.getPatroni().getInitialConfig().put(
149-
"postgresql", Map.of("callbacks", Map.of("on_start", "sh -c 'echo \"on_start: $*\"'")));
149+
"postgresql", Map.of("callbacks", Map.of("on_start", "/usr/local/bin/on_start")));
150150

151151
validator.validate(review);
152152
}
@@ -159,7 +159,7 @@ void givenAnUpdateWithPatroniInitialConfigWithCallbacksAddedFromScratch_shouldPa
159159
.setInitialConfig(new StackGresClusterPatroniConfig());
160160
review.getRequest().getObject().getSpec().getConfigurations()
161161
.getPatroni().getInitialConfig().put(
162-
"postgresql", Map.of("callbacks", Map.of("on_start", "sh -c 'echo \"on_start: $*\"'")));
162+
"postgresql", Map.of("callbacks", Map.of("on_start", "/usr/local/bin/on_start")));
163163

164164
validator.validate(review);
165165
}
@@ -169,7 +169,89 @@ void givenAnUpdateWithPatroniInitialConfigWithCallbacksRemoved_shouldPass() thro
169169
final StackGresClusterReview review = getUpdateReview();
170170
review.getRequest().getOldObject().getSpec().getConfigurations()
171171
.getPatroni().getInitialConfig().put(
172-
"postgresql", Map.of("callbacks", Map.of("on_role_change", "sh -c 'echo \"on_role_change: $*\"'")));
172+
"postgresql", Map.of("callbacks", Map.of("on_role_change", "/usr/local/bin/on_role_change")));
173+
174+
validator.validate(review);
175+
}
176+
177+
@Test
178+
void givenAnUpdateWithPatroniInitialConfigWithPrePromoteChanged_shouldPass() throws ValidationFailed {
179+
final StackGresClusterReview review = getUpdateReview();
180+
review.getRequest().getOldObject().getSpec().getConfigurations()
181+
.getPatroni().getInitialConfig().put("postgresql", Map.of("pre_promote", "/usr/local/bin/pre_promote"));
182+
review.getRequest().getObject().getSpec().getConfigurations()
183+
.getPatroni().getInitialConfig().put("postgresql", Map.of("pre_promote", "/usr/local/bin/pre_promote_2"));
184+
185+
validator.validate(review);
186+
}
187+
188+
@Test
189+
void givenAnUpdateWithPatroniInitialConfigWithPrePromoteAdded_shouldPass() throws ValidationFailed {
190+
final StackGresClusterReview review = getUpdateReview();
191+
review.getRequest().getObject().getSpec().getConfigurations()
192+
.getPatroni().getInitialConfig().put("postgresql", Map.of("pre_promote", "/usr/local/bin/pre_promote_2"));
193+
194+
validator.validate(review);
195+
}
196+
197+
@Test
198+
void givenAnUpdateWithPatroniInitialConfigWithPrePromoteAddedFromScratch_shouldPass() throws ValidationFailed {
199+
final StackGresClusterReview review = getUpdateReview();
200+
review.getRequest().getOldObject().getSpec().getConfigurations().getPatroni().setInitialConfig(null);
201+
review.getRequest().getObject().getSpec().getConfigurations().getPatroni()
202+
.setInitialConfig(new StackGresClusterPatroniConfig());
203+
review.getRequest().getObject().getSpec().getConfigurations()
204+
.getPatroni().getInitialConfig().put("postgresql", Map.of("pre_promote", "/usr/local/bin/pre_promote_2"));
205+
206+
validator.validate(review);
207+
}
208+
209+
@Test
210+
void givenAnUpdateWithPatroniInitialConfigWithPrePromoteRemoved_shouldPass() throws ValidationFailed {
211+
final StackGresClusterReview review = getUpdateReview();
212+
review.getRequest().getOldObject().getSpec().getConfigurations()
213+
.getPatroni().getInitialConfig().put("postgresql", Map.of("pre_promote", "/usr/local/bin/pre_promote"));
214+
215+
validator.validate(review);
216+
}
217+
218+
@Test
219+
void givenAnUpdateWithPatroniInitialConfigWithBeforeStopChanged_shouldPass() throws ValidationFailed {
220+
final StackGresClusterReview review = getUpdateReview();
221+
review.getRequest().getOldObject().getSpec().getConfigurations()
222+
.getPatroni().getInitialConfig().put("postgresql", Map.of("before_stop", "/usr/local/bin/before_stop"));
223+
review.getRequest().getObject().getSpec().getConfigurations()
224+
.getPatroni().getInitialConfig().put("postgresql", Map.of("before_stop", "/usr/local/bin/before_stop_2"));
225+
226+
validator.validate(review);
227+
}
228+
229+
@Test
230+
void givenAnUpdateWithPatroniInitialConfigWithBeforeStopAdded_shouldPass() throws ValidationFailed {
231+
final StackGresClusterReview review = getUpdateReview();
232+
review.getRequest().getObject().getSpec().getConfigurations()
233+
.getPatroni().getInitialConfig().put("postgresql", Map.of("before_stop", "/usr/local/bin/before_stop_2"));
234+
235+
validator.validate(review);
236+
}
237+
238+
@Test
239+
void givenAnUpdateWithPatroniInitialConfigWithBeforeStopAddedFromScratch_shouldPass() throws ValidationFailed {
240+
final StackGresClusterReview review = getUpdateReview();
241+
review.getRequest().getOldObject().getSpec().getConfigurations().getPatroni().setInitialConfig(null);
242+
review.getRequest().getObject().getSpec().getConfigurations().getPatroni()
243+
.setInitialConfig(new StackGresClusterPatroniConfig());
244+
review.getRequest().getObject().getSpec().getConfigurations()
245+
.getPatroni().getInitialConfig().put("postgresql", Map.of("before_stop", "/usr/local/bin/before_stop_2"));
246+
247+
validator.validate(review);
248+
}
249+
250+
@Test
251+
void givenAnUpdateWithPatroniInitialConfigWithBeforeStopRemoved_shouldPass() throws ValidationFailed {
252+
final StackGresClusterReview review = getUpdateReview();
253+
review.getRequest().getOldObject().getSpec().getConfigurations()
254+
.getPatroni().getInitialConfig().put("postgresql", Map.of("before_stop", "/usr/local/bin/before_stop"));
173255

174256
validator.validate(review);
175257
}

0 commit comments

Comments
 (0)