Skip to content

Commit 49e8385

Browse files
committed
feat(sidecars): enable DB-Backup and Meilisearch features with required connection settings
1 parent a576953 commit 49e8385

7 files changed

Lines changed: 104 additions & 34 deletions

File tree

README.md

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ come from Maven Central):
4141

4242
## Quick Start
4343

44-
The simplest way to enable every feature is to import `FullSpringServiceConfig`:
44+
The simplest way to wire the platform is to import `FullSpringServiceConfig` (this wires every
45+
feature except the opt-in sidecar features — Search and DB-Backup — which you enable explicitly, see
46+
below):
4547

4648
```java
4749
@SpringBootApplication
@@ -58,6 +60,30 @@ for example `SecurityConfig`, `TenantConfig`, `TagConfig`, `WebhookConfig`, `Set
5860
`ApiKeyConfig`. Multi-tenancy can also be enabled declaratively via the `@EnableTenant`
5961
meta-annotation.
6062

63+
### Optional sidecar features (opt-in)
64+
65+
The two features that talk to an external sidecar — **Search** (Meilisearch) and **DB-Backup** — are
66+
**disabled by default**, even when you import `FullSpringServiceConfig`. Importing the config does not
67+
activate them; you enable each explicitly. When a feature is enabled, its host is **required** and a
68+
missing/blank host **fails startup** (no silent `localhost` fallback):
69+
70+
```properties
71+
# Search (Meilisearch) — off by default
72+
openelements.meilisearch.enabled=true
73+
openelements.meilisearch.host=https://meilisearch.internal:7700 # required when enabled
74+
openelements.meilisearch.master-key=${MEILI_MASTER_KEY}
75+
openelements.meilisearch.index-prefix= # optional, default ""
76+
77+
# DB-Backup sidecar — off by default
78+
openelements.db-backup.enabled=true
79+
openelements.db-backup.base-url=https://db-backup.internal:8081 # required when enabled
80+
openelements.db-backup.api-token=${DB_BACKUP_API_TOKEN} # required for authenticated calls
81+
```
82+
83+
If a feature stays disabled, none of its beans are created and no connection settings are needed.
84+
Secrets (`master-key`, `api-token`) must come from environment variables or secret management, never
85+
from committed configuration.
86+
6187
## Features
6288

6389
- **JWT / OAuth2 Resource Server** — Standard Spring Security JWT validation with `roles`-claim
@@ -116,14 +142,19 @@ meta-annotation.
116142
- **Lifecycle Events**`OnObjectCreate`, `OnObjectUpdate` and `OnObjectDelete` are published
117143
synchronously inside the originating transaction; consumers opt into post-commit behaviour
118144
with `@TransactionalEventListener`.
119-
- **Search** — Meilisearch-backed full-text search via a thin `RestClient` wrapper
120-
(`MeilisearchClient`). At startup the lib exchanges the master key for a scoped runtime key,
121-
applies declarative per-index settings, and runs a full reindex: the application contributes one
122-
`SearchIndexBootstrapStep` bean per index (streaming already-mapped documents) and an optional
123-
`IndexSettings` / `ScopedKeySpec` bean. The lib never sees domain types. `Highlighter` turns
124-
Meilisearch's `_formatted` output into HTML-safe highlighted fragments. An unreachable sidecar is
125-
skipped with a warning, so the feature is inert until Meilisearch is available. Connection settings
126-
bind from `openelements.meilisearch.*`.
145+
- **Search** *(opt-in, off by default)* — Meilisearch-backed full-text search via a thin `RestClient`
146+
wrapper (`MeilisearchClient`). Enabled with `openelements.meilisearch.enabled=true`; `host` is then
147+
required. At startup the lib exchanges the master key for a scoped runtime key, applies declarative
148+
per-index settings, and runs a full reindex: the application contributes one `SearchIndexBootstrapStep`
149+
bean per index (streaming already-mapped documents) and an optional `IndexSettings` / `ScopedKeySpec`
150+
bean. The lib never sees domain types. `Highlighter` turns Meilisearch's `_formatted` output into
151+
HTML-safe highlighted fragments. Once enabled, an unreachable sidecar is skipped with a warning, so
152+
the feature is inert until Meilisearch is available. Connection settings bind from
153+
`openelements.meilisearch.*` (see *Optional sidecar features*).
154+
- **DB-Backup client** *(opt-in, off by default)* — thin `RestClient` wrapper (`DbBackupClient`) for the
155+
db-backup-service sidecar: trigger backups, poll job status, list and stream backup artefacts.
156+
Enabled with `openelements.db-backup.enabled=true`; `base-url` is then required. Connection settings
157+
bind from `openelements.db-backup.*` (see *Optional sidecar features*).
127158

128159
For per-package overviews, see the `package-info.java` files under
129160
`src/main/java/com/openelements/spring/base/`.

src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupConfig.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.openelements.spring.base.services.dbbackup;
22

3+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
34
import org.springframework.boot.context.properties.EnableConfigurationProperties;
45
import org.springframework.context.annotation.ComponentScan;
56
import org.springframework.context.annotation.Configuration;
@@ -16,10 +17,14 @@
1617
* {@code @ComponentScan} reliably picks up plain configurations but excludes auto-configurations,
1718
* so the properties are always registered alongside the client.
1819
*
19-
* <p>The feature is inert until the consuming application configures
20-
* {@code openelements.db-backup.base-url} and {@code openelements.db-backup.api-token}.
20+
* <p>The feature is <strong>opt-in and disabled by default</strong>: none of these beans are created
21+
* unless {@code openelements.db-backup.enabled=true}. This avoids forcing a db-backup configuration
22+
* on consumers that do not use the sidecar. When enabled, {@code openelements.db-backup.base-url} is
23+
* required (see {@link DbBackupProperties}) — a missing base URL fails startup rather than defaulting
24+
* to {@code localhost}.
2125
*/
2226
@Configuration(proxyBeanMethods = false)
27+
@ConditionalOnProperty(prefix = "openelements.db-backup", name = "enabled", havingValue = "true")
2328
@ComponentScan(basePackageClasses = DbBackupConfig.class)
2429
@EnableConfigurationProperties(DbBackupProperties.class)
2530
public class DbBackupConfig {}

src/main/java/com/openelements/spring/base/services/dbbackup/DbBackupProperties.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,40 @@
11
package com.openelements.spring.base.services.dbbackup;
22

3+
import jakarta.validation.constraints.NotBlank;
34
import java.time.Duration;
45
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.validation.annotation.Validated;
57

68
/**
79
* Connection settings for the <a
810
* href="https://github.com/OpenElementsLabs/db-backup-service">db-backup-service</a> sidecar. Bound
911
* from {@code openelements.db-backup.*} application properties.
1012
*
13+
* <p>The db-backup feature is <strong>opt-in</strong>: it is disabled by default and only activates
14+
* when {@code openelements.db-backup.enabled=true} (see {@link DbBackupConfig}). When the feature is
15+
* enabled, {@code baseUrl} is <strong>required</strong> — there is no default. A blank or missing
16+
* {@code baseUrl} fails context startup with a binding/validation error rather than silently
17+
* pointing at {@code localhost}, so a misconfiguration surfaces immediately instead of at first use.
18+
*
1119
* <p>The sidecar exposes two unauthenticated probes ({@code /health}, {@code /info}) and a set of
1220
* authenticated backup endpoints under {@code /api/v1/backups/*} that require a Bearer token
1321
* matching the sidecar's {@code API_TOKEN}. The token must be provided through environment
1422
* variables or secret management — never committed to source control.
1523
*
16-
* @param baseUrl the sidecar root URL (scheme + host + port, no trailing path). Defaults to
17-
* {@code http://localhost:8081}.
24+
* @param baseUrl the sidecar root URL (scheme + host + port, no trailing path). Required when the
25+
* feature is enabled; no default.
1826
* @param apiToken the bearer token the sidecar accepts on its authenticated endpoints. May be
1927
* {@code null} if only the unauthenticated probes are used; the client throws at the point of
2028
* use when an authenticated call is made without a token.
2129
* @param requestTimeout per-request socket / read timeout. Defaults to 30 seconds because the
2230
* download endpoints stream gzip blobs that can take a while.
2331
*/
32+
@Validated
2433
@ConfigurationProperties("openelements.db-backup")
25-
public record DbBackupProperties(String baseUrl, String apiToken, Duration requestTimeout) {
34+
public record DbBackupProperties(
35+
@NotBlank String baseUrl, String apiToken, Duration requestTimeout) {
2636

2737
public DbBackupProperties {
28-
if (baseUrl == null || baseUrl.isBlank()) {
29-
baseUrl = "http://localhost:8081";
30-
}
3138
if (requestTimeout == null) {
3239
requestTimeout = Duration.ofSeconds(30);
3340
}

src/main/java/com/openelements/spring/base/services/search/MeilisearchProperties.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
11
package com.openelements.spring.base.services.search;
22

3+
import jakarta.validation.constraints.NotBlank;
34
import java.time.Duration;
45
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.validation.annotation.Validated;
57

68
/**
79
* Connection settings for the Meilisearch sidecar. Bound from {@code openelements.meilisearch.*}
810
* application properties.
911
*
12+
* <p>The search feature is <strong>opt-in</strong>: it is disabled by default and only activates
13+
* when {@code openelements.meilisearch.enabled=true} (see {@link SearchConfig}). When the feature is
14+
* enabled, {@code host} is <strong>required</strong> — there is no default. A blank or missing host
15+
* fails context startup with a binding/validation error rather than silently pointing at
16+
* {@code localhost}, so a misconfiguration surfaces immediately instead of at first use.
17+
*
1018
* <p>This type is app-agnostic: it carries only the connection data and the index-prefix resolution
1119
* helper. By default no prefix is applied; an application that hosts several logical datasets in
1220
* one Meilisearch instance can set {@code openelements.meilisearch.index-prefix} to namespace its
1321
* indexes. The concrete index names live in the application, not here.
1422
*/
23+
@Validated
1524
@ConfigurationProperties("openelements.meilisearch")
1625
public record MeilisearchProperties(
17-
String host, String masterKey, String indexPrefix, Duration requestTimeout) {
26+
@NotBlank String host, String masterKey, String indexPrefix, Duration requestTimeout) {
1827

1928
public MeilisearchProperties {
20-
if (host == null || host.isBlank()) {
21-
host = "http://localhost:7700";
22-
}
2329
if (indexPrefix == null) {
2430
indexPrefix = "";
2531
}

src/main/java/com/openelements/spring/base/services/search/SearchConfig.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.openelements.spring.base.services.search;
22

3+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
34
import org.springframework.boot.context.properties.EnableConfigurationProperties;
45
import org.springframework.context.annotation.ComponentScan;
56
import org.springframework.context.annotation.Configuration;
@@ -20,13 +21,20 @@
2021
* (which excludes {@code @AutoConfiguration} classes), so the properties are always registered
2122
* alongside the components.
2223
*
23-
* <p>The feature is inert unless a Meilisearch sidecar is reachable: when it is not, the startup
24-
* runners log a warning and flip readiness to ready without indexing. The consuming application
25-
* supplies what the lib indexes — an optional {@link ScopedKeySpec} bean, one {@link IndexSettings}
26-
* bean per index, and one {@link SearchIndexBootstrapStep} bean per index — plus, for asynchronous
27-
* bootstrap, {@code @EnableAsync} and a {@code searchIndexExecutor} bean.
24+
* <p>The feature is <strong>opt-in and disabled by default</strong>: none of these beans are
25+
* created unless {@code openelements.meilisearch.enabled=true}. This avoids forcing a Meilisearch
26+
* configuration on consumers that do not use search. When enabled, {@code
27+
* openelements.meilisearch.host} is required (see {@link MeilisearchProperties}) — a missing host
28+
* fails startup rather than defaulting to {@code localhost}.
29+
*
30+
* <p>Once enabled, the feature is still inert unless a Meilisearch sidecar is reachable: when it is
31+
* not, the startup runners log a warning and flip readiness to ready without indexing. The consuming
32+
* application supplies what the lib indexes — an optional {@link ScopedKeySpec} bean, one {@link
33+
* IndexSettings} bean per index, and one {@link SearchIndexBootstrapStep} bean per index — plus, for
34+
* asynchronous bootstrap, {@code @EnableAsync} and a {@code searchIndexExecutor} bean.
2835
*/
2936
@Configuration(proxyBeanMethods = false)
37+
@ConditionalOnProperty(prefix = "openelements.meilisearch", name = "enabled", havingValue = "true")
3038
@ComponentScan(basePackageClasses = SearchConfig.class)
3139
@EnableConfigurationProperties(MeilisearchProperties.class)
3240
public class SearchConfig {}

src/test/java/com/openelements/spring/base/services/search/MeilisearchPropertiesBindingTest.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.openelements.spring.base.services.search;
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNull;
45

56
import java.util.Map;
67
import org.junit.jupiter.api.DisplayName;
@@ -16,9 +17,10 @@
1617
*
1718
* <p>The {@code @ConfigurationProperties} prefix contract: the {@code openelements.meilisearch.*}
1819
* prefix populates the record fields, while the pre-rename {@code
19-
* openelements.search.meilisearch.*} prefix is silently ignored and the record's compact-
20-
* constructor defaults take over. The legacy-prefix case acts as a regression guard — if a future
21-
* change reintroduces the longer prefix it must do so deliberately, not by accident.
20+
* openelements.search.meilisearch.*} prefix is silently ignored — leaving {@code host} unset
21+
* ({@code null}), since the record no longer defaults it. The legacy-prefix case acts as a
22+
* regression guard — if a future change reintroduces the longer prefix it must do so deliberately,
23+
* not by accident.
2224
*
2325
* <h2>How it is tested</h2>
2426
*
@@ -46,16 +48,15 @@ void renamedPrefixIsHonored() {
4648

4749
/**
4850
* Regression guard: the legacy {@code openelements.search.meilisearch.*} prefix must not bind.
49-
* Verified indirectly by asserting the record's compact-constructor default ({@code
50-
* http://localhost:7700}) is returned even though the input map carries a value under the
51-
* legacy prefix.
51+
* Verified by asserting {@code host} stays {@code null} — the legacy-prefix value is ignored and
52+
* the record no longer supplies a default host.
5253
*/
5354
@Test
5455
@DisplayName(
55-
"The legacy openelements.search.meilisearch.* prefix is ignored — the compact-constructor default wins.")
56+
"The legacy openelements.search.meilisearch.* prefix is ignored — host stays unset (null).")
5657
void legacyPrefixIsNoLongerHonored() {
5758
final MeilisearchProperties props =
5859
bind(Map.of("openelements.search.meilisearch.host", "http://example:7700"));
59-
assertEquals("http://localhost:7700", props.host());
60+
assertNull(props.host());
6061
}
6162
}

src/test/resources/application-testcontainers.properties

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,18 @@ spring.main.allow-bean-definition-overriding=true
1010
# Disable OAuth2 for tests - we handle auth manually
1111
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration
1212

13+
# TestApplication uses a broad scanBasePackages over com.openelements.spring.base, so the library's
14+
# @Component clients (DbBackupClient, MeilisearchClient) are picked up directly — independent of the
15+
# now-conditional feature configs. Provide the required connection settings so their *Properties
16+
# beans bind. The sidecars are not reachable in tests; the clients/runners degrade gracefully.
17+
# both features are opt-in (off by default); enable them so the broad-scanned clients get their
18+
# *Properties beans, and provide the now-required hosts:
19+
openelements.db-backup.enabled=true
20+
openelements.db-backup.base-url=http://localhost:8081
21+
# search is opt-in (off by default) — enable it for the full-scan test context and give it a host:
22+
openelements.meilisearch.enabled=true
23+
openelements.meilisearch.host=http://localhost:7700
24+
1325
# Concurrency tests use Propagation.REQUIRES_NEW for user provisioning, which holds two
1426
# connections per thread (suspended outer + inner). Default HikariCP pool of 10 deadlocks
1527
# under 10-thread concurrency tests — give tests enough headroom.

0 commit comments

Comments
 (0)