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 @@ -20,7 +20,7 @@ public abstract class BaseReconciler<CR extends CustomResource<?, S> & Named, S
protected abstract S newStatus();

public S initializeStatus(CR resource) {
S status = resource.getStatus();
var status = resource.getStatus();

//noinspection ConstantConditions
if (status == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ private static QueryPart concatenateQueryParts(
List<? extends QueryPart> items,
String separator
) {
int size = items.size();
var size = items.size();

if (items.isEmpty()) {
return sql("");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ private UpdateControl<Database> reconcileWithoutTransaction(
name
);

return UpdateControl.noUpdate();
status.setPhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
}

log.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,10 @@ private UpdateControl<Role> reconcileInTransaction(
name
);

return UpdateControl.noUpdate();
status.setPhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
}

var changePassword = loginExpected && !passwordMatches;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ private UpdateControl<Schema> reconcileInTransaction(
name
);

return UpdateControl.noUpdate();
status.setPhase(CRPhase.READY)
.setMessage(null);

return UpdateControl.patchStatus(resource);
}

log.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ void whenSecretExists_shouldReuseKey() {
}

@Test
@DisplayName("When the key Secret is lost and the operator restarts, should create a new key and the fingerprints change")
@DisplayName(
"When the key Secret is lost and the operator restarts, should create a new key and the fingerprints change"
)
void whenSecretLost_shouldCreateNewKey() {
// given
var fingerprint = newService().fingerprint("password", PasswordEncryption.SCRAM_SHA_256);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -175,6 +176,59 @@ void createsCustomResourceWithFileRef_andReconcilerStatusIsReady() throws IOExce
}
}

@Test
@DisplayName("When the server of a ClusterConnection is reachable again after an ERROR, the status should be ready")
void reachableAgainAfterError_setsReady() {
// given: a reconciled ClusterConnection
var clusterConnection = given.one()
.clusterConnection()
.withName("test-connection-error-to-ready")
.returnFirst();

var spec = clusterConnection.getSpec();
var port = spec.getPort();

// and: the server is not reachable for a while
spec.setPort(1);

var erroredClusterConnection = applyClusterConnection(clusterConnection);

assertThat(erroredClusterConnection.getStatus().getPhase()).isEqualTo(CRPhase.ERROR);

// when: the server is reachable again
spec.setPort(port);

var updatedClusterConnection = applyClusterConnection(clusterConnection);

// then
assertThat(updatedClusterConnection.getStatus().getPhase()).isEqualTo(CRPhase.READY);
assertThat(updatedClusterConnection.getStatus().getMessage()).startsWith("PostgreSQL");
}

private ClusterConnection applyClusterConnection(ClusterConnection clusterConnection) {
var namespace = kubernetesClient.getNamespace();

clusterConnection.getMetadata().setManagedFields(null);
clusterConnection.getMetadata().setResourceVersion(null);

var applied = kubernetesClient.resources(ClusterConnection.class)
.inNamespace(namespace)
.resource(clusterConnection)
.serverSideApply();

var generation = applied.getMetadata().getGeneration();

//noinspection ConstantConditions
return kubernetesClient.resources(ClusterConnection.class)
.inNamespace(namespace)
.withName(applied.getMetadata().getName())
.waitUntilCondition(
c -> c.getStatus() != null && c.getStatus().getObservedGeneration() >= generation,
5,
TimeUnit.SECONDS
);
}

