Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add callback/notification #5

Merged
merged 3 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@
<artifactId>quarkus-test-kubernetes-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>3.10.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
60 changes: 60 additions & 0 deletions src/main/java/org/jboss/pnc/konfluxbuilddriver/Driver.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
package org.jboss.pnc.konfluxbuilddriver;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.core.MediaType;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.pnc.api.constants.HttpHeaders;
import org.jboss.pnc.api.dto.Request;
import org.jboss.pnc.konfluxbuilddriver.clients.IndyService;
import org.jboss.pnc.konfluxbuilddriver.clients.IndyTokenRequestDTO;
import org.jboss.pnc.konfluxbuilddriver.clients.IndyTokenResponseDTO;
import org.jboss.pnc.konfluxbuilddriver.dto.BuildCompleted;
import org.jboss.pnc.konfluxbuilddriver.dto.BuildRequest;
import org.jboss.pnc.konfluxbuilddriver.dto.BuildResponse;
import org.jboss.pnc.konfluxbuilddriver.dto.CancelRequest;
import org.jboss.pnc.konfluxbuilddriver.dto.PipelineNotification;
import org.jboss.pnc.konfluxbuilddriver.util.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.fabric8.knative.internal.pkg.apis.Condition;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.utils.Serialization;
import io.fabric8.tekton.client.TektonClient;
import io.fabric8.tekton.pipeline.v1.ParamBuilder;
import io.fabric8.tekton.pipeline.v1.PipelineRun;
import io.fabric8.tekton.pipeline.v1.PipelineRunStatusBuilder;
import io.quarkus.logging.Log;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.runtime.StartupEvent;

Expand All @@ -49,6 +67,9 @@ public class Driver {
@Inject
Configuration config;

@Inject
ObjectMapper objectMapper;

URL pipelineRunTemplate;

void onStart(@Observes StartupEvent ev) {
Expand Down Expand Up @@ -86,6 +107,18 @@ public BuildResponse create(BuildRequest buildRequest) {
// TODO: This should be changed to true eventually.
templateProperties.put("ENABLE_INDY_PROXY", config.indyProxyEnabled());

try {
Request notificationCallback = new Request(
Request.Method.PUT,
new URI(StringUtils.appendIfMissing(config.selfBaseUrl(), "/") + "internal/completed"),
Copy link

Choose a reason for hiding this comment

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

It seems the "internal/completed" REST api calls the Driver's "completed()" method which calls the "internal/completed" endpoint again... would this be an infinite loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry I missed this. The build endpoint takes a callback (to call back to PNC). That callback is embedded into the pipeline which is then passed to the PipelineNotification so the driver completed method should be instead calling the very first callback, not the internal endpoint.

Collections.singletonList(new Request.Header(HttpHeaders.CONTENT_TYPE_STRING, MediaType.APPLICATION_JSON)),
buildRequest.completionCallback());

templateProperties.put("NOTIFICATION_CONTEXT", objectMapper.writeValueAsString(notificationCallback));
} catch (JsonProcessingException | URISyntaxException e) {
throw new RuntimeException(e);
}

// Various ways to create the initial PipelineRun object. We can use an objectmapper,
// client.getKubernetesSerialization() or the load calls on the Fabric8 objects.
PipelineRun pipelineRun = client.adapt(TektonClient.class).v1().pipelineRuns().load(pipelineRunTemplate).item();
Expand Down Expand Up @@ -139,4 +172,31 @@ public void cancel(CancelRequest request) {
public String getFreshAccessToken() {
return oidcClient.getTokens().await().indefinitely().getAccessToken();
}

public void completed(PipelineNotification notification) {

// TODO: PNC build-driver uses BuildCompleted when notifying the callback.
String body = Serialization
.asJson(BuildCompleted.builder().buildId(notification.buildId()).status(notification.status()).build());

HttpRequest.Builder builder = HttpRequest.newBuilder()
.uri(notification.completionCallback().getUri())
.method(notification.completionCallback().getMethod().name(), HttpRequest.BodyPublishers.ofString(body))
// TOOD: Timeouts?
// .timeout(Duration.ofSeconds(requestTimeout))
;
notification.completionCallback().getHeaders().forEach(h -> builder.header(h.getName(), h.getValue()));

HttpRequest request = builder.build();
// TODO: Retry? Send async? Some useful mutiny examples from quarkus in https://gist.github.com/cescoffier/e9abce907a1c3d05d70bea3dae6dc3d5
// TODO: Do we need the bearer token here?
HttpResponse<String> response;
try (var httpClient = HttpClient.newHttpClient()) {
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
Log.infof("Response %s", response);

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.jboss.pnc.konfluxbuilddriver.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.Builder;

@Builder(builderClassName = "Builder")
@JsonIgnoreProperties(ignoreUnknown = true)
public record BuildCompleted(
String status,
String buildId) {

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jboss.pnc.konfluxbuilddriver.dto;

import org.jboss.pnc.api.dto.Request;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.Builder;
Expand All @@ -19,6 +21,8 @@ public record BuildRequest(
String repositoryDeployUrl,
String repositoryBuildContentId,
String namespace,
String podMemoryOverride) {
String podMemoryOverride,
// Callback to use for the completion notification
Request completionCallback) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.jboss.pnc.konfluxbuilddriver.dto;

import org.jboss.pnc.api.dto.Request;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.Builder;

// TODO: This is a direct copy of the same class in jvm-build-service. Both need moved to pnc-api to
// avoid clashes and duplication.
@Builder(builderClassName = "Builder")
@JsonIgnoreProperties(ignoreUnknown = true)
public record PipelineNotification(
String status,
String buildId,
Request completionCallback) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.jboss.pnc.konfluxbuilddriver.endpoints;

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.jboss.pnc.konfluxbuilddriver.Driver;
import org.jboss.pnc.konfluxbuilddriver.dto.PipelineNotification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.smallrye.common.annotation.RunOnVirtualThread;

/**
* Endpoint to receive build result from the build agent.
*/
@Path("/internal")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class Internal {

private static final Logger logger = LoggerFactory.getLogger(Internal.class);

@Inject
Driver driver;

@PUT
@Path("/completed")
@RunOnVirtualThread
public void buildExecutionCompleted(PipelineNotification notification) {
logger.info("Build completed, taskId: {}; status: {}.", notification.buildId(), notification.status());
driver.completed(notification);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public interface Configuration {
String resolverTarget();

String indyProxyEnabled();

String selfBaseUrl();
}
1 change: 1 addition & 0 deletions src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ konflux-build-driver:
# TODO: This will eventually be build-definitions repository
pipeline-resolver: "https://raw.githubusercontent.com/redhat-appstudio/jvm-build-service/refs/heads/main/deploy/pipeline/mw-pipeline-v0.1.yaml"
indy-proxy-enabled: false
self-base-url: http://localhost:8081/
quarkus:
application:
name: konflux-build-driver
Expand Down
29 changes: 28 additions & 1 deletion src/test/java/org/jboss/pnc/konfluxbuilddriver/DriverTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,30 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.logging.LogRecord;

import javax.ws.rs.core.MediaType;

import jakarta.inject.Inject;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jboss.pnc.api.constants.HttpHeaders;
import org.jboss.pnc.api.dto.Request;
import org.jboss.pnc.konfluxbuilddriver.clients.IndyService;
import org.jboss.pnc.konfluxbuilddriver.clients.IndyTokenRequestDTO;
import org.jboss.pnc.konfluxbuilddriver.clients.IndyTokenResponseDTO;
import org.jboss.pnc.konfluxbuilddriver.dto.BuildRequest;
import org.jboss.pnc.konfluxbuilddriver.dto.BuildResponse;
import org.jboss.pnc.konfluxbuilddriver.dto.CancelRequest;
import org.jboss.pnc.konfluxbuilddriver.dto.PipelineNotification;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import com.github.tomakehurst.wiremock.WireMockServer;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import io.fabric8.tekton.client.TektonClient;
Expand All @@ -34,10 +43,13 @@
@WithKubernetesTestServer
@QuarkusTest
@QuarkusTestResource(value = LogCollectingTestResource.class, restrictToAnnotatedClass = true, initArgs = @ResourceArg(name = LogCollectingTestResource.LEVEL, value = "FINE"))
@QuarkusTestResource(WireMockExtensions.class)
public class DriverTest {

private static final String namespace = "test-namespace";

private WireMockServer wireMockServer;

@KubernetesTestServer
KubernetesServer mockServer;

Expand All @@ -60,7 +72,7 @@ public void setup() {
}

@Test
void verify() {
void cancel() {
BuildRequest request = BuildRequest.builder().namespace(namespace).podMemoryOverride("1Gi").build();
BuildResponse response = driver.create(request);

Expand All @@ -78,4 +90,19 @@ void verify() {
assertTrue(logRecords.stream().anyMatch(r -> LogCollectingTestResource.format(r)
.contains("Retrieved pipeline run-mw-pipeline--00000000-0000-0000-0000-000000000005")));
}

@Test
public void testCompleted() throws URISyntaxException {

Request request = Request.builder()
.method(Request.Method.PUT)
.header(new Request.Header(HttpHeaders.CONTENT_TYPE_STRING, MediaType.APPLICATION_JSON))
.attachment(null)
.uri(new URI(wireMockServer.baseUrl() + "/invoker"))
.build();

driver.completed(
PipelineNotification.builder().completionCallback(request).buildId("1234").status("Succeeded").build());
assertEquals(200, wireMockServer.getServeEvents().getServeEvents().getFirst().getResponse().getStatus());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package org.jboss.pnc.konfluxbuilddriver;

import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
import static com.github.tomakehurst.wiremock.client.WireMock.put;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;

import java.util.Map;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

public class WireMockExtensions implements QuarkusTestResourceLifecycleManager {
private WireMockServer wireMockServer;

@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer(wireMockConfig().notifier(new ConsoleNotifier(true)));
wireMockServer.start();

wireMockServer.stubFor(
put(urlEqualTo("/invoker"))
.withRequestBody(
equalToJson("{\"status\":\"Succeeded\",\"buildId\":\"1234\"}"))
.willReturn(aResponse()
.withStatus(200)));

return Map.of("quarkus.rest-client.wiremockextensions.url", wireMockServer.baseUrl());
}

@Override
public void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
}
}

@Override
public void inject(TestInjector testInjector) {
testInjector.injectIntoFields(wireMockServer, new TestInjector.MatchesType(WireMockServer.class));
}
}
Loading