This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 00e094c9 fix: refresh K8s certificate expiry status (#765)
00e094c9 is described below
commit 00e094c939062b3fd6edf437c8f4cda1e136ec8e
Author: Rui <[email protected]>
AuthorDate: Mon Aug 3 11:38:53 2026 +0800
fix: refresh K8s certificate expiry status (#765)
---
.../studio/cluster/k8s/K8sCertService.java | 52 ++++++++--
.../studio/cluster/k8s/K8sCertServiceTest.java | 107 ++++++++++++++++++---
2 files changed, 139 insertions(+), 20 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertService.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertService.java
index 8230abdc..88206e44 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertService.java
@@ -19,31 +19,49 @@ package org.apache.rocketmq.studio.cluster.k8s;
import org.apache.rocketmq.studio.common.domain.enums.CertStatus;
import org.apache.rocketmq.studio.common.domain.enums.CertType;
import org.apache.rocketmq.studio.common.exception.BusinessException;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
+import java.time.Clock;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import java.util.stream.Collectors;
@Slf4j
@Service
-@RequiredArgsConstructor
public class K8sCertService {
+ private static final int EXPIRING_THRESHOLD_DAYS = 30;
+
private final K8sCertRepository k8sCertRepository;
+ private final Clock clock;
+
+ @Autowired
+ public K8sCertService(K8sCertRepository k8sCertRepository) {
+ this(k8sCertRepository, Clock.systemDefaultZone());
+ }
+
+ K8sCertService(K8sCertRepository k8sCertRepository, Clock clock) {
+ this.k8sCertRepository = k8sCertRepository;
+ this.clock = clock;
+ }
public List<K8sCertVO> listCerts() {
log.info("Listing all K8s certificates");
- return k8sCertRepository.findAll();
+ LocalDateTime now = LocalDateTime.now(clock);
+ return k8sCertRepository.findAll().stream()
+ .map(cert -> refreshExpirationState(cert, now))
+ .collect(Collectors.toCollection(ArrayList::new));
}
public K8sCertVO createCert(CreateCertDTO command) {
log.info("Creating K8s certificate: {}", command.getName());
- LocalDateTime now = LocalDateTime.now();
+ LocalDateTime now = LocalDateTime.now(clock);
LocalDateTime notAfter = now.plusYears(1);
K8sCertVO cert = K8sCertVO.builder()
@@ -91,9 +109,10 @@ public class K8sCertService {
if (command.getSan() != null) {
updated.setSan(command.getSan());
}
- updated.setUpdatedAt(LocalDateTime.now());
+ LocalDateTime now = LocalDateTime.now(clock);
+ updated.setUpdatedAt(now);
- K8sCertVO saved = k8sCertRepository.save(updated);
+ K8sCertVO saved =
k8sCertRepository.save(refreshExpirationState(updated, now));
log.info("K8s certificate updated: {} (id={})", saved.getName(),
saved.getId());
return saved;
}
@@ -103,7 +122,7 @@ public class K8sCertService {
K8sCertVO existing = k8sCertRepository.findById(command.getId())
.orElseThrow(() -> new BusinessException(404, "Certificate not
found: " + command.getId()));
- LocalDateTime now = LocalDateTime.now();
+ LocalDateTime now = LocalDateTime.now(clock);
LocalDateTime notAfter = now.plusYears(1);
K8sCertVO renewed = copyOf(existing);
@@ -126,6 +145,25 @@ public class K8sCertService {
log.info("K8s certificate deleted: {}", command.getId());
}
+ private K8sCertVO refreshExpirationState(K8sCertVO cert, LocalDateTime
now) {
+ K8sCertVO refreshed = copyOf(cert);
+ LocalDateTime notAfter = refreshed.getNotAfter();
+ if (notAfter == null) {
+ return refreshed;
+ }
+
+ int daysRemaining = (int) ChronoUnit.DAYS.between(now, notAfter);
+ refreshed.setDaysRemaining(daysRemaining);
+ if (!notAfter.isAfter(now)) {
+ refreshed.setStatus(CertStatus.expired);
+ } else if (daysRemaining <= EXPIRING_THRESHOLD_DAYS) {
+ refreshed.setStatus(CertStatus.expiring);
+ } else {
+ refreshed.setStatus(CertStatus.valid);
+ }
+ return refreshed;
+ }
+
private K8sCertVO copyOf(K8sCertVO cert) {
K8sCertVO copy = K8sCertVO.builder()
.name(cert.getName())
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
index b023935f..3bd4d515 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/k8s/K8sCertServiceTest.java
@@ -23,11 +23,13 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import java.time.Clock;
+import java.time.Instant;
import java.time.LocalDateTime;
+import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -42,16 +44,19 @@ import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class K8sCertServiceTest {
+ private static final Clock CLOCK = Clock.fixed(
+ Instant.parse("2025-07-01T00:00:00Z"), ZoneOffset.UTC);
+
@Mock
private K8sCertRepository k8sCertRepository;
- @InjectMocks
private K8sCertService k8sCertService;
private K8sCertVO sampleCert;
@BeforeEach
void setUp() {
+ k8sCertService = new K8sCertService(k8sCertRepository, CLOCK);
sampleCert = K8sCertVO.builder()
.name("rocketmq-tls")
.namespace("mq-system")
@@ -99,6 +104,35 @@ class K8sCertServiceTest {
assertThat(result).isEmpty();
}
+ @Test
+ void listCertsShouldRefreshTimeDerivedExpiryFields() {
+ LocalDateTime now = LocalDateTime.now(CLOCK);
+ sampleCert.setNotAfter(now.minusDays(1));
+ sampleCert.setStatus(CertStatus.valid);
+ sampleCert.setDaysRemaining(180);
+ K8sCertVO expiresNowCert = copyWithExpiry("cert-expires-now", now,
+ CertStatus.valid, 180);
+ K8sCertVO expiringCert = copyWithExpiry("cert-expiring",
now.plusDays(30),
+ CertStatus.expired, -1);
+ K8sCertVO validCert = copyWithExpiry("cert-valid", now.plusDays(31),
+ CertStatus.expired, -1);
+ when(k8sCertRepository.findAll())
+ .thenReturn(List.of(sampleCert, expiresNowCert, expiringCert,
validCert));
+
+ List<K8sCertVO> result = k8sCertService.listCerts();
+
+ assertThat(result).extracting(K8sCertVO::getStatus)
+ .containsExactly(CertStatus.expired, CertStatus.expired,
+ CertStatus.expiring, CertStatus.valid);
+ assertThat(result).extracting(K8sCertVO::getDaysRemaining)
+ .containsExactly(-1, 0, 30, 31);
+ assertThat(result.get(0)).isNotSameAs(sampleCert);
+ assertThat(sampleCert.getStatus()).isEqualTo(CertStatus.valid);
+ assertThat(sampleCert.getDaysRemaining()).isEqualTo(180);
+ assertThat(expiringCert.getStatus()).isEqualTo(CertStatus.expired);
+ assertThat(expiringCert.getDaysRemaining()).isEqualTo(-1);
+ }
+
@Test
void createCertShouldCreateAndSaveCert() {
CreateCertDTO command = CreateCertDTO.builder()
@@ -119,6 +153,7 @@ class K8sCertServiceTest {
});
K8sCertVO result = k8sCertService.createCert(command);
+ LocalDateTime now = LocalDateTime.now(CLOCK);
assertThat(result.getName()).isEqualTo("new-tls-cert");
assertThat(result.getNamespace()).isEqualTo("default");
@@ -127,9 +162,11 @@ class K8sCertServiceTest {
assertThat(result.getIssuer()).isEqualTo("vault");
assertThat(result.getStatus()).isEqualTo(CertStatus.valid);
assertThat(result.getSan()).containsExactly("svc.example.com");
- assertThat(result.getNotBefore()).isNotNull();
- assertThat(result.getNotAfter()).isAfter(result.getNotBefore());
- assertThat(result.getDaysRemaining()).isGreaterThan(0);
+ assertThat(result.getNotBefore()).isEqualTo(now);
+ assertThat(result.getNotAfter()).isEqualTo(now.plusYears(1));
+ assertThat(result.getDaysRemaining()).isEqualTo(365);
+ assertThat(result.getCreatedAt()).isEqualTo(now);
+ assertThat(result.getUpdatedAt()).isEqualTo(now);
verify(k8sCertRepository).save(any(K8sCertVO.class));
}
@@ -149,9 +186,10 @@ class K8sCertServiceTest {
k8sCertService.createCert(command);
K8sCertVO saved = captor.getValue();
- assertThat(saved.getNotAfter()).isAfter(saved.getNotBefore());
- long expectedDays =
java.time.temporal.ChronoUnit.DAYS.between(saved.getNotBefore(),
saved.getNotAfter());
- assertThat(saved.getDaysRemaining()).isEqualTo((int) expectedDays);
+ LocalDateTime now = LocalDateTime.now(CLOCK);
+ assertThat(saved.getNotBefore()).isEqualTo(now);
+ assertThat(saved.getNotAfter()).isEqualTo(now.plusYears(1));
+ assertThat(saved.getDaysRemaining()).isEqualTo(365);
}
@Test
@@ -179,7 +217,7 @@ class K8sCertServiceTest {
assertThat(result.getSan()).containsExactly("new.example.com");
assertThat(result.getId()).isEqualTo("cert-1");
assertThat(result.getCreatedAt()).isEqualTo(LocalDateTime.of(2024, 12,
1, 0, 0));
- assertThat(result.getUpdatedAt()).isAfter(sampleCert.getUpdatedAt());
+ assertThat(result.getUpdatedAt()).isEqualTo(LocalDateTime.now(CLOCK));
assertThat(result).isNotSameAs(sampleCert);
assertThat(sampleCert.getName()).isEqualTo("rocketmq-tls");
assertThat(sampleCert.getType()).isEqualTo(CertType.TLS);
@@ -187,6 +225,28 @@ class K8sCertServiceTest {
verify(k8sCertRepository).save(any(K8sCertVO.class));
}
+ @Test
+ void updateCertShouldRefreshTimeDerivedExpiryFields() {
+ LocalDateTime now = LocalDateTime.now(CLOCK);
+ sampleCert.setNotAfter(now.minusDays(1));
+ sampleCert.setStatus(CertStatus.valid);
+ sampleCert.setDaysRemaining(180);
+
when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
+
when(k8sCertRepository.save(any(K8sCertVO.class))).thenAnswer(invocation ->
invocation.getArgument(0));
+ UpdateCertDTO command = UpdateCertDTO.builder()
+ .id("cert-1")
+ .name("updated-expired-cert")
+ .build();
+
+ K8sCertVO result = k8sCertService.updateCert(command);
+
+ assertThat(result.getStatus()).isEqualTo(CertStatus.expired);
+ assertThat(result.getDaysRemaining()).isEqualTo(-1);
+ assertThat(sampleCert.getStatus()).isEqualTo(CertStatus.valid);
+ assertThat(sampleCert.getDaysRemaining()).isEqualTo(180);
+ verify(k8sCertRepository).save(result);
+ }
+
@Test
void updateCertShouldPreserveExistingFieldsWhenCommandFieldsAreNull() {
when(k8sCertRepository.findById("cert-1")).thenReturn(Optional.of(sampleCert));
@@ -253,12 +313,13 @@ class K8sCertServiceTest {
RenewCertDTO command = RenewCertDTO.builder().id("cert-1").build();
K8sCertVO result = k8sCertService.renewCert(command);
+ LocalDateTime now = LocalDateTime.now(CLOCK);
assertThat(result.getStatus()).isEqualTo(CertStatus.valid);
- assertThat(result.getDaysRemaining()).isGreaterThan(0);
- assertThat(result.getNotBefore()).isNotNull();
- assertThat(result.getNotAfter()).isAfter(result.getNotBefore());
- assertThat(result.getUpdatedAt()).isNotNull();
+ assertThat(result.getDaysRemaining()).isEqualTo(365);
+ assertThat(result.getNotBefore()).isEqualTo(now);
+ assertThat(result.getNotAfter()).isEqualTo(now.plusYears(1));
+ assertThat(result.getUpdatedAt()).isEqualTo(now);
assertThat(result.getId()).isEqualTo("cert-1");
assertThat(result.getCreatedAt()).isEqualTo(sampleCert.getCreatedAt());
assertThat(result).isNotSameAs(sampleCert);
@@ -324,4 +385,24 @@ class K8sCertServiceTest {
.isInstanceOf(BusinessException.class)
.hasMessageContaining("Certificate not found: nonexistent");
}
+
+ private K8sCertVO copyWithExpiry(String id, LocalDateTime notAfter,
CertStatus status,
+ int daysRemaining) {
+ K8sCertVO cert = K8sCertVO.builder()
+ .name(sampleCert.getName())
+ .namespace(sampleCert.getNamespace())
+ .cluster(sampleCert.getCluster())
+ .type(sampleCert.getType())
+ .issuer(sampleCert.getIssuer())
+ .notBefore(sampleCert.getNotBefore())
+ .notAfter(notAfter)
+ .status(status)
+ .daysRemaining(daysRemaining)
+ .san(sampleCert.getSan())
+ .build();
+ cert.setId(id);
+ cert.setCreatedAt(sampleCert.getCreatedAt());
+ cert.setUpdatedAt(sampleCert.getUpdatedAt());
+ return cert;
+ }
}