Skip to content

Migration of the cf-java-client dependency - #1907

Open
karrgov wants to merge 20 commits into
masterfrom
cf_java_client_poc
Open

karrgov wants to merge 20 commits into
masterfrom
cf_java_client_poc

Conversation

@karrgov

@karrgov karrgov commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Migration of the cf-java-client dependency

@theghost5800 theghost5800 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of comments are repeatable or not but overall do the checks through all the code again if possible

Comment thread multiapps-controller-client/pom.xml Resolved
Comment thread multiapps-controller-client/pom.xml Resolved
Comment thread pom.xml Resolved

@theghost5800 theghost5800 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of comments are repeatable or not but overall do the checks through all the code again if possible

@karrgov
karrgov force-pushed the cf_java_client_poc branch from 19c69f8 to e7fa4e6 Compare October 13, 2026 05:37
@sonarqubecloud

Copy link
Copy Markdown

Comment thread pom.xml
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>${hamcrest.version}}</version>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the second '}' here

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +229 to +235
private ExecutorService createUploadMonitorExecutor() {
ThreadFactory threadFactory = Thread.ofVirtual()
.name(Constants.UPLOAD_MONITOR_THREAD_NAME_PREFIX, 0)
.factory();

return Executors.newThreadPerTaskExecutor(threadFactory);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExecutorService should be shut down as part of its owner's lifecycle. Here its created but it does not appear to be closed.

Comment on lines +17 to +19
public static final int MAX_CONCURRENT_PAGES = 4; //leaving it like that for testing purposes
public static final int SECOND_PAGE = 2;
public static final int DEFAULT_CONCURRENT_TASKS = 8; //this one can be a bit more, but first tests will be like that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these values be left for prod? If so remove the comments.

Comment on lines +201 to +207
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
// package was most probably already consumed by CF between the polls, meaning that since the build was already created from the package object, the package is not needed anymore
// and the cf blobstore garbage collector has deleted it
keepPolling = false;
continue;
}
throw e;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-404 throw runs on the background monitor thread, and it seems like the error gets swallowed. Should this call callback.onError(e) like the FAILED-status branch below?

public record V3Application(@JsonProperty(V3Fields.GUID) String guid, @JsonProperty(V3Fields.NAME) String name,
@JsonProperty(V3Fields.STATE) String state,
@JsonProperty(V3Fields.CREATED_AT) String createdAt, @JsonProperty(V3Fields.UPDATED_AT) String updatedAt,
@JsonProperty("lifecycle") V3Lifecycle lifecycle, @JsonProperty(V3Fields.METADATA) V3Metadata metadata,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only lifecycle is still string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

}

@JsonIgnoreProperties(ignoreUnknown = true)
public record V3EnvironmentVariables(@JsonProperty("var") Map<String, String> environmentVariables) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And this var

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +53 to +54
private static BitsData.Checksum
parseBitsChecksum(V3Package.V3Checksum checksum) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird formatting

Comment on lines +23 to +24
@JsonProperty("shareable") Boolean shareable,
@JsonProperty("documentation_url") String documentationUrl,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constants

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +36 to +44
public record V3Features(@JsonProperty("bindable") Boolean bindable,
@JsonProperty("allow_context_updates") Boolean allowContextUpdates,
@JsonProperty("bindings_retrievable") Boolean bindingsRetrievable,
@JsonProperty("instances_retrievable") Boolean instancesRetrievable,
@JsonProperty("plan_updatable") Boolean planUpdatable) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record V3Relationships(@JsonProperty("service_broker") V3ToOneRelationship serviceBroker) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constants

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +27 to +28
@JsonProperty(V3Fields.STATE) String state, @JsonProperty("memory_in_mb") Integer memoryInMb,
@JsonProperty("disk_in_mb") Integer diskInMb, @JsonProperty("result") V3Result result,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

constants

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@JsonProperty(V3Fields.METADATA) V3Metadata metadata) {

@JsonIgnoreProperties(ignoreUnknown = true)
public record V3Result(@JsonProperty("failure_reason") String failureReason) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +195 to +226
while (keepPolling) {
Upload upload;

try {
upload = getUploadStatus(cloudPackage.getGuid());
} catch (CloudOperationException e) {
if (e.getStatusCode() == HttpStatus.NOT_FOUND) {
// package was most probably already consumed by CF between the polls, meaning that since the build was already created from the package object, the package is not needed anymore
// and the cf blobstore garbage collector has deleted it
keepPolling = false;
continue;
}
throw e;
}

Status uploadStatus = upload.getStatus();
boolean unsubscribe = callback.onProgress(uploadStatus.toString());

if (unsubscribe || OperationsUtil.isUploadReady(uploadStatus)) {
keepPolling = false;
} else if (OperationsUtil.hasUploadFailed(uploadStatus)) {
callback.onError(upload.getErrorDetails()
.getDescription());
keepPolling = false;
} else {
try {
MiscUtil.sleep(Constants.PACKAGE_UPLOAD_JOB_POLLING_PERIOD);
} catch (IllegalStateException e) {
keepPolling = false;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats a very big while loop. Can you extract some of the things in it to methods


private static ObjectMapper configureObjectMapper() {
return new ObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing configs to fully match cf-java-client objectmapper behaviour

.registerModule(new Jdk8Module())
.setSerializationInclusion(NON_NULL);

Duration interval = Constants.JOB_POLL_MIN_INTERVAL;

while (true) {
V3Job asyncJob = get(CloudControllerV3Endpoints.JOBS + jobGuid, V3Job.class);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this changed, I expect to use similar structures as this method postForObject(String uri, Object body, Class<T> responseType, Object... uriVars). jobGuid to be passed as uriVar, not as concatenated String.

long timeoutNanos = timeout.toNanos();
Duration interval = Constants.JOB_POLL_MIN_INTERVAL;

while (true) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while (asyncJob == null || !asyncJob.isTerminal()) {
        long elapsedNanos = determineElapsedTime(startTimeNanos, timeout, jobGuid);
        long remainingNanos = timeoutNanos - elapsedNanos;

        Duration sleepDuration = interval.toNanos() > remainingNanos ? Duration.ofNanos(remainingNanos) : interval;
        sleep(sleepDuration);

        interval = nextInterval(interval);
        asyncJob = get(CloudControllerV3Endpoints.JOBS + jobGuid, V3Job.class);
    }

    if (asyncJob.isFailed()) {
        throw jobFailed(asyncJob);
    }
    return asyncJob;

Here is one solution which can avoid this.

Also I have notice in the current behaviour that there is no exit from the loop due to exceeded timeout which means if job doesn't return any success or failed state in expected timeout frame, then it will stuck indefinitely.

private CloudOperationException convertV3ClientException(AbstractCloudFoundryException e) {
HttpStatus httpStatus = HttpStatus.valueOf(e.getStatusCode());
return new CloudOperationException(httpStatus, httpStatus.getReasonPhrase(), e.getMessage(), e);
// The underlying CloudControllerRestClient already surfaces failures as CloudOperationException

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My question again is why we need this method if we do not make anymore any translations or changes? Can we just remove these two methods and stop invoking delegated method through lambda expressions?

Comment on lines +36 to +44
public record V3Features(@JsonProperty("bindable") Boolean bindable,
@JsonProperty("allow_context_updates") Boolean allowContextUpdates,
@JsonProperty("bindings_retrievable") Boolean bindingsRetrievable,
@JsonProperty("instances_retrievable") Boolean instancesRetrievable,
@JsonProperty("plan_updatable") Boolean planUpdatable) {
}

@JsonIgnoreProperties(ignoreUnknown = true)
public record V3Relationships(@JsonProperty("service_broker") V3ToOneRelationship serviceBroker) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

assertEquals(serviceName, service.getName());
assertEquals(IntegrationTestConstants.SERVICE_OFFERING, service.getLabel());
assertEquals(IntegrationTestConstants.SERVICE_PLAN, service.getPlan());
} catch (Exception e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this try-catch block. If some of method invocations throw exception, it will lead to failed test

assertTrue(finchServiceOffering.getServicePlans()
.stream()
.anyMatch(plan -> IntegrationTestConstants.SERVICE_PLAN.equals(plan.getName())),
MessageFormat.format("Expected plan \"{0}\" in the offering's plans", IntegrationTestConstants.SERVICE_PLAN));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape single quote

MessageFormat.format("Expected service \"{0}\" to no longer match the selector after the label was unset",
serviceName));
} catch (Exception e) {
fail(e);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this

assertTrue(tasks.stream()
.anyMatch(currentTask -> currentTask.getGuid()
.equals(task.getGuid())),
MessageFormat.format("Expected the created task \"{0}\" to be in the application's task list", task.getName()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape single quote

Comment thread pom.xml
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>${hamcrest.version}}</version>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment on lines +63 to +68
String domain = route.url();

if (StringUtils.hasText(route.host())) {
domain = domain.substring(route.host()
.length() + 1);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we slicing the url here?

Comment on lines +112 to +124
@Override
public void createApplication(ApplicationToCreateDto dto) {
applicationsOperations.createApplication(dto);

if (dto.getStaging() != null) {
updateApplicationStaging(dto.getName(), dto.getStaging());
}

if (dto.getRoutes() != null && !dto.getRoutes()
.isEmpty()) {
updateApplicationRoutes(dto.getName(), dto.getRoutes());
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lifecycle gets written twice on every app create: once in createApplication, then again in the updateApplicationStaging. Can you build it once in the create body and drop it from the staging update? Also the two builders aren't equivalent: only updateApplicationStaging validates CNB buildpacks.

Comment on lines +174 to +186
private UUID getRequiredApplicationGuid(String applicationName) {
String applicationsQuery = OperationsQueryUtil.buildApplicationsQuery(applicationName, targetSpace);
List<V3GuidReference> apps = client.list(applicationsQuery, APP_PAGE);

if (apps.isEmpty() || apps.getFirst()
.guid() == null) {
throw new CloudOperationException(HttpStatus.NOT_FOUND, HttpStatus.NOT_FOUND.getReasonPhrase(),
MessageFormat.format(Messages.APPLICATION_0_NOT_FOUND, applicationName));
}

return UUID.fromString(apps.getFirst()
.guid());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getRequiredApplicationGuid is duplicated across 6 classes: here PackagesV3Operations, EventsV3Operations, ProcessesV3Operations, RoutesV3Operations, TasksV3Operations, ServiceBindingsV3Operations. Can you extract it into one shared helper?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants