Skip to content
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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<String>) lifecycle.getData()
.get("buildpacks");
var stack = (String) lifecycle.getData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Staging> {
Expand All @@ -32,6 +34,10 @@ public Staging parse(List<Map<String, Object>> 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)
Expand All @@ -42,9 +48,39 @@ public Staging parse(List<Map<String, Object>> parametersList) {
.healthCheckHttpEndpoint(healthCheckHttpEndpoint)
.isSshEnabled(isSshEnabled)
.dockerInfo(dockerInfo)
.lifecycleType(lifecycleType)
.build();
}

private LifecycleType parseLifecycleType(List<Map<String, Object>> 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<String> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, Object>> 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<String, Object> getDockerParams() {
return Map.of(SupportedParameters.LIFECYCLE, "docker", SupportedParameters.DOCKER, Map.of("image", "cloudfoundry/test-app"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<CreateOrUpdateAppStep> {
Expand All @@ -65,74 +41,101 @@ static Stream<Arguments> 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")
.isSshEnabled(true).build(),
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;
}
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()))
Expand Down
Loading