From 8b579cc6957aa6601e5a03702c8fc38872f60361 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 12:04:35 +0300 Subject: [PATCH 1/8] Add health-check-interval parameter for liveness probe Surface a new MTA module parameter `health-check-interval` (Integer, seconds) that controls the liveness health-check interval for a CF application. Mirrors the existing `readiness-health-check-interval` end-to-end: - New SupportedParameters constant registered in MODULE_PARAMETERS. - StagingParametersParser reads the value and forwards it through the ImmutableStaging builder. - Staging interface and CloudProcess gain a getHealthCheckInterval() accessor; Immutables regenerates the builders. - CloudControllerRestClientImpl.buildHealthCheck forwards the value via Data.builder().interval(...) so the CF API call carries it. - RawCloudProcess populates the new field from healthCheckData.getInterval() so updates round-trip correctly. - HealthCheckInfo carries the new field so StagingApplicationAttributeUpdater detects redeploys that change only the liveness interval. - Parser unit tests cover both present and absent cases. Behaviour is fully additive: omitting the parameter is byte-for-byte identical to the pre-change baseline. JIRA:LMCROSSITXSADEPLOY-3316 --- .../facade/adapters/RawCloudProcess.java | 3 +++ .../client/facade/domain/CloudProcess.java | 3 +++ .../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 +++ .../parser/StagingParametersParserTest.java | 19 +++++++++++++++++++ 8 files changed, 49 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, @@ -67,6 +69,7 @@ public Staging parse(List> parametersList) { .stackName(stackName) .healthCheckTimeout(healthCheckTimeout) .invocationTimeout(healthCheckInvocationTimeout) + .healthCheckInterval(healthCheckInterval) .healthCheckType(healthCheckType) .healthCheckHttpEndpoint(healthCheckHttpEndpoint) .readinessHealthCheckType(readinessHealthCheckType) 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..a3c6ad9a14 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 testHealthCheckIntervalIsParsedWhenProvided() { + parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 15)); + + Staging staging = parser.parse(parametersList); + + assertNotNull(staging); + assertEquals(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); } From cc0aeaa15c4b27b075031e798d7dcea57d2a2782 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 12:50:19 +0300 Subject: [PATCH 2/8] Cover health-check-interval drift detection in update-app test Extend the existing parameterized drift-detection test with two rows that exercise the new liveness interval field: one where the value changes between existing and new staging (should trigger an update) and one where it matches (should not). Plumb the value through prepareClient so the mocked getApplicationProcess echoes the configured interval back. JIRA:LMCROSSITXSADEPLOY-3316 --- .../steps/CreateOrUpdateStepWithExistingAppTest.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateOrUpdateStepWithExistingAppTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateOrUpdateStepWithExistingAppTest.java index 78f20b5f65..789436bbe3 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateOrUpdateStepWithExistingAppTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/CreateOrUpdateStepWithExistingAppTest.java @@ -111,7 +111,15 @@ static Stream testHandleStagingApplicationAttributes() { true, LifecycleType.CNB), Arguments.of(ImmutableStaging.builder().addBuildpack("buildpack-333").build(), ImmutableStaging.builder().addBuildpacks("buildpack-4", "buildpack-8").lifecycleType(LifecycleType.CNB).build(), - true, LifecycleType.CNB)); + true, LifecycleType.CNB), + Arguments.of( + ImmutableStaging.builder().addBuildpack("buildpack-1").healthCheckType("port").healthCheckInterval(10).build(), + ImmutableStaging.builder().addBuildpack("buildpack-1").healthCheckType("port").healthCheckInterval(20).build(), + true, LifecycleType.BUILDPACK), + Arguments.of( + ImmutableStaging.builder().addBuildpack("buildpack-1").healthCheckType("port").healthCheckInterval(10).build(), + ImmutableStaging.builder().addBuildpack("buildpack-1").healthCheckType("port").healthCheckInterval(10).build(), + false, LifecycleType.BUILDPACK)); //@formatter:on } @@ -232,6 +240,7 @@ private void prepareClient(CloudApplication application, Set routes, String command = staging.getCommand() == null ? DEFAULT_COMMAND : staging.getCommand(); var hcType = staging.getHealthCheckType(); var hcTimeout = staging.getHealthCheckTimeout(); + var hcInterval = staging.getHealthCheckInterval(); var hcEndpoint = staging.getHealthCheckHttpEndpoint(); when(client.getApplicationProcess(application.getGuid())).thenReturn(ImmutableCloudProcess.builder() .command(command) @@ -243,6 +252,7 @@ private void prepareClient(CloudApplication application, Set routes, : HealthCheckType.valueOf( hcType.toUpperCase())) .healthCheckTimeout(hcTimeout) + .healthCheckInterval(hcInterval) .healthCheckHttpEndpoint(hcEndpoint) .instances(1) .build()); From 9c8a3df19927991705f6df7c1149d7e71377af21 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 12:52:05 +0300 Subject: [PATCH 3/8] Add unit test for liveness interval round-trip in RawCloudProcess Cover the new RawCloudProcess#derive branch that copies the liveness health check interval from the cf-java-client Data into the derived CloudProcess. Three cases: explicit interval is propagated, absent interval surfaces as null, and a HealthCheck with null Data does not NPE and yields a null interval. JIRA:LMCROSSITXSADEPLOY-3316 --- .../facade/adapters/RawCloudProcessTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.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..d3be5ffcf7 --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/facade/adapters/RawCloudProcessTest.java @@ -0,0 +1,84 @@ +package org.cloudfoundry.multiapps.controller.client.facade.adapters; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +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.Process; +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.junit.jupiter.api.Test; + +class RawCloudProcessTest { + + @Test + void testDeriveCarriesLivenessIntervalFromHealthCheckData() { + Process process = buildProcess(buildHealthCheck(15), buildReadinessHealthCheck(null)); + + CloudProcess derived = ImmutableRawCloudProcess.of(process) + .derive(); + + assertEquals(15, derived.getHealthCheckInterval()); + } + + @Test + void testDeriveLivenessIntervalIsNullWhenAbsent() { + Process process = buildProcess(buildHealthCheck(null), buildReadinessHealthCheck(null)); + + CloudProcess derived = ImmutableRawCloudProcess.of(process) + .derive(); + + assertNull(derived.getHealthCheckInterval()); + } + + @Test + void testDeriveLivenessIntervalIsNullWhenHealthCheckDataIsNull() { + HealthCheck healthCheck = mock(HealthCheck.class); + when(healthCheck.getData()).thenReturn(null); + when(healthCheck.getType()).thenReturn(HealthCheckType.PORT); + Process process = buildProcess(healthCheck, buildReadinessHealthCheck(null)); + + CloudProcess derived = ImmutableRawCloudProcess.of(process) + .derive(); + + assertNull(derived.getHealthCheckInterval()); + } + + private static Process buildProcess(HealthCheck healthCheck, ReadinessHealthCheck readinessHealthCheck) { + Process process = mock(Process.class); + when(process.getCommand()).thenReturn("test-command"); + when(process.getInstances()).thenReturn(1); + when(process.getMemoryInMb()).thenReturn(256); + when(process.getDiskInMb()).thenReturn(512); + when(process.getHealthCheck()).thenReturn(healthCheck); + when(process.getReadinessHealthCheck()).thenReturn(readinessHealthCheck); + return process; + } + + private static HealthCheck buildHealthCheck(Integer interval) { + Data.Builder dataBuilder = Data.builder(); + if (interval != null) { + dataBuilder.interval(interval); + } + return HealthCheck.builder() + .type(HealthCheckType.PORT) + .data(dataBuilder.build()) + .build(); + } + + private static ReadinessHealthCheck buildReadinessHealthCheck(Integer interval) { + Data.Builder dataBuilder = Data.builder(); + if (interval != null) { + dataBuilder.interval(interval); + } + return ReadinessHealthCheck.builder() + .type(ReadinessHealthCheckType.PORT) + .data(dataBuilder.build()) + .build(); + } +} From 18bf0b617914182a9f6bd58dacf2b1c9ca96e6cb Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 13:00:28 +0300 Subject: [PATCH 4/8] Add HealthCheckInfo unit tests for liveness interval JIRA:LMCROSSITXSADEPLOY-3316 --- .../lib/domain/HealthCheckInfoTest.java | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) 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/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..f95e2ec93e --- /dev/null +++ b/multiapps-controller-client/src/test/java/org/cloudfoundry/multiapps/controller/client/lib/domain/HealthCheckInfoTest.java @@ -0,0 +1,142 @@ +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 { + + @Test + void testFromStagingCarriesInterval() { + Staging staging = ImmutableStaging.builder() + .healthCheckType("http") + .healthCheckTimeout(30) + .invocationTimeout(5) + .healthCheckInterval(15) + .healthCheckHttpEndpoint("/health") + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertEquals("http", info.getType()); + assertEquals(30, info.getTimeout()); + assertEquals(5, info.getInvocationTimeout()); + assertEquals(15, info.getInterval()); + assertEquals("/health", info.getHttpEndpoint()); + } + + @Test + void testFromStagingIntervalIsNullWhenAbsent() { + Staging staging = ImmutableStaging.builder() + .healthCheckType("port") + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertNull(info.getInterval()); + } + + @Test + void testFromStagingDefaultsTypeToPortWhenNull() { + Staging staging = ImmutableStaging.builder() + .healthCheckInterval(10) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromStaging(staging); + + assertEquals("port", info.getType()); + assertEquals(10, info.getInterval()); + } + + @Test + void testFromProcessCarriesInterval() { + CloudProcess process = ImmutableCloudProcess.builder() + .command("test-cmd") + .diskInMb(512) + .instances(1) + .memoryInMb(256) + .healthCheckType(HealthCheckType.HTTP) + .healthCheckTimeout(45) + .healthCheckInvocationTimeout(7) + .healthCheckInterval(20) + .healthCheckHttpEndpoint("/live") + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromProcess(process); + + assertEquals("http", info.getType()); + assertEquals(45, info.getTimeout()); + assertEquals(7, info.getInvocationTimeout()); + assertEquals(20, info.getInterval()); + assertEquals("/live", info.getHttpEndpoint()); + } + + @Test + void testFromProcessIntervalIsNullWhenAbsent() { + CloudProcess process = ImmutableCloudProcess.builder() + .command("test-cmd") + .diskInMb(512) + .instances(1) + .memoryInMb(256) + .healthCheckType(HealthCheckType.PORT) + .build(); + + HealthCheckInfo info = HealthCheckInfo.fromProcess(process); + + assertNull(info.getInterval()); + } + + @Test + void testEqualsReturnsFalseWhenIntervalDiffers() { + Staging stagingA = ImmutableStaging.builder() + .healthCheckType("port") + .healthCheckInterval(10) + .build(); + Staging stagingB = ImmutableStaging.builder() + .healthCheckType("port") + .healthCheckInterval(20) + .build(); + + assertNotEquals(HealthCheckInfo.fromStaging(stagingA), HealthCheckInfo.fromStaging(stagingB)); + } + + @Test + void testEqualsReturnsTrueWhenAllFieldsMatchIncludingInterval() { + Staging stagingA = ImmutableStaging.builder() + .healthCheckType("port") + .healthCheckTimeout(30) + .invocationTimeout(5) + .healthCheckInterval(10) + .healthCheckHttpEndpoint("/health") + .build(); + Staging stagingB = ImmutableStaging.builder() + .healthCheckType("port") + .healthCheckTimeout(30) + .invocationTimeout(5) + .healthCheckInterval(10) + .healthCheckHttpEndpoint("/health") + .build(); + + assertEquals(HealthCheckInfo.fromStaging(stagingA), HealthCheckInfo.fromStaging(stagingB)); + } + + @Test + void testEqualsReturnsFalseWhenOneIntervalIsNull() { + Staging stagingWithInterval = ImmutableStaging.builder() + .healthCheckType("port") + .healthCheckInterval(10) + .build(); + Staging stagingWithoutInterval = ImmutableStaging.builder() + .healthCheckType("port") + .build(); + + assertNotEquals(HealthCheckInfo.fromStaging(stagingWithInterval), HealthCheckInfo.fromStaging(stagingWithoutInterval)); + } +} From d50db5e9c6012995407171ea3d278f99740e1eae Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 13:25:58 +0300 Subject: [PATCH 5/8] Reject non-positive health-check-interval values Adds an explicit guard in StagingParametersParser that throws a ContentException with a clear message when health-check-interval is zero or negative, plus tests covering both rejection cases. JIRA:LMCROSSITXSADEPLOY-3316 --- .../multiapps/controller/core/Messages.java | 1 + .../core/parser/StagingParametersParser.java | 8 ++++++++ .../parser/StagingParametersParserTest.java | 17 +++++++++++++++++ 3 files changed, 26 insertions(+) diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 6efb7e7b3d..6775612479 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -91,6 +91,7 @@ public final class Messages { public static final String BUILDPACKS_REQUIRED_FOR_CNB = "Buildpacks must be provided when lifecycle is set to 'cnb'."; public static final String DOCKER_INFO_REQUIRED = "Docker information must be provided when lifecycle is set to 'docker'."; public static final String BUILDPACKS_NOT_ALLOWED_WITH_DOCKER = "Buildpacks must not be provided when lifecycle is set to 'docker'."; + public static final String INVALID_HEALTH_CHECK_INTERVAL = "Invalid value \"{0}\" for parameter \"health-check-interval\". The value must be a positive integer (seconds)."; public static final String EXTENSION_DESCRIPTORS_COULD_NOT_BE_PARSED_TO_VALID_YAML = "Extension descriptor(s) could not be parsed as a valid YAML file. These descriptors may fail future deployments once stricter validation is enforced. Please review and correct them now to avoid future issues. Use at your own risk"; public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected"; public static final String ENCRYPTION_HAS_FAILED = "Encryption has failed! Errored with \"{0}\""; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java index b538455a48..5f0b077adc 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java @@ -20,6 +20,7 @@ import static org.cloudfoundry.multiapps.controller.core.Messages.BUILDPACKS_REQUIRED_FOR_CNB; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_NOT_ALLOWED_WITH_LIFECYCLE; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_REQUIRED; +import static org.cloudfoundry.multiapps.controller.core.Messages.INVALID_HEALTH_CHECK_INTERVAL; import static org.cloudfoundry.multiapps.controller.core.Messages.UNSUPPORTED_LIFECYCLE_VALUE; public class StagingParametersParser implements ParametersParser { @@ -39,6 +40,7 @@ public Staging parse(List> parametersList) { null); Integer healthCheckInterval = (Integer) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_INTERVAL, null); + validateHealthCheckInterval(healthCheckInterval); String healthCheckType = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_TYPE, null); String healthCheckHttpEndpoint = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT, @@ -83,6 +85,12 @@ public Staging parse(List> parametersList) { .build(); } + private void validateHealthCheckInterval(Integer healthCheckInterval) { + if (healthCheckInterval != null && healthCheckInterval <= 0) { + throw new ContentException(MessageFormat.format(INVALID_HEALTH_CHECK_INTERVAL, healthCheckInterval)); + } + } + @SuppressWarnings("unchecked") private Map getAppFeatures(List> parametersList) { return new HashMap<>((Map) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.APP_FEATURES, 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 a3c6ad9a14..8682dfbd56 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 @@ -17,6 +17,7 @@ import static org.cloudfoundry.multiapps.controller.core.Messages.BUILDPACKS_REQUIRED_FOR_CNB; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_NOT_ALLOWED_WITH_LIFECYCLE; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_REQUIRED; +import static org.cloudfoundry.multiapps.controller.core.Messages.INVALID_HEALTH_CHECK_INTERVAL; import static org.cloudfoundry.multiapps.controller.core.Messages.UNSUPPORTED_LIFECYCLE_VALUE; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACK; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACKS; @@ -157,6 +158,22 @@ void testHealthCheckIntervalIsNullWhenAbsent() { assertNull(staging.getHealthCheckInterval()); } + @Test + void testHealthCheckIntervalRejectsZero() { + parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 0)); + + ContentException exception = assertThrows(ContentException.class, () -> parser.parse(parametersList)); + assertEquals(MessageFormat.format(INVALID_HEALTH_CHECK_INTERVAL, 0), exception.getMessage()); + } + + @Test + void testHealthCheckIntervalRejectsNegative() { + parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, -5)); + + ContentException exception = assertThrows(ContentException.class, () -> parser.parse(parametersList)); + assertEquals(MessageFormat.format(INVALID_HEALTH_CHECK_INTERVAL, -5), exception.getMessage()); + } + private static Map mapOf(String key, Object value) { return Collections.singletonMap(key, value); } From e6efbdac42da7ec5d5938f0b813febb5ed6d1a30 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 13:32:45 +0300 Subject: [PATCH 6/8] Cover health-check-interval boundary in StagingParametersParser test Adds a boundary case for health-check-interval=1 to lock in the inclusive-zero exclusion enforced by StagingParametersParser. JIRA:LMCROSSITXSADEPLOY-3316 --- .../core/parser/StagingParametersParserTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 8682dfbd56..c8eede9d37 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 @@ -158,6 +158,16 @@ void testHealthCheckIntervalIsNullWhenAbsent() { assertNull(staging.getHealthCheckInterval()); } + @Test + void testHealthCheckIntervalAcceptsSmallestPositiveValue() { + parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 1)); + + Staging staging = parser.parse(parametersList); + + assertNotNull(staging); + assertEquals(1, staging.getHealthCheckInterval()); + } + @Test void testHealthCheckIntervalRejectsZero() { parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 0)); From f450bf3b5cd69c5275a3cb469fb03d4b765556d4 Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 14:04:57 +0300 Subject: [PATCH 7/8] Drop validation of health-check-interval values Remove the parser-side rejection of zero/negative health-check-interval values to keep parity with healthCheckTimeout and readinessHealthCheckInterval, which forward any non-null Integer to the Cloud Controller without validation. The CF Java client and Cloud Controller remain the source of truth for value validity. JIRA:LMCROSSITXSADEPLOY-3316 --- .../multiapps/controller/core/Messages.java | 1 - .../core/parser/StagingParametersParser.java | 8 ------ .../parser/StagingParametersParserTest.java | 27 ------------------- 3 files changed, 36 deletions(-) diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 6775612479..6efb7e7b3d 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -91,7 +91,6 @@ public final class Messages { public static final String BUILDPACKS_REQUIRED_FOR_CNB = "Buildpacks must be provided when lifecycle is set to 'cnb'."; public static final String DOCKER_INFO_REQUIRED = "Docker information must be provided when lifecycle is set to 'docker'."; public static final String BUILDPACKS_NOT_ALLOWED_WITH_DOCKER = "Buildpacks must not be provided when lifecycle is set to 'docker'."; - public static final String INVALID_HEALTH_CHECK_INTERVAL = "Invalid value \"{0}\" for parameter \"health-check-interval\". The value must be a positive integer (seconds)."; public static final String EXTENSION_DESCRIPTORS_COULD_NOT_BE_PARSED_TO_VALID_YAML = "Extension descriptor(s) could not be parsed as a valid YAML file. These descriptors may fail future deployments once stricter validation is enforced. Please review and correct them now to avoid future issues. Use at your own risk"; public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected"; public static final String ENCRYPTION_HAS_FAILED = "Encryption has failed! Errored with \"{0}\""; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java index 5f0b077adc..b538455a48 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParser.java @@ -20,7 +20,6 @@ import static org.cloudfoundry.multiapps.controller.core.Messages.BUILDPACKS_REQUIRED_FOR_CNB; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_NOT_ALLOWED_WITH_LIFECYCLE; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_REQUIRED; -import static org.cloudfoundry.multiapps.controller.core.Messages.INVALID_HEALTH_CHECK_INTERVAL; import static org.cloudfoundry.multiapps.controller.core.Messages.UNSUPPORTED_LIFECYCLE_VALUE; public class StagingParametersParser implements ParametersParser { @@ -40,7 +39,6 @@ public Staging parse(List> parametersList) { null); Integer healthCheckInterval = (Integer) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_INTERVAL, null); - validateHealthCheckInterval(healthCheckInterval); String healthCheckType = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_TYPE, null); String healthCheckHttpEndpoint = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.HEALTH_CHECK_HTTP_ENDPOINT, @@ -85,12 +83,6 @@ public Staging parse(List> parametersList) { .build(); } - private void validateHealthCheckInterval(Integer healthCheckInterval) { - if (healthCheckInterval != null && healthCheckInterval <= 0) { - throw new ContentException(MessageFormat.format(INVALID_HEALTH_CHECK_INTERVAL, healthCheckInterval)); - } - } - @SuppressWarnings("unchecked") private Map getAppFeatures(List> parametersList) { return new HashMap<>((Map) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.APP_FEATURES, 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 c8eede9d37..a3c6ad9a14 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 @@ -17,7 +17,6 @@ import static org.cloudfoundry.multiapps.controller.core.Messages.BUILDPACKS_REQUIRED_FOR_CNB; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_NOT_ALLOWED_WITH_LIFECYCLE; import static org.cloudfoundry.multiapps.controller.core.Messages.DOCKER_INFO_REQUIRED; -import static org.cloudfoundry.multiapps.controller.core.Messages.INVALID_HEALTH_CHECK_INTERVAL; import static org.cloudfoundry.multiapps.controller.core.Messages.UNSUPPORTED_LIFECYCLE_VALUE; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACK; import static org.cloudfoundry.multiapps.controller.core.model.SupportedParameters.BUILDPACKS; @@ -158,32 +157,6 @@ void testHealthCheckIntervalIsNullWhenAbsent() { assertNull(staging.getHealthCheckInterval()); } - @Test - void testHealthCheckIntervalAcceptsSmallestPositiveValue() { - parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 1)); - - Staging staging = parser.parse(parametersList); - - assertNotNull(staging); - assertEquals(1, staging.getHealthCheckInterval()); - } - - @Test - void testHealthCheckIntervalRejectsZero() { - parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, 0)); - - ContentException exception = assertThrows(ContentException.class, () -> parser.parse(parametersList)); - assertEquals(MessageFormat.format(INVALID_HEALTH_CHECK_INTERVAL, 0), exception.getMessage()); - } - - @Test - void testHealthCheckIntervalRejectsNegative() { - parametersList.add(mapOf(HEALTH_CHECK_INTERVAL, -5)); - - ContentException exception = assertThrows(ContentException.class, () -> parser.parse(parametersList)); - assertEquals(MessageFormat.format(INVALID_HEALTH_CHECK_INTERVAL, -5), exception.getMessage()); - } - private static Map mapOf(String key, Object value) { return Collections.singletonMap(key, value); } From 16495ce687edc42d5d3e6550dddc5c1034ea46cf Mon Sep 17 00:00:00 2001 From: IvanBorislavovDimitrov Date: Mon, 1 Jun 2026 14:14:44 +0300 Subject: [PATCH 8/8] Report usage of liveness health-check-interval parameter Extend AdditionalModuleParametersReporter to log a separate line when a module specifies the new health-check-interval (liveness) parameter. The existing readiness log line is left untouched because it is observed by Dynatrace. Add a package-private constructor that accepts a Logger so the new liveness branch is verifiable in unit tests; the public constructor delegates to it with the static SLF4J logger. JIRA:LMCROSSITXSADEPLOY-3316 --- .../AdditionalModuleParametersReporter.java | 17 ++++ ...dditionalModuleParametersReporterTest.java | 81 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java index 1d2cff63be..13e1230fc1 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporter.java @@ -16,9 +16,15 @@ public class AdditionalModuleParametersReporter { private static final Logger LOGGER = LoggerFactory.getLogger(AdditionalModuleParametersReporter.class); private final ProcessContext context; + private final Logger logger; public AdditionalModuleParametersReporter(ProcessContext context) { + this(context, LOGGER); + } + + AdditionalModuleParametersReporter(ProcessContext context, Logger logger) { this.context = context; + this.logger = logger; } public void reportUsageOfAdditionalParameters(Module module) { @@ -33,6 +39,8 @@ public void reportUsageOfAdditionalParameters(Module module) { .get(SupportedParameters.READINESS_HEALTH_CHECK_INVOCATION_TIMEOUT); Integer readinessHealthCheckInterval = (Integer) module.getParameters() .get(SupportedParameters.READINESS_HEALTH_CHECK_INTERVAL); + Integer healthCheckInterval = (Integer) module.getParameters() + .get(SupportedParameters.HEALTH_CHECK_INTERVAL); List buildpacks = PropertiesUtil.getPluralOrSingular(List.of(module.getParameters()), SupportedParameters.BUILDPACKS, SupportedParameters.BUILDPACK); if (readinessHealthCheckType != null) { @@ -40,6 +48,9 @@ public void reportUsageOfAdditionalParameters(Module module) { readinessHealthCheckInvocationTimeout, readinessHealthCheckInterval, buildpacks.toString(), module.getType()); } + if (healthCheckInterval != null) { + reportUsageOfHealthCheckInterval(mtaId, correlationId, healthCheckInterval, buildpacks.toString(), module.getType()); + } } // this method is being observed by Dynatrace, be careful if you change it @@ -51,4 +62,10 @@ private void reportUsageOfReadinessHealthCheckParameters(String mtaId, String co mtaId, correlationId, readinessHealthCheckType, readinessHealthCheckHttpEndpoint, readinessHealthCheckInvocationTimeout, readinessHealthCheckInterval, buildpacks, moduleType)); } + + private void reportUsageOfHealthCheckInterval(String mtaId, String correlationId, Integer healthCheckInterval, String buildpacks, + String moduleType) { + logger.info(MessageFormat.format("MTA with ID \"{0}\" associated with operation ID \"{1}\" uses liveness health check parameters: interval=\"{2}\", buildpacks=\"{3}\", moduleType=\"{4}\"", + mtaId, correlationId, healthCheckInterval, buildpacks, moduleType)); + } } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java new file mode 100644 index 0000000000..629d849a5c --- /dev/null +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/util/AdditionalModuleParametersReporterTest.java @@ -0,0 +1,81 @@ +package org.cloudfoundry.multiapps.controller.process.util; + +import java.util.HashMap; +import java.util.Map; + +import org.cloudfoundry.multiapps.controller.core.cf.CloudControllerClientProvider; +import org.cloudfoundry.multiapps.controller.process.steps.ProcessContext; +import org.cloudfoundry.multiapps.controller.process.variables.Variables; +import org.cloudfoundry.multiapps.mta.model.DeploymentDescriptor; +import org.cloudfoundry.multiapps.mta.model.Module; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.slf4j.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class AdditionalModuleParametersReporterTest { + + private static final String MTA_ID = "test-mta"; + private static final String CORRELATION_ID = "test-correlation-id"; + + @Test + void testHealthCheckIntervalLoggedWhenSet() { + Logger mockLogger = Mockito.mock(Logger.class); + Module module = createModule(Map.of("health-check-interval", 30)); + + new AdditionalModuleParametersReporter(createContext(), mockLogger).reportUsageOfAdditionalParameters(module); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(String.class); + verify(mockLogger, atLeastOnce()).info(messageCaptor.capture()); + long livenessLogs = messageCaptor.getAllValues() + .stream() + .filter(message -> message.contains("liveness health check")) + .count(); + assertEquals(1, livenessLogs); + String message = messageCaptor.getAllValues() + .stream() + .filter(formatted -> formatted.contains("liveness health check")) + .findFirst() + .orElseThrow(); + assertTrue(message.contains("interval=\"30\""), "Log message should contain interval=\"30\", was: " + message); + assertTrue(message.contains(MTA_ID), "Log message should contain MTA id, was: " + message); + assertTrue(message.contains(CORRELATION_ID), "Log message should contain correlation id, was: " + message); + } + + @Test + void testHealthCheckIntervalNotLoggedWhenAbsent() { + Logger mockLogger = Mockito.mock(Logger.class); + Module module = createModule(new HashMap<>()); + + new AdditionalModuleParametersReporter(createContext(), mockLogger).reportUsageOfAdditionalParameters(module); + + verify(mockLogger, never()).info(anyString()); + } + + private static Module createModule(Map parameters) { + return Module.createV3() + .setName("test-module") + .setType("application") + .setParameters(parameters); + } + + private static ProcessContext createContext() { + DelegateExecution delegateExecution = MockDelegateExecution.createSpyInstance(); + StepLogger stepLogger = Mockito.mock(StepLogger.class); + CloudControllerClientProvider cloudControllerClientProvider = Mockito.mock(CloudControllerClientProvider.class); + ProcessContext context = new ProcessContext(delegateExecution, stepLogger, cloudControllerClientProvider); + DeploymentDescriptor deploymentDescriptor = DeploymentDescriptor.createV3() + .setId(MTA_ID); + context.setVariable(Variables.DEPLOYMENT_DESCRIPTOR, deploymentDescriptor); + context.setVariable(Variables.CORRELATION_ID, CORRELATION_ID); + return context; + } +}