Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void setErrorPhase(
message
);

status.setPhase(CRPhase.ERROR)
status.updatePhase(CRPhase.ERROR)
.setMessage(message);
}

Expand All @@ -117,7 +117,7 @@ public <E extends Exception> UpdateControl<CR> handleError(
exception
);

status.setPhase(CRPhase.ERROR)
status.updatePhase(CRPhase.ERROR)
.setMessage(exception.getMessage());

return UpdateControl.patchStatus(resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ public class CRStatus {
private long observedGeneration = 0;

/// Update the current phase. When the phase changes, the [#lastPhaseTransitionTime]
/// is updated to the current UTC time and the message is set to `null`.
/// is updated to the current UTC time.
///
/// Deliberately not a setter: Jackson would call it while it reads a status from the cluster.
/// The API server sends `lastPhaseTransitionTime` before `phase`, so the stored time would be
/// overwritten with now.
///
/// @param newPhase the new phase
/// @return this status instance
public CRStatus setPhase(CRPhase newPhase) {
public CRStatus updatePhase(CRPhase newPhase) {
if (this.phase == newPhase) {
return this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public UpdateControl<ClusterConnection> reconcile(
try (var dsl = contextFactory.getDSLContext(resource)) {
var version = dsl.fetchSingle("select version()").into(String.class);

status.setPhase(CRPhase.READY).setMessage(version);
status.updatePhase(CRPhase.READY).setMessage(version);

return UpdateControl.patchStatus(resource);
} catch (RuntimeException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public UpdateControl<Database> reconcile(
);

if (clusterConnectionOptional.isEmpty()) {
status.setPhase(CRPhase.PENDING)
status.updatePhase(CRPhase.PENDING)
.setMessage("The specified ClusterConnection does not exist or is not ready yet [resource=%s/%s]".formatted(
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
clusterRef.getName()
Expand Down Expand Up @@ -110,7 +110,7 @@ public DeleteControl cleanup(
);

if (status.getPhase() != CRPhase.DELETING) {
status.setPhase(CRPhase.DELETING);
status.updatePhase(CRPhase.DELETING);

if (spec.getReclaimPolicy() == ReclaimPolicy.DELETE) {
status.setMessage("Database deletion in progress");
Expand Down Expand Up @@ -201,7 +201,7 @@ private UpdateControl<Database> reconcileWithoutTransaction(
spec
);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand All @@ -218,7 +218,7 @@ private UpdateControl<Database> reconcileWithoutTransaction(
name
);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand All @@ -232,7 +232,7 @@ private UpdateControl<Database> reconcileWithoutTransaction(

databaseService.changeDatabaseOwner(dsl, spec);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage("Database owner changed [previousOwner=%s, newOwner=%s]".formatted(
currentOwner,
expectedOwner
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public UpdateControl<DefaultPrivilege> reconcile(
}

if (clusterConnectionOptional.isEmpty()) {
status.setPhase(CRPhase.PENDING)
status.updatePhase(CRPhase.PENDING)
.setMessage("The specified ClusterConnection does not exist or is not ready yet [resource=%s/%s]".formatted(
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
clusterRef.getName()
Expand Down Expand Up @@ -138,7 +138,7 @@ public DeleteControl cleanup(
);

if (status.getPhase() != CRPhase.DELETING) {
status.setPhase(CRPhase.DELETING)
status.updatePhase(CRPhase.DELETING)
.setMessage("DefaultPrivilege deletion in progress");

context.getClient().resource(resource).patchStatus();
Expand Down Expand Up @@ -290,7 +290,7 @@ private UpdateControl<DefaultPrivilege> reconcileInTransaction(
);
}

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public UpdateControl<Grant> reconcile(
}

if (clusterConnectionOptional.isEmpty()) {
status.setPhase(CRPhase.PENDING)
status.updatePhase(CRPhase.PENDING)
.setMessage("The specified ClusterConnection does not exist or is not ready yet [resource=%s/%s]".formatted(
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
clusterRef.getName()
Expand Down Expand Up @@ -143,7 +143,7 @@ public DeleteControl cleanup(
);

if (status.getPhase() != CRPhase.DELETING) {
status.setPhase(CRPhase.DELETING)
status.updatePhase(CRPhase.DELETING)
.setMessage("Grant deletion in progress");

context.getClient().resource(resource).patchStatus();
Expand Down Expand Up @@ -421,7 +421,7 @@ private UpdateControl<Grant> reconcileInTransaction(
);
}

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(message);

return UpdateControl.patchStatus(resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public UpdateControl<Role> reconcile(
);

if (clusterConnectionOptional.isEmpty()) {
status.setPhase(CRPhase.PENDING)
status.updatePhase(CRPhase.PENDING)
.setMessage("The specified ClusterConnection does not exist or is not ready yet [resource=%s/%s]".formatted(
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
clusterRef.getName()
Expand Down Expand Up @@ -169,7 +169,7 @@ public DeleteControl cleanup(
);

if (status.getPhase() != CRPhase.DELETING) {
status.setPhase(CRPhase.DELETING)
status.updatePhase(CRPhase.DELETING)
.setMessage("Role deletion in progress");

context.getClient().resource(resource).patchStatus();
Expand Down Expand Up @@ -290,7 +290,7 @@ private UpdateControl<Role> reconcileInTransaction(
serverPassword
);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand All @@ -310,7 +310,7 @@ private UpdateControl<Role> reconcileInTransaction(
name
);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand Down Expand Up @@ -357,7 +357,7 @@ private UpdateControl<Role> reconcileInTransaction(
);
}

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public UpdateControl<Schema> reconcile(
);

if (clusterConnectionOptional.isEmpty()) {
status.setPhase(CRPhase.PENDING)
status.updatePhase(CRPhase.PENDING)
.setMessage("The specified ClusterConnection does not exist or is not ready yet [resource=%s/%s]".formatted(
getResourceNamespaceOrOwn(resource, clusterRef.getNamespace()),
clusterRef.getName()
Expand Down Expand Up @@ -113,7 +113,7 @@ public DeleteControl cleanup(
);

if (status.getPhase() != CRPhase.DELETING) {
status.setPhase(CRPhase.DELETING);
status.updatePhase(CRPhase.DELETING);

if (spec.getReclaimPolicy() == ReclaimPolicy.DELETE) {
status.setMessage("Schema deletion in progress");
Expand Down Expand Up @@ -205,7 +205,7 @@ private UpdateControl<Schema> reconcileInTransaction(
spec
);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand All @@ -222,7 +222,7 @@ private UpdateControl<Schema> reconcileInTransaction(
name
);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
Expand All @@ -236,7 +236,7 @@ private UpdateControl<Schema> reconcileInTransaction(

schemaService.changeSchemaOwner(tx, spec);

status.setPhase(CRPhase.READY)
status.updatePhase(CRPhase.READY)
.setMessage("Schema owner changed [previousOwner=%s, newOwner=%s]".formatted(
currentOwner,
expectedOwner
Expand Down
126 changes: 126 additions & 0 deletions operator/src/test/java/it/aboutbits/postgresql/core/CRStatusTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package it.aboutbits.postgresql.core;

import io.fabric8.kubernetes.client.utils.KubernetesSerialization;
import org.jspecify.annotations.NullMarked;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.time.OffsetDateTime;

import static org.assertj.core.api.Assertions.assertThat;

@NullMarked
class CRStatusTest {
@Nested
class UpdatePhase {
@Test
@DisplayName("A new status should start out pending without a transition time")
void newStatus_isPending() {
// when
var status = new CRStatus();

// then
assertThat(status.getPhase()).isEqualTo(CRPhase.PENDING);
assertThat(status.getLastPhaseTransitionTime()).isNull();
}

@Test
@DisplayName("Changing the phase should stamp the transition time")
void phaseChange_stampsTransitionTime() {
// given
var status = new CRStatus();

// when
status.updatePhase(CRPhase.READY);

// then
assertThat(status.getPhase()).isEqualTo(CRPhase.READY);
assertThat(status.getLastPhaseTransitionTime()).isNotNull();
}

@Test
@DisplayName("Changing the phase again should replace the previous transition time")
void secondPhaseChange_replacesTransitionTime() {
// given
var json = """
{"lastPhaseTransitionTime":"2026-01-01T00:00:00Z","phase":"READY"}
""";

var status = new KubernetesSerialization().unmarshal(
json,
CRStatus.class
);

// when
status.updatePhase(CRPhase.ERROR);

// then
assertThat(status.getPhase()).isEqualTo(CRPhase.ERROR);
assertThat(status.getLastPhaseTransitionTime())
.isNotNull()
.isAfter(OffsetDateTime.parse("2026-01-01T00:00:00Z"));
}

@Test
@DisplayName("Setting the same phase again should not touch the transition time")
void samePhase_keepsTransitionTime() {
// given
// A fixed time in the past, so the test fails if the time is overwritten with now.
var json = """
{"lastPhaseTransitionTime":"2026-01-01T00:00:00Z","phase":"READY"}
""";

var status = new KubernetesSerialization().unmarshal(
json,
CRStatus.class
);

// when
status.updatePhase(CRPhase.READY);

// then
assertThat(status.getLastPhaseTransitionTime())
.isNotNull()
.isAtSameInstantAs(OffsetDateTime.parse("2026-01-01T00:00:00Z"));
}

@Test
@DisplayName("Updating the phase should be chainable with the remaining status setters")
void updatePhase_isChainable() {
// when
var status = new CRStatus()
.updatePhase(CRPhase.ERROR)
.setMessage("Something went wrong");

// then
assertThat(status.getPhase()).isEqualTo(CRPhase.ERROR);
assertThat(status.getMessage()).isEqualTo("Something went wrong");
}
}

@Nested
class Deserialize {
@Test
@DisplayName("Reading a status from the cluster should keep its transition time")
void statusFromCluster_keepsTransitionTime() {
// given
// The API server sends the status keys in alphabetical order.
var json = """
{"lastPhaseTransitionTime":"2026-01-01T00:00:00Z","phase":"READY"}
""";

// when
var status = new KubernetesSerialization().unmarshal(
json,
CRStatus.class
);

// then
assertThat(status.getPhase()).isEqualTo(CRPhase.READY);
assertThat(status.getLastPhaseTransitionTime())
.isNotNull()
.isAtSameInstantAs(OffsetDateTime.parse("2026-01-01T00:00:00Z"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ private static void assertThatClusterConnectionHasExpectedStatus(
private static CRStatus getInitialClusterConnectionStatus(ClusterConnection clusterConnection) {
return new CRStatus()
.setName(clusterConnection.getName())
.setPhase(CRPhase.READY)
.updatePhase(CRPhase.READY)
.setObservedGeneration(1L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ void createDatabase_andStatusReady() {
// then
var expectedStatus = new CRStatus()
.setName(dbName)
.setPhase(CRPhase.READY)
.updatePhase(CRPhase.READY)
.setObservedGeneration(1L);

assertThatDatabaseHasExpectedStatus(
Expand Down
Loading