diff --git a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/DropletInfoFactory.java b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/DropletInfoFactory.java index bba3f61b15..af168d2ed7 100644 --- a/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/DropletInfoFactory.java +++ b/multiapps-controller-client/src/main/java/org/cloudfoundry/multiapps/controller/client/lib/domain/DropletInfoFactory.java @@ -1,13 +1,13 @@ package org.cloudfoundry.multiapps.controller.client.lib.domain; +import java.util.List; + import com.sap.cloudfoundry.client.facade.CloudControllerClient; import com.sap.cloudfoundry.client.facade.domain.CloudApplication; import com.sap.cloudfoundry.client.facade.domain.DockerData; import com.sap.cloudfoundry.client.facade.domain.LifecycleType; import com.sap.cloudfoundry.client.facade.domain.Staging; -import java.util.List; - public class DropletInfoFactory { public DropletInfo createDropletInfo(Staging staging) { @@ -20,7 +20,7 @@ public DropletInfo createDropletInfo(Staging staging) { public DropletInfo createDropletInfo(CloudApplication app, CloudControllerClient client) { var lifecycle = app.getLifecycle(); - if (lifecycle.getType() == LifecycleType.BUILDPACK) { + if (lifecycle.getType() == LifecycleType.BUILDPACK || lifecycle.getType() == LifecycleType.CNB) { var buildpacks = (List) lifecycle.getData() .get("buildpacks"); var stack = (String) lifecycle.getData() diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java index 781061f4b1..f6ed7472aa 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/model/SupportedParameters.java @@ -54,6 +54,7 @@ public class SupportedParameters { public static final String BUILDPACK = "buildpack"; public static final String BUILDPACKS = "buildpacks"; public static final String STACK = "stack"; + public static final String LIFECYCLE = "lifecycle"; public static final String HEALTH_CHECK_INVOCATION_TIMEOUT = "health-check-invocation-timeout"; public static final String HEALTH_CHECK_TIMEOUT = "health-check-timeout"; public static final String HEALTH_CHECK_TYPE = "health-check-type"; 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 141f91d20e..9e1787e188 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 @@ -3,11 +3,13 @@ import java.util.List; import java.util.Map; +import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; import org.cloudfoundry.multiapps.mta.util.PropertiesUtil; import com.sap.cloudfoundry.client.facade.domain.DockerInfo; import com.sap.cloudfoundry.client.facade.domain.ImmutableStaging; +import com.sap.cloudfoundry.client.facade.domain.LifecycleType; import com.sap.cloudfoundry.client.facade.domain.Staging; public class StagingParametersParser implements ParametersParser { @@ -32,6 +34,10 @@ public Staging parse(List> parametersList) { getDefaultHealthCheckHttpEndpoint(healthCheckType)); Boolean isSshEnabled = (Boolean) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.ENABLE_SSH, null); DockerInfo dockerInfo = new DockerInfoParser().parse(parametersList); + LifecycleType lifecycleType = parseLifecycleType(parametersList); + + validateLifecycleType(lifecycleType, buildpacks, dockerInfo); + return ImmutableStaging.builder() .command(command) .buildpacks(buildpacks) @@ -42,9 +48,39 @@ public Staging parse(List> parametersList) { .healthCheckHttpEndpoint(healthCheckHttpEndpoint) .isSshEnabled(isSshEnabled) .dockerInfo(dockerInfo) + .lifecycleType(lifecycleType) .build(); } + private LifecycleType parseLifecycleType(List> parametersList) { + String lifecycleValue = (String) PropertiesUtil.getPropertyValue(parametersList, SupportedParameters.LIFECYCLE, null); + if (lifecycleValue == null) { + return null; + } + try { + return LifecycleType.valueOf(lifecycleValue.toUpperCase()); + } catch (IllegalArgumentException e) { + throw new SLException("Unsupported lifecycle value: " + lifecycleValue); + } + } + + private void validateLifecycleType(LifecycleType lifecycleType, List buildpacks, DockerInfo dockerInfo) { + if (lifecycleType == LifecycleType.CNB && (buildpacks == null || buildpacks.isEmpty())) { + throw new SLException("Buildpacks must be provided when lifecycle is set to 'cnb'."); + } + // Validate Docker-specific conditions + if (lifecycleType == LifecycleType.DOCKER) { + if (dockerInfo == null) { + throw new SLException("Docker information must be provided when lifecycle is set to 'docker'."); + } + if (buildpacks != null && !buildpacks.isEmpty()) { + throw new SLException("Buildpacks must not be provided when lifecycle is set to 'docker'."); + } + } else if (dockerInfo != null && lifecycleType != null) { + throw new SLException("Docker information must not be provided when lifecycle is set to " + lifecycleType + "'."); + } + } + private String getDefaultHealthCheckHttpEndpoint(String healthCheckType) { return HTTP_HEALTH_CHECK_TYPE.equals(healthCheckType) ? DEFAULT_HEALTH_CHECK_HTTP_ENDPOINT : null; } 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 new file mode 100644 index 0000000000..4920af01a5 --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/parser/StagingParametersParserTest.java @@ -0,0 +1,72 @@ +package org.cloudfoundry.multiapps.controller.core.parser; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.core.model.SupportedParameters; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class StagingParametersParserTest { + + private StagingParametersParser parser; + private List> parametersList; + + @BeforeEach + void setup() { + parser = new StagingParametersParser(); + parametersList = new ArrayList<>(); + } + + @Test + void testValidateLifecycleWithCnbAndValidBuildpacks() { + parametersList.add(Collections.singletonMap("lifecycle", "cnb")); + parametersList.add(Collections.singletonMap("buildpacks", List.of("custom-buildpack-url"))); + assertDoesNotThrow(() -> parser.parse(parametersList)); + } + + @Test + void testValidateLifecycleWithCnbAndNoBuildpacks() { + parametersList.add(Collections.singletonMap(SupportedParameters.LIFECYCLE, "cnb")); + Exception exception = assertThrows(SLException.class, () -> parser.parse(parametersList)); + assertEquals("Buildpacks must be provided when lifecycle is set to 'cnb'.", exception.getMessage()); + } + + @Test + void testValidateLifecycleWithDockerAndValidDockerInfo() { + parametersList.add(getDockerParams()); + assertDoesNotThrow(() -> parser.parse(parametersList)); + } + + @Test + void testValidateLifecycleWithDockerAndBuildpacksProvided() { + parametersList.add(getDockerParams()); + parametersList.add(Collections.singletonMap("buildpacks", List.of("some-buildpack"))); + + Exception exception = assertThrows(SLException.class, () -> parser.parse(parametersList)); + assertEquals("Buildpacks must not be provided when lifecycle is set to 'docker'.", exception.getMessage()); + } + + @Test + void testValidateLifecycleWithBuildpackAndNoBuildpacks() { + parametersList.add(Collections.singletonMap("lifecycle", "buildpack")); + assertDoesNotThrow(() -> parser.parse(parametersList)); + } + + @Test + void testValidateLifecycleWithInvalidLifecycleValue() { + parametersList.add(Collections.singletonMap("lifecycle", "invalid_value")); + Exception exception = assertThrows(SLException.class, () -> parser.parse(parametersList)); + assertEquals("Unsupported lifecycle value: invalid_value", exception.getMessage()); + } + + private static Map getDockerParams() { + return Map.of(SupportedParameters.LIFECYCLE, "docker", SupportedParameters.DOCKER, Map.of("image", "cloudfoundry/test-app")); + } + +} 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 652c316304..cfe4220fb6 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 @@ -2,21 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anySet; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -32,20 +21,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mock; -import com.sap.cloudfoundry.client.facade.domain.CloudApplication; -import com.sap.cloudfoundry.client.facade.domain.CloudRoute; -import com.sap.cloudfoundry.client.facade.domain.HealthCheckType; -import com.sap.cloudfoundry.client.facade.domain.ImmutableCloudApplication; -import com.sap.cloudfoundry.client.facade.domain.ImmutableCloudMetadata; -import com.sap.cloudfoundry.client.facade.domain.ImmutableCloudPackage; -import com.sap.cloudfoundry.client.facade.domain.ImmutableCloudProcess; -import com.sap.cloudfoundry.client.facade.domain.ImmutableDockerData; -import com.sap.cloudfoundry.client.facade.domain.ImmutableDockerInfo; -import com.sap.cloudfoundry.client.facade.domain.ImmutableDropletInfo; -import com.sap.cloudfoundry.client.facade.domain.ImmutableLifecycle; -import com.sap.cloudfoundry.client.facade.domain.ImmutableStaging; -import com.sap.cloudfoundry.client.facade.domain.LifecycleType; -import com.sap.cloudfoundry.client.facade.domain.Staging; +import com.sap.cloudfoundry.client.facade.domain.*; import com.sap.cloudfoundry.client.facade.util.JsonUtil; class CreateOrUpdateStepWithExistingAppTest extends SyncFlowableStepTest { @@ -65,16 +41,16 @@ static Stream testHandleStagingApplicationAttributes() { return Stream.of( //@formatter:off Arguments.of(ImmutableStaging.builder().addBuildpack("buildpack-1").command("command1").build(), - ImmutableStaging.builder().addBuildpack("buildpack-1").command("command2").build(), true), + ImmutableStaging.builder().addBuildpack("buildpack-1").command("command2").build(), true, LifecycleType.BUILDPACK), Arguments.of(ImmutableStaging.builder().addBuildpack("buildpack-1").build(), - ImmutableStaging.builder().addBuildpack("buildpack-1").build(), false), + ImmutableStaging.builder().addBuildpack("buildpack-1").build(), false, LifecycleType.BUILDPACK), Arguments.of( ImmutableStaging.builder().addBuildpack("buildpack-1").command("command1").stackName("stack1") .healthCheckTimeout(5).healthCheckType("process").isSshEnabled(false).build(), ImmutableStaging.builder().addBuildpack("buildpack-2").command("command2").stackName("stack2") .healthCheckTimeout(10).healthCheckType("port").healthCheckHttpEndpoint("/test") .isSshEnabled(true).build(), - true), + true, LifecycleType.BUILDPACK), Arguments.of( ImmutableStaging.builder().addBuildpack("buildpack-2").command("command2").stackName("stack2") .healthCheckTimeout(10).healthCheckType("process").healthCheckHttpEndpoint("/test") @@ -82,50 +58,56 @@ static Stream testHandleStagingApplicationAttributes() { ImmutableStaging.builder().addBuildpack("buildpack-2").command("command2").stackName("stack2") .healthCheckTimeout(10).healthCheckType("process").healthCheckHttpEndpoint("/test") .isSshEnabled(true).build(), - false), + false, LifecycleType.BUILDPACK), Arguments.of(ImmutableStaging.builder() .dockerInfo(ImmutableDockerInfo.builder().image("cloudfoundry/test-app").build()).build(), ImmutableStaging.builder() .dockerInfo(ImmutableDockerInfo.builder().image("cloudfoundry/test-app2").build()) .build(), - true), + true, LifecycleType.DOCKER), Arguments.of(ImmutableStaging.builder() .dockerInfo(ImmutableDockerInfo.builder().image("cloudfoundry/test-app").build()).build(), ImmutableStaging.builder() .dockerInfo(ImmutableDockerInfo.builder().image("cloudfoundry/test-app").build()) .build(), - false)); + false, LifecycleType.DOCKER), + Arguments.of(ImmutableStaging.builder().build(), + ImmutableStaging.builder().lifecycleType(LifecycleType.CNB).build(), + false, LifecycleType.CNB), + Arguments.of(ImmutableStaging.builder().addBuildpack("buildpack-1").command("command1").build(), + ImmutableStaging.builder().addBuildpack("buildpack-1").command("command2").lifecycleType(LifecycleType.CNB).build(), + 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)); //@formatter:on } @ParameterizedTest @MethodSource - void testHandleStagingApplicationAttributes(Staging existingStaging, Staging staging, boolean expectedPropertiesChanged) { + void testHandleStagingApplicationAttributes(Staging existingStaging, Staging staging, boolean expectedPropertiesChanged, + LifecycleType expectedLifecycleType) { CloudApplication existingApplication = getApplicationBuilder(false).staging(existingStaging) .build(); - if (staging.getCommand() == null) { - staging = ImmutableStaging.copyOf(staging) - .withCommand(DEFAULT_COMMAND); - } - if (staging.getStackName() == null) { - staging = ImmutableStaging.copyOf(staging) - .withStackName(DEFAULT_STACK); - } + staging = ImmutableStaging.copyOf(applyDefaultsToStaging(staging)); CloudApplicationExtended application = getApplicationBuilder(false).staging(staging) .build(); - if (staging.getDockerInfo() != null) { - application = ImmutableCloudApplicationExtended.copyOf(application) - .withLifecycle(ImmutableLifecycle.builder() - .type(LifecycleType.DOCKER) - .build()); - } + LifecycleType lifecycleTypeToApply = determineLifecycleType(staging); + application = ImmutableCloudApplicationExtended.copyOf(application) + .withLifecycle(ImmutableLifecycle.builder() + .type(lifecycleTypeToApply) + .build()); + prepareContext(application, false); prepareClientWithStaging(existingApplication, existingStaging); step.execute(execution); assertStepFinishedSuccessfully(); + assertEquals(expectedLifecycleType, application.getLifecycle() + .getType()); assertEquals(expectedPropertiesChanged, context.getVariable(Variables.VCAP_APP_PROPERTIES_CHANGED)); + if (expectedPropertiesChanged) { verify(client).updateApplicationStaging(APP_NAME, staging); return; @@ -133,6 +115,27 @@ void testHandleStagingApplicationAttributes(Staging existingStaging, Staging sta verify(client, never()).updateApplicationStaging(eq(APP_NAME), any()); } + private LifecycleType determineLifecycleType(Staging staging) { + if (staging.getLifecycleType() != null) { + return staging.getLifecycleType(); + } else if (staging.getDockerInfo() != null) { + return LifecycleType.DOCKER; + } + return LifecycleType.BUILDPACK; + } + + private Staging applyDefaultsToStaging(Staging staging) { + if (staging.getCommand() == null) { + staging = ImmutableStaging.copyOf(staging) + .withCommand(DEFAULT_COMMAND); + } + if (staging.getStackName() == null) { + staging = ImmutableStaging.copyOf(staging) + .withStackName(DEFAULT_STACK); + } + return staging; + } + private ImmutableCloudApplicationExtended.Builder getApplicationBuilder(boolean shouldKeepExistingEnv) { return ImmutableCloudApplicationExtended.builder() .metadata(ImmutableCloudMetadata.of(UUID.randomUUID()))