private static void assertThatClusterConnectionHasExpectedStatus(
ClusterConnection clusterConnection,
CRStatus expectedStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;

import static it.aboutbits.postgresql.core.ReclaimPolicy.DELETE;
import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -72,6 +73,71 @@ void createDatabase_andStatusReady() {
assertThat(databaseService.databaseExists(dsl, database.getSpec())).isTrue();
}

@Test
@DisplayName(
"When the ClusterConnection of an up-to-date Database is back, the Database should be reconciled to READY"
)
void upToDateDatabase_afterPendingClusterConnection_setsReady() {
// given: a reconciled Database with an explicit owner, so that the next reconciles find it up to date
var clusterConnection = given.one()
.clusterConnection()
.withName("test-connection-database-pending-to-ready")
.returnFirst();

var database = given.one()
.database()
.withClusterConnectionName(clusterConnection.getMetadata().getName())
.withReclaimPolicy(DELETE)
.returnFirst();

var spec = database.getSpec();
var clusterRef = spec.getClusterRef();

var dsl = postgreSQLContextFactory.getDSLContext(clusterConnection);

spec.setOwner(databaseService.fetchDatabaseOwner(dsl, spec));

// and: the ClusterConnection is not available for a while
clusterRef.setName("non-existing-cc");

var pendingDatabase = applyDatabase(database);

assertThat(pendingDatabase.getStatus().getPhase()).isEqualTo(CRPhase.PENDING);

// when: the ClusterConnection is available again
clusterRef.setName(clusterConnection.getMetadata().getName());

var updatedDatabase = applyDatabase(database);

// then
assertThat(updatedDatabase.getStatus().getPhase()).isEqualTo(CRPhase.READY);
assertThat(updatedDatabase.getStatus().getMessage()).isNull();
}

private Database applyDatabase(Database database) {
var namespace = kubernetesClient.getNamespace();

database.getMetadata().setManagedFields(null);
database.getMetadata().setResourceVersion(null);

var applied = kubernetesClient.resources(Database.class)
.inNamespace(namespace)
.resource(database)
.serverSideApply();

var generation = applied.getMetadata().getGeneration();

//noinspection ConstantConditions
return kubernetesClient.resources(Database.class)
.inNamespace(namespace)
.withName(applied.getMetadata().getName())
.waitUntilCondition(
d -> d.getStatus() != null && d.getStatus().getObservedGeneration() >= generation,
5,
TimeUnit.SECONDS
);
}

private static void assertThatDatabaseHasExpectedStatus(
Database database,
CRStatus expectedStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,67 @@ void defaultPrivilegeOnSequence() {
}
}

@Test
@DisplayName(
"When the ClusterConnection of an up-to-date DefaultPrivilege is back, the DefaultPrivilege should be reconciled to READY"
)
void upToDateDefaultPrivilege_afterPendingClusterConnection_setsReady() {
// given: a reconciled DefaultPrivilege
var clusterConnectionMain = given.one()
.clusterConnection()
.returnFirst();

var database = given.one()
.database()
.withClusterConnectionName(clusterConnectionMain.getMetadata().getName())
.withReclaimPolicy(DELETE)
.returnFirst();

// To create a Schema in the new database, we need a ClusterConnection pointing to it
var clusterConnectionDb = given.one()
.clusterConnection()
.withDatabase(database.getSpec().getName())
.returnFirst();

var role = given.one()
.role()
.withClusterConnectionName(clusterConnectionMain.getMetadata().getName())
.returnFirst();

var defaultPrivilege = given.one()
.defaultPrivilege()
.withClusterConnectionName(clusterConnectionDb.getMetadata().getName())
.withDatabase(database.getSpec().getName())
.withRole(role.getSpec().getName())
.withObjectType(SCHEMA)
.withPrivileges(USAGE)
.returnFirst();

var clusterRef = defaultPrivilege.getSpec().getClusterRef();

// and: the ClusterConnection is not available for a while
clusterRef.setName("non-existing-cc");

var pendingDefaultPrivilege = applyDefaultPrivilege(
defaultPrivilege,
dp -> dp.getStatus().getObservedGeneration() >= 2L
);

assertThat(pendingDefaultPrivilege.getStatus().getPhase()).isEqualTo(CRPhase.PENDING);

// when: the ClusterConnection is available again
clusterRef.setName(clusterConnectionDb.getMetadata().getName());

var updatedDefaultPrivilege = applyDefaultPrivilege(
defaultPrivilege,
dp -> dp.getStatus().getObservedGeneration() >= 3L
);

// then
assertThat(updatedDefaultPrivilege.getStatus().getPhase()).isEqualTo(CRPhase.READY);
assertThat(updatedDefaultPrivilege.getStatus().getMessage()).isNull();
}

private DefaultPrivilege applyDefaultPrivilege(
DefaultPrivilege defaultPrivilege,
Predicate<DefaultPrivilege> condition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,59 @@ void grantOnAllSequences() {
}
}

@Test
@DisplayName("When the ClusterConnection of an up-to-date Grant is back, the Grant should be reconciled to READY")
void upToDateGrant_afterPendingClusterConnection_setsReady() {
// given: a reconciled Grant
var clusterConnection = given.one()
.clusterConnection()
.returnFirst();

var role = given.one()
.role()
.withClusterConnectionName(clusterConnection.getMetadata().getName())
.returnFirst();

var database = given.one()
.database()
.withClusterConnectionName(clusterConnection.getMetadata().getName())
.withReclaimPolicy(DELETE)
.returnFirst();

var grant = given.one()
.grant()
.withClusterConnectionName(clusterConnection.getMetadata().getName())
.withDatabase(database.getSpec().getName())
.withRole(role.getSpec().getName())
.withObjectType(DATABASE)
.withPrivileges(CONNECT)
.returnFirst();

var clusterRef = grant.getSpec().getClusterRef();

// and: the ClusterConnection is not available for a while
clusterRef.setName("non-existing-cc");

var pendingGrant = applyGrant(
grant,
g -> g.getStatus().getObservedGeneration() >= 2L
);

assertThat(pendingGrant.getStatus().getPhase()).isEqualTo(CRPhase.PENDING);

// when: the ClusterConnection is available again
clusterRef.setName(clusterConnection.getMetadata().getName());

var updatedGrant = applyGrant(
grant,
g -> g.getStatus().getObservedGeneration() >= 3L
);

// then
assertThat(updatedGrant.getStatus().getPhase()).isEqualTo(CRPhase.READY);
assertThat(updatedGrant.getStatus().getMessage()).isNull();
}

private Grant applyGrant(
Grant grant,
Predicate<Grant> condition
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,9 @@ void nonSuperuserAdmin_updatesLoginAndMembership() {
}

@Test
@DisplayName("When the admin is not a superuser, its implicit membership in a created Role should be ignored and kept")
@DisplayName(
"When the admin is not a superuser, its implicit membership in a created Role should be ignored and kept"
)
void nonSuperuserAdmin_implicitAdminMembership_isIgnoredAndKept() {
// given
var rootConnection = givenRootClusterConnection("test-connection-root-self-grant");
Expand Down Expand Up @@ -262,7 +264,9 @@ void nonSuperuserAdmin_implicitAdminMembership_isIgnoredAndKept() {

@ParameterizedTest(name = "flag {0}")
@MethodSource("provideSuperuserOnlyFlags")
@DisplayName("When the admin is not a superuser and the Role asks for a superuser-only flag, the status should be ERROR")
@DisplayName(
"When the admin is not a superuser and the Role asks for a superuser-only flag, the status should be ERROR"
)
void nonSuperuserAdmin_superuserOnlyFlag_setsError(
String flagName,
BiConsumer<RoleSpec.Flags, Boolean> flagSetter
Expand Down Expand Up @@ -301,7 +305,9 @@ void nonSuperuserAdmin_superuserOnlyFlag_setsError(
}

@Test
@DisplayName("When the admin is not a superuser and passwordEncryption is 'server', the server should hash the password")
@DisplayName(
"When the admin is not a superuser and passwordEncryption is 'server', the server should hash the password"
)
void nonSuperuserAdmin_serverPasswordEncryption_letsTheServerHash() {
// given
var rootConnection = givenRootClusterConnection("test-connection-root-server-encryption");
Expand Down Expand Up @@ -335,7 +341,9 @@ void nonSuperuserAdmin_serverPasswordEncryption_letsTheServerHash() {
}

@Test
@DisplayName("When the admin is not a superuser and the Secret contains a SCRAM-SHA-256 verifier, it should be stored verbatim")
@DisplayName(
"When the admin is not a superuser and the Secret contains a SCRAM-SHA-256 verifier, it should be stored verbatim"
)
void nonSuperuserAdmin_preHashedPassword_isStoredVerbatim() {
// given
var rootConnection = givenRootClusterConnection("test-connection-root-prehashed");
Expand Down
Loading