From 33914ee71f1fde4252f53800e278295318b58301 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 18:44:40 +0300 Subject: [PATCH 1/2] Add health-check-interval module parameter Introduce the health-check-interval MTA module parameter (kebab-case, integer seconds). It mirrors how Cloud Foundry's native health-check-interval works on the v3 process HealthCheck.Data object. The parameter is declared in SupportedParameters, parsed by StagingParametersParser, propagated through Staging (request side) and CloudProcess / RawCloudProcess (response side), included in the HealthCheckInfo diff used by StagingApplicationAttributeUpdater (so that changing only the interval triggers a staging update), and pushed to CF via CloudControllerRestClientImpl#buildHealthCheck using the existing Data.Builder.interval(...) method (already in use for the readiness side). When the parameter is absent, PropertiesUtil returns null and Data.Builder.interval(null) does not serialize the field, preserving the omit-when-absent semantics consistent with the other health-check-* parameters. JIRA:LMCROSSITXSADEPLOY-3316 --- .../client/facade/adapters/RawCloudProcess.java | 3 +++ .../client/facade/domain/CloudProcess.java | 3 +++ .../controller/client/facade/domain/Staging.java | 6 ++++++ .../rest/CloudControllerRestClientImpl.java | 1 + .../client/lib/domain/HealthCheckInfo.java | 15 ++++++++++++--- .../core/model/SupportedParameters.java | 2 ++ .../core/parser/StagingParametersParser.java | 3 +++ 7 files changed, 30 insertions(+), 3 deletions(-) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java index 4bd4b83775..931f528b75 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcess.java @@ -23,11 +23,13 @@ public CloudProcess derive() { Integer healthCheckTimeout = null; String healthCheckHttpEndpoint = null; Integer healthCheckInvocationTimeout = null; + Integer healthCheckInterval = null; if (healthCheck.getData() != null) { Data healthCheckData = healthCheck.getData(); healthCheckTimeout = healthCheckData.getTimeout(); healthCheckInvocationTimeout = healthCheckData.getInvocationTimeout(); healthCheckHttpEndpoint = healthCheckData.getEndpoint(); + healthCheckInterval = healthCheckData.getInterval(); } Integer readinessHealthCheckInvocationTimeout = null; String readinessHealthCheckHttpEndpoint = null; @@ -49,6 +51,7 @@ public CloudProcess derive() { .healthCheckHttpEndpoint(healthCheckHttpEndpoint) .healthCheckTimeout(healthCheckTimeout) .healthCheckInvocationTimeout(healthCheckInvocationTimeout) + .healthCheckInterval(healthCheckInterval) .readinessHealthCheckType(readinessHealthCheckType.getType() .getValue()) .readinessHealthCheckHttpEndpoint(readinessHealthCheckHttpEndpoint) diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java index 86df6003a7..ef141ae3fe 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/facade/domain/CloudProcess.java @@ -29,6 +29,9 @@ public abstract class CloudProcess extends CloudEntity implements Derivable> parametersList) { Integer healthCheckInvocationTimeout = (Integer) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_INVOCATION_TIMEOUT, null); + Integer healthCheckInterval = (Integer) PropertiesUtil.getPropertyValue(parametersList, + SupportedParameters.HEALTH_CHECK_INTERVAL, null); String healthCheckType = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_TYPE, null); String healthCheckHttpEndpoint = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT, @@ -69,6 +71,7 @@ public Staging parse(List> parametersList) { .invocationTimeout(healthCheckInvocationTimeout) .healthCheckType(healthCheckType) .healthCheckHttpEndpoint(healthCheckHttpEndpoint) + .healthCheckInterval(healthCheckInterval) .readinessHealthCheckType(readinessHealthCheckType) .readinessHealthCheckHttpEndpoint(readinessHealthCheckHttpEndpoint) .readinessHealthCheckInvocationTimeout(readinessHealthCheckInvocationTimeout) From bb90b413bab1d19456db3afcbc6a5bdc5e45ae1d Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 18:51:51 +0300 Subject: [PATCH 2/2] Add tests for health-check-interval parameter Cover the new health-check-interval module parameter: - HealthCheckInfoTest: 9 cases exercising the diff/equality logic that drives StagingApplicationAttributeUpdater, including interval-only changes triggering an update and null-interval round-trips. - RawCloudProcessTest: 3 cases covering the response-side derivation of the interval from the v3 process payload. - StagingParametersParserTest: 2 added cases for parsing the new parameter (present and absent). JIRA:LMCROSSITXSADEPLOY-3316 --- .../facade/adapters/RawCloudProcessTest.java | 146 ++++++++++++++++ .../lib/domain/HealthCheckInfoTest.java | 165 ++++++++++++++++++ .../parser/StagingParametersParserTest.java | 19 ++ 3 files changed, 330 insertions(+) create mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java create mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java new file mode 100644 index 0000000000..b57652433c --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java @@ -0,0 +1,146 @@ +package org.cloudfoundry.multiapps.controller.client.facade.adapters; + +import org.cloudfoundry.client.v3.Metadata; +import org.cloudfoundry.client.v3.processes.Data; +import org.cloudfoundry.client.v3.processes.HealthCheck; +import org.cloudfoundry.client.v3.processes.HealthCheckType; +import org.cloudfoundry.client.v3.processes.ProcessRelationships; +import org.cloudfoundry.client.v3.processes.ProcessResource; +import org.cloudfoundry.client.v3.processes.ReadinessHealthCheck; +import org.cloudfoundry.client.v3.processes.ReadinessHealthCheckType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class RawCloudProcessTest { + + private static final String COMMAND = "bundle exec rackup"; + private static final Integer DISK_IN_MB = 1024; + private static final Integer INSTANCES = 2; + private static final Integer MEMORY_IN_MB = 512; + private static final String HTTP_ENDPOINT = "/health"; + private static final Integer TIMEOUT = 30; + private static final Integer INVOCATION_TIMEOUT = 5; + private static final Integer INTERVAL = 10; + private static final String READINESS_HTTP_ENDPOINT = "/ready"; + private static final Integer READINESS_INVOCATION_TIMEOUT = 4; + private static final Integer READINESS_INTERVAL = 8; + + @Test + void testDeriveWithFullHealthCheckDataIncludingInterval() { + RawCloudProcess raw = ImmutableRawCloudProcess.of(buildProcessResource(HealthCheckType.HTTP, buildHealthCheckData(), + ReadinessHealthCheckType.HTTP, + buildReadinessHealthCheckData())); + + CloudProcess derived = raw.derive(); + + assertEquals(buildExpected(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType.HTTP, INTERVAL, + ReadinessHealthCheckType.HTTP.getValue()), + derived); + } + + @Test + void testDeriveWithMissingHealthCheckDataLeavesIntervalNull() { + // healthCheck.getData() returns null -> all four health-check Integer/String fields stay null + RawCloudProcess raw = ImmutableRawCloudProcess.of(buildProcessResource(HealthCheckType.PORT, null, + ReadinessHealthCheckType.PORT, null)); + + CloudProcess derived = raw.derive(); + + assertEquals(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType.PORT, derived.getHealthCheckType()); + assertNull(derived.getHealthCheckTimeout()); + assertNull(derived.getHealthCheckInvocationTimeout()); + assertNull(derived.getHealthCheckHttpEndpoint()); + assertNull(derived.getHealthCheckInterval()); + assertNull(derived.getReadinessHealthCheckInvocationTimeout()); + assertNull(derived.getReadinessHealthCheckHttpEndpoint()); + assertNull(derived.getReadinessHealthCheckInterval()); + } + + @Test + void testDeriveWithHealthCheckDataPresentButIntervalNull() { + Data dataWithoutInterval = Data.builder() + .endpoint(HTTP_ENDPOINT) + .timeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .build(); + RawCloudProcess raw = ImmutableRawCloudProcess.of(buildProcessResource(HealthCheckType.HTTP, dataWithoutInterval, + ReadinessHealthCheckType.PORT, null)); + + CloudProcess derived = raw.derive(); + + assertEquals(TIMEOUT, derived.getHealthCheckTimeout()); + assertEquals(INVOCATION_TIMEOUT, derived.getHealthCheckInvocationTimeout()); + assertEquals(HTTP_ENDPOINT, derived.getHealthCheckHttpEndpoint()); + assertNull(derived.getHealthCheckInterval()); + } + + private static ProcessResource buildProcessResource(HealthCheckType healthCheckType, Data healthCheckData, + ReadinessHealthCheckType readinessHealthCheckType, Data readinessHealthCheckData) { + HealthCheck.Builder healthCheckBuilder = HealthCheck.builder() + .type(healthCheckType); + if (healthCheckData != null) { + healthCheckBuilder.data(healthCheckData); + } + ReadinessHealthCheck.Builder readinessBuilder = ReadinessHealthCheck.builder() + .type(readinessHealthCheckType); + if (readinessHealthCheckData != null) { + readinessBuilder.data(readinessHealthCheckData); + } + return ProcessResource.builder() + .id(RawCloudEntityTest.GUID_STRING) + .createdAt(RawCloudEntityTest.CREATED_AT_STRING) + .updatedAt(RawCloudEntityTest.UPDATED_AT_STRING) + .command(COMMAND) + .diskInMb(DISK_IN_MB) + .instances(INSTANCES) + .memoryInMb(MEMORY_IN_MB) + .type("web") + .metadata(Metadata.builder() + .build()) + .relationships(ProcessRelationships.builder() + .build()) + .healthCheck(healthCheckBuilder.build()) + .readinessHealthCheck(readinessBuilder.build()) + .build(); + } + + private static Data buildHealthCheckData() { + return Data.builder() + .endpoint(HTTP_ENDPOINT) + .timeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .interval(INTERVAL) + .build(); + } + + private static Data buildReadinessHealthCheckData() { + return Data.builder() + .endpoint(READINESS_HTTP_ENDPOINT) + .invocationTimeout(READINESS_INVOCATION_TIMEOUT) + .interval(READINESS_INTERVAL) + .build(); + } + + private static CloudProcess buildExpected(org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType type, + Integer interval, String readinessHealthCheckType) { + return ImmutableCloudProcess.builder() + .command(COMMAND) + .instances(INSTANCES) + .memoryInMb(MEMORY_IN_MB) + .diskInMb(DISK_IN_MB) + .healthCheckType(type) + .healthCheckHttpEndpoint(HTTP_ENDPOINT) + .healthCheckTimeout(TIMEOUT) + .healthCheckInvocationTimeout(INVOCATION_TIMEOUT) + .healthCheckInterval(interval) + .readinessHealthCheckType(readinessHealthCheckType) + .readinessHealthCheckHttpEndpoint(READINESS_HTTP_ENDPOINT) + .readinessHealthCheckInvocationTimeout(READINESS_INVOCATION_TIMEOUT) + .readinessHealthCheckInterval(READINESS_INTERVAL) + .build(); + } +} diff --git a/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java new file mode 100644 index 0000000000..f58f490335 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java @@ -0,0 +1,165 @@ +package org.cloudfoundry.multiapps.controller.client.lib.domain; + +import org.cloudfoundry.multiapps.controller.client.facade.domain.CloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.HealthCheckType; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableCloudProcess; +import org.cloudfoundry.multiapps.controller.client.facade.domain.ImmutableStaging; +import org.cloudfoundry.multiapps.controller.client.facade.domain.Staging; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +class HealthCheckInfoTest { + + private static final String HTTP_TYPE = "http"; + private static final Integer TIMEOUT = 30; + private static final Integer INVOCATION_TIMEOUT = 5; + private static final Integer INTERVAL = 10; + private static final String HTTP_ENDPOINT = "/health"; + + @Test + void testFromStagingPropagatesAllFieldsIncludingInterval() { + Staging staging = ImmutableStaging.builder() + .healthCheckType(HTTP_TYPE) + .healthCheckTimeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .healthCheckInterval(INTERVAL) + .healthCheckHttpEndpoint(HTTP_ENDPOINT) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertEquals(HTTP_TYPE, info.getType()); + assertEquals(TIMEOUT, info.getTimeout()); + assertEquals(INVOCATION_TIMEOUT, info.getInvocationTimeout()); + assertEquals(INTERVAL, info.getInterval()); + assertEquals(HTTP_ENDPOINT, info.getHttpEndpoint()); + } + + @Test + void testFromStagingDefaultsTypeToPortWhenNullAndCarriesNullInterval() { + Staging staging = ImmutableStaging.builder() + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertEquals("port", info.getType()); + assertNull(info.getTimeout()); + assertNull(info.getInvocationTimeout()); + assertNull(info.getInterval()); + assertNull(info.getHttpEndpoint()); + } + + @Test + void testFromProcessPropagatesAllFieldsIncludingInterval() { + CloudProcess process = ImmutableCloudProcess.builder() + .command("cmd") + .diskInMb(512) + .instances(1) + .memoryInMb(1024) + .healthCheckType(HealthCheckType.HTTP) + .healthCheckHttpEndpoint(HTTP_ENDPOINT) + .healthCheckTimeout(TIMEOUT) + .healthCheckInvocationTimeout(INVOCATION_TIMEOUT) + .healthCheckInterval(INTERVAL) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromProcess(process); + + assertEquals(HealthCheckType.HTTP.toString(), info.getType()); + assertEquals(TIMEOUT, info.getTimeout()); + assertEquals(INVOCATION_TIMEOUT, info.getInvocationTimeout()); + assertEquals(INTERVAL, info.getInterval()); + assertEquals(HTTP_ENDPOINT, info.getHttpEndpoint()); + } + + @Test + void testFromProcessWithNullIntervalCarriesNull() { + CloudProcess process = ImmutableCloudProcess.builder() + .command("cmd") + .diskInMb(512) + .instances(1) + .memoryInMb(1024) + .healthCheckType(HealthCheckType.PORT) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromProcess(process); + + assertEquals(HealthCheckType.PORT.toString(), info.getType()); + assertNull(info.getInterval()); + } + + @Test + void testEqualsReturnsTrueForIdenticalContent() { + Staging staging = ImmutableStaging.builder() + .healthCheckType(HTTP_TYPE) + .healthCheckTimeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .healthCheckInterval(INTERVAL) + .healthCheckHttpEndpoint(HTTP_ENDPOINT) + .build(); + + HealthCheckInfo a = HealthCheckInfo.fromStaging(staging); + HealthCheckInfo b = HealthCheckInfo.fromStaging(staging); + + assertEquals(a, b); + } + + @Test + void testEqualsReturnsFalseWhenIntervalDiffers() { + Staging stagingWith10 = ImmutableStaging.builder() + .healthCheckType(HTTP_TYPE) + .healthCheckTimeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .healthCheckInterval(10) + .healthCheckHttpEndpoint(HTTP_ENDPOINT) + .build(); + Staging stagingWith20 = ImmutableStaging.builder() + .healthCheckType(HTTP_TYPE) + .healthCheckTimeout(TIMEOUT) + .invocationTimeout(INVOCATION_TIMEOUT) + .healthCheckInterval(20) + .healthCheckHttpEndpoint(HTTP_ENDPOINT) + .build(); + + HealthCheckInfo a = HealthCheckInfo.fromStaging(stagingWith10); + HealthCheckInfo b = HealthCheckInfo.fromStaging(stagingWith20); + + assertNotEquals(a, b); + } + + @Test + void testEqualsReturnsFalseWhenOneIntervalIsNull() { + Staging stagingWithInterval = ImmutableStaging.builder() + .healthCheckType(HTTP_TYPE) + .healthCheckInterval(INTERVAL) + .build(); + Staging stagingWithoutInterval = ImmutableStaging.builder() + .healthCheckType(HTTP_TYPE) + .build(); + + HealthCheckInfo a = HealthCheckInfo.fromStaging(stagingWithInterval); + HealthCheckInfo b = HealthCheckInfo.fromStaging(stagingWithoutInterval); + + assertNotEquals(a, b); + } + + @Test + void testEqualsReturnsFalseForNonHealthCheckInfo() { + HealthCheckInfo info = HealthCheckInfo.fromStaging(ImmutableStaging.builder() + .build()); + + assertNotEquals(info, "not-a-health-check-info"); + } + + @Test + void testEqualsReturnsTrueForSelf() { + HealthCheckInfo info = HealthCheckInfo.fromStaging(ImmutableStaging.builder() + .healthCheckInterval(INTERVAL) + .build()); + + assertEquals(info, info); + } +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java index 98677fc307..b2a9edf5da 100644 --- a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java @@ -21,6 +21,7 @@ import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACK; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACKS; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.DOCKER; +import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.HEALTH_CHECK_INTERVAL; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.LIFECYCLE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -138,6 +139,24 @@ void testValidateWithAllParametersMissing() { assertNull(staging.getDockerInfo()); } + @Test + void testHealthCheckIntervalIsParsedWhenPresent() { + parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 15)); + + Staging staging = parser.parse(parametersList); + + assertNotNull(staging); + assertEquals(Integer.valueOf(15), staging.getHealthCheckInterval()); + } + + @Test + void testHealthCheckIntervalIsNullWhenAbsent() { + Staging staging = parser.parse(parametersList); + + assertNotNull(staging); + assertNull(staging.getHealthCheckInterval()); + } + private static Map mapOf(String key, Object value) { return Collections.singletonMap(key, value); }