Skip to content

Commit 7ecd131

Browse files
committed
chore: configure actuator warmup
1 parent a2a8405 commit 7ecd131

9 files changed

Lines changed: 239 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package kz.ncanode.configuration;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import jakarta.servlet.http.HttpServletResponse;
5+
import kz.ncanode.exception.WarmupException;
6+
import kz.ncanode.service.WarmupService;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.boot.actuate.health.Health;
9+
import org.springframework.boot.actuate.health.HealthIndicator;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.web.servlet.HandlerInterceptor;
13+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
14+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
15+
16+
/**
17+
* Пока не скачаны сертификаты УЦ / не обновлены CRL:
18+
* <ul>
19+
* <li>{@code /actuator/health} показывает компоненты {@code ca} / {@code crl} со статусом DOWN
20+
* (а значит и общий статус — DOWN);</li>
21+
* <li>эндпоинты {@code /*}{@code /verify} отвечают 503.</li>
22+
* </ul>
23+
*/
24+
@Configuration
25+
@RequiredArgsConstructor
26+
public class WarmupConfiguration implements WebMvcConfigurer {
27+
private final WarmupService warmupService;
28+
29+
@Bean
30+
public HealthIndicator caHealthIndicator() {
31+
return () -> warmupService.isCaReady()
32+
? Health.up().build()
33+
: Health.down().withDetail("reason", "CA certificates are not downloaded yet").build();
34+
}
35+
36+
@Bean
37+
public HealthIndicator crlHealthIndicator() {
38+
return () -> warmupService.isCrlReady()
39+
? Health.up().build()
40+
: Health.down().withDetail("reason", "CRL cache is not updated yet").build();
41+
}
42+
43+
@Override
44+
public void addInterceptors(InterceptorRegistry registry) {
45+
registry.addInterceptor(new HandlerInterceptor() {
46+
@Override
47+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
48+
if (!warmupService.isReady()) {
49+
throw new WarmupException();
50+
}
51+
return true;
52+
}
53+
}).addPathPatterns("/*/verify");
54+
}
55+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package kz.ncanode.exception;
2+
3+
import org.springframework.http.HttpStatus;
4+
5+
/**
6+
* Сервер ещё не прогрелся: не скачаны сертификаты УЦ / не обновлены CRL.
7+
*/
8+
public class WarmupException extends ApplicationException {
9+
public WarmupException() {
10+
super("Service is warming up: CA certificate and CRL caches are not ready yet. Try again shortly.");
11+
}
12+
13+
@Override
14+
public Integer getStatus() {
15+
return HttpStatus.SERVICE_UNAVAILABLE.value();
16+
}
17+
}

src/main/java/kz/ncanode/service/CaService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,21 @@ public List<CertificateWrapper> buildChain(CertificateWrapper leaf) {
157157
return chain;
158158
}
159159

160+
/**
161+
* Прогрет ли кэш УЦ: сертификаты скачаны и читаются (или фича выключена).
162+
*/
163+
public boolean isCacheReady() {
164+
if (!caConfiguration.isEnabled()) {
165+
return true;
166+
}
167+
168+
try {
169+
return !getRootCertificates().isEmpty();
170+
} catch (RuntimeException e) {
171+
return false;
172+
}
173+
}
174+
160175
public List<CertificateWrapper> getRootCertificates() {
161176
synchronized (directoryService) {
162177
synchronized (certificates) {

src/main/java/kz/ncanode/service/CrlService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,21 @@ private void initializeDeltaScheduler() {
7777
taskScheduler.schedule(() -> updateCache(false, crlConfiguration.getDelta(), CRL_CACHE_DELTA_DIR_NAME), periodicTrigger);
7878
}
7979

80+
/**
81+
* Прогрет ли кэш CRL: хотя бы один full-CRL скачан (или фича/расписание выключены).
82+
*/
83+
public boolean isCacheReady() {
84+
if (!crlConfiguration.isEnabled() || crlConfiguration.getTtl() == null || crlConfiguration.getTtl() < 1) {
85+
return true;
86+
}
87+
88+
try {
89+
return !getCrlFiles(CRL_CACHE_FULL_DIR_NAME).isEmpty();
90+
} catch (RuntimeException e) {
91+
return false;
92+
}
93+
}
94+
8095
/**
8196
* Проверка сертификата в CRL
8297
*
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package kz.ncanode.service;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.stereotype.Service;
5+
6+
import java.util.List;
7+
8+
/**
9+
* Признак «прогрева» сервера: скачаны сертификаты УЦ и обновлены CRL.
10+
* Пока не прогрет — {@code /actuator/health} отдаёт DOWN, а эндпоинты verify — 503.
11+
*/
12+
@Service
13+
@RequiredArgsConstructor
14+
public class WarmupService {
15+
private final CaService caService;
16+
private final List<CrlService> crlServices;
17+
18+
public boolean isCaReady() {
19+
return caService.isCacheReady();
20+
}
21+
22+
public boolean isCrlReady() {
23+
return crlServices.stream().allMatch(CrlService::isCacheReady);
24+
}
25+
26+
public boolean isReady() {
27+
return isCaReady() && isCrlReady();
28+
}
29+
}

src/main/resources/application.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ ncanode:
3737
tsp:
3838
url: ${NCANODE_TSP_URL:http://tsp.pki.gov.kz/}
3939
retries: ${NCANODE_TSP_RETRIES:3}
40+
management:
41+
endpoint:
42+
health:
43+
show-components: always
44+
show-details: always
4045
springdoc:
4146
show-actuator: true
4247
swagger-ui:

src/test/groovy/kz/ncanode/unit/service/CaServiceExtraTest.groovy

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,40 @@ class CaServiceExtraTest extends Specification implements WithTestData {
148148
caService.getRootCertificateFor(root).isEmpty()
149149
}
150150

151+
def "isCacheReady is true when the CA feature is disabled"() {
152+
given:
153+
def service = standalone()
154+
caConfiguration.isEnabled() >> false
155+
156+
expect:
157+
service.isCacheReady()
158+
}
159+
160+
def "isCacheReady is false when root certificates cannot be loaded"() {
161+
given:
162+
def service = standalone()
163+
caConfiguration.isEnabled() >> true
164+
directoryService.getCachePathFor('ca') >> Optional.empty()
165+
166+
expect:
167+
!service.isCacheReady()
168+
}
169+
170+
def "isCacheReady is true once a CA certificate is cached"() {
171+
given:
172+
def service = standalone()
173+
def dir = File.createTempDir()
174+
new File(dir, 'root.cer').bytes = rootBytes()
175+
caConfiguration.isEnabled() >> true
176+
directoryService.getCachePathFor('ca') >> Optional.of(dir)
177+
178+
expect:
179+
service.isCacheReady()
180+
181+
cleanup:
182+
dir.deleteDir()
183+
}
184+
151185
def "updateCache(false) is a no-op when the CA feature is disabled"() {
152186
given:
153187
def service = standalone()

src/test/groovy/kz/ncanode/unit/service/CrlServiceExtraTest.groovy

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,30 @@ class CrlServiceExtraTest extends Specification implements WithTestData {
5252
ResourceUtils.getFile("classpath:crl/${name}").bytes
5353
}
5454

55+
def "isCacheReady is true when CRL scheduling is disabled"() {
56+
given:
57+
crlConfiguration.isEnabled() >> false
58+
59+
expect:
60+
service.isCacheReady()
61+
}
62+
63+
def "isCacheReady reflects presence of a full CRL file"() {
64+
given:
65+
crlConfiguration.isEnabled() >> true
66+
crlConfiguration.getTtl() >> 10
67+
directoryService.getCachePathFor('crl/full') >> Optional.of(cacheDir)
68+
69+
expect:
70+
!service.isCacheReady()
71+
72+
when:
73+
new File(cacheDir, 'x.crl').text = 'x'
74+
75+
then:
76+
service.isCacheReady()
77+
}
78+
5579
def "verify returns ACTIVE immediately when CRL checking is disabled"() {
5680
given:
5781
crlConfiguration.isEnabled() >> false
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package kz.ncanode.unit.service
2+
3+
import kz.ncanode.service.CaService
4+
import kz.ncanode.service.CrlService
5+
import kz.ncanode.service.WarmupService
6+
import spock.lang.Specification
7+
8+
class WarmupServiceTest extends Specification {
9+
10+
CaService caService = Mock()
11+
CrlService crlA = Mock()
12+
CrlService crlB = Mock()
13+
14+
WarmupService service() {
15+
new WarmupService(caService, [crlA, crlB])
16+
}
17+
18+
def "isReady requires CA and every CRL service to be ready"() {
19+
given:
20+
caService.isCacheReady() >> ca
21+
crlA.isCacheReady() >> a
22+
crlB.isCacheReady() >> b
23+
24+
expect:
25+
service().isReady() == expected
26+
27+
where:
28+
ca | a | b || expected
29+
true | true | true || true
30+
false | true | true || false
31+
true | false | true || false
32+
true | true | false || false
33+
}
34+
35+
def "exposes per-module readiness"() {
36+
given:
37+
caService.isCacheReady() >> true
38+
crlA.isCacheReady() >> true
39+
crlB.isCacheReady() >> false
40+
41+
expect:
42+
service().isCaReady()
43+
!service().isCrlReady()
44+
}
45+
}

0 commit comments

Comments
 (0)