Skip to content

Commit

Permalink
Add proxy support to Jenkins client (#1755)
Browse files Browse the repository at this point in the history
* Remove unused methods

Cleans up some unused methods

* Add proxy support to Jenkins client

Add the following new properties to the `JenkinsFactory` configuration:
* `useProxy`
* `httpProxyAddr`
* `httpProxyPort`

These properties will be used in the `HttpClient` in the `Jenkins` class
in order to support Jenkins calls through a proxy.

Testing done:
* Add unit tests which verify the proxy is used and `Host` header is
  sent
* Will deploy to nonprod environment
  • Loading branch information
osoriano authored Nov 26, 2024
1 parent e8a36ba commit 61b650e
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.pinterest.deployservice.allowlists.Allowlist;
import com.pinterest.deployservice.buildtags.BuildTagsManager;
import com.pinterest.deployservice.chat.ChatManager;
import com.pinterest.deployservice.common.Jenkins;
import com.pinterest.deployservice.dao.AgentCountDAO;
import com.pinterest.deployservice.dao.AgentDAO;
import com.pinterest.deployservice.dao.AgentErrorDAO;
Expand Down Expand Up @@ -96,8 +97,7 @@ public class ServiceContext {
private boolean deployCacheEnabled;
private String deployBoardUrlPrefix;
private String changeFeedUrl;
private String jenkinsUrl;
private String jenkinsRemoteToken;
private Jenkins jenkins;
private List<PingRequestValidator> pingRequestValidators;
private Long agentCountCacheTtl;
private Long maxParallelThreshold;
Expand Down Expand Up @@ -395,20 +395,12 @@ public void setChangeFeedUrl(String changeFeedUrl) {
this.changeFeedUrl = changeFeedUrl;
}

public String getJenkinsUrl() {
return jenkinsUrl;
public Jenkins getJenkins() {
return jenkins;
}

public void setJenkinsUrl(String jenkinsUrl) {
this.jenkinsUrl = jenkinsUrl;
}

public String getJenkinsRemoteToken() {
return jenkinsRemoteToken;
}

public void setJenkinsRemoteToken(String jenkinsRemoteToken) {
this.jenkinsRemoteToken = jenkinsRemoteToken;
public void setJenkins(Jenkins jenkins) {
this.jenkins = jenkins;
}

public TagDAO getTagDAO() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2016-2017 Pinterest, Inc.
* Copyright (c) 2016-2024 Pinterest, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,22 +18,41 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.pinterest.teletraan.universal.http.HttpClient;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// TODO: make it generic
/** Wrapper for Jenkins API calls */
public class Jenkins {
private static final Logger LOG = LoggerFactory.getLogger(Jenkins.class);
private static final HttpClient httpClient = HttpClient.builder().build();
private String jenkinsUrl;
private String jenkinsRemoteToken;
private final HttpClient httpClient;
private final String jenkinsUrl;
private final String jenkinsRemoteToken;

public Jenkins(String jenkinsUrl, String jenkinsRemoteToken) {
public Jenkins(
String jenkinsUrl,
String jenkinsRemoteToken,
boolean useProxy,
String httpProxyAddr,
String httpProxyPort) {
this.jenkinsUrl = jenkinsUrl;
this.jenkinsRemoteToken = jenkinsRemoteToken;

int httpProxyPortInt;
HttpClient.HttpClientBuilder clientBuilder = HttpClient.builder();
if (useProxy) {
try {
httpProxyPortInt = Integer.parseInt(httpProxyPort);
} catch (NumberFormatException exception) {
LOG.error("Failed to parse Jenkins port: {}", httpProxyPort, exception);
throw exception;
}
clientBuilder
.useProxy(true)
.httpProxyAddr(httpProxyAddr)
.httpProxyPort(httpProxyPortInt);
}
this.httpClient = clientBuilder.build();
}

public static class Build {
Expand Down Expand Up @@ -81,23 +100,8 @@ public int getProgress() {
}
}

public boolean isPinterestJenkinsUrl(String url) {
return url.startsWith(this.jenkinsUrl);
}

String getJenkinsToken() throws Exception {
String url = String.format("%s/%s", this.jenkinsUrl, "crumbIssuer/api/json");
String ret = httpClient.get(url, null, null);
JsonObject json = (JsonObject) JsonParser.parseString(ret);
return json.get("crumb").getAsString();
}

public void startBuild(String url) throws Exception {
String token = getJenkinsToken();
Map<String, String> headers = new HashMap<>(1);
headers.put(".crumb", token);
LOG.debug("Calling jenkins with url " + url + " and token " + token);
httpClient.post(url, null, headers);
public String getJenkinsUrl() {
return jenkinsUrl;
}

public void startBuild(String jobName, String buildParams) throws Exception {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* Copyright (c) 2024 Pinterest, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.pinterest.deployservice.common;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.google.common.collect.ImmutableList;
import com.google.gson.JsonObject;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.ServerErrorException;
import okhttp3.mockwebserver.Dispatcher;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class JenkinsTest {
private static final String TEST_JOB = "testJob";
private static final String TEST_PARAMS = "testKey1=testValue1&testKey2=testValue2";
private static final String TEST_JENKINS_HOST = "example.com";
private static final String TEST_JENKINS_URL =
String.format("http://%s/testUrl", TEST_JENKINS_HOST);
private static final String TEST_REMOTE_TOKEN = "testRemoteToken";
private static final String TEST_BUILD_NUMBER = "testBuildNumber";
private static final String TEST_BUILD_RESULT = "testBuildResult";
private static final boolean TEST_BUILD_BUILDING = true;
private static final long TEST_BUILD_TIMESTAMP = 7;
private static final int TEST_BUILD_ESTIMATED_DURATION = 8;
private static final int TEST_BUILD_DURATION = 9;

private static MockWebServer mockWebServer;
private Jenkins sut;

@BeforeEach
public void setUpEach() throws Exception {
mockWebServer = new MockWebServer();
mockWebServer.start();
sut =
new Jenkins(
TEST_JENKINS_URL,
TEST_REMOTE_TOKEN,
true,
mockWebServer.getHostName(),
String.valueOf(mockWebServer.getPort()));
}

@AfterEach
public void tearDown() throws Exception {
mockWebServer.shutdown();
}

@Test
void testConstructorInValidProxyConfig() {
assertThrows(
NumberFormatException.class,
() -> {
new Jenkins(
TEST_JENKINS_URL, TEST_REMOTE_TOKEN, true, "localhost", "invalidPort");
});
}

@Test
void testStartBuildOk() throws InterruptedException {
mockWebServer.enqueue(new MockResponse());

assertDoesNotThrow(
() -> {
sut.startBuild(TEST_JOB, TEST_PARAMS);
});

assertEquals(1, mockWebServer.getRequestCount());
RecordedRequest request = mockWebServer.takeRequest();
assertEquals(
String.format(
"GET %s/job/%s/buildWithParameters?token=%s&%s HTTP/1.1",
TEST_JENKINS_URL, TEST_JOB, TEST_REMOTE_TOKEN, TEST_PARAMS),
request.getRequestLine());
assertEquals(ImmutableList.of(TEST_JENKINS_HOST), request.getHeaders().values("Host"));
}

@Test
void testStartBuildClientError() {
mockWebServer.enqueue(new MockResponse().setResponseCode(404));

ClientErrorException exception =
assertThrows(
ClientErrorException.class,
() -> {
sut.startBuild(TEST_JOB, TEST_PARAMS);
});
assertEquals(404, exception.getResponse().getStatus());
assertEquals(1, mockWebServer.getRequestCount());
}

@Test
void testStartBuildServerError() {
mockWebServer.setDispatcher(new ServerErrorDispatcher());
ServerErrorException exception =
assertThrows(
ServerErrorException.class,
() -> {
sut.startBuild(TEST_JOB, TEST_PARAMS);
});
assertEquals(500, exception.getResponse().getStatus());
assertEquals(3, mockWebServer.getRequestCount());
}

@Test
void testGetBuildOk() throws Exception {
JsonObject mockResponseBody = new JsonObject();
mockResponseBody.addProperty("number", TEST_BUILD_NUMBER);
mockResponseBody.addProperty("result", TEST_BUILD_RESULT);
mockResponseBody.addProperty("building", TEST_BUILD_BUILDING);
mockResponseBody.addProperty("timestamp", TEST_BUILD_TIMESTAMP);
mockResponseBody.addProperty("estimatedDuration", TEST_BUILD_ESTIMATED_DURATION);
mockResponseBody.addProperty("duration", TEST_BUILD_DURATION);
mockWebServer.enqueue(new MockResponse().setBody(mockResponseBody.toString()));

Jenkins.Build build = sut.getBuild(TEST_JOB, TEST_BUILD_NUMBER);
assertEquals('"' + TEST_BUILD_NUMBER + '"', build.buildId);
assertEquals('"' + TEST_BUILD_RESULT + '"', build.result);
assertEquals(TEST_BUILD_BUILDING, build.isBuilding);
assertEquals(TEST_BUILD_TIMESTAMP, build.startTimestamp);
assertEquals(TEST_BUILD_ESTIMATED_DURATION, build.estimateDuration);
assertEquals(TEST_BUILD_DURATION, build.duration);

assertEquals(1, mockWebServer.getRequestCount());
RecordedRequest request = mockWebServer.takeRequest();
assertEquals(
String.format(
"GET %s/job/%s/%s/api/json HTTP/1.1",
TEST_JENKINS_URL, TEST_JOB, TEST_BUILD_NUMBER),
request.getRequestLine());
assertEquals(ImmutableList.of(TEST_JENKINS_HOST), request.getHeaders().values("Host"));
}

@Test
void testGetBuildClientError() {
mockWebServer.enqueue(new MockResponse().setResponseCode(404));

ClientErrorException exception =
assertThrows(
ClientErrorException.class,
() -> {
sut.getBuild(TEST_JOB, TEST_BUILD_NUMBER);
});
assertEquals(404, exception.getResponse().getStatus());
assertEquals(1, mockWebServer.getRequestCount());
}

@Test
void testGetBuildServerError() {
mockWebServer.setDispatcher(new ServerErrorDispatcher());
ServerErrorException exception =
assertThrows(
ServerErrorException.class,
() -> {
sut.getBuild(TEST_JOB, TEST_BUILD_NUMBER);
});
assertEquals(500, exception.getResponse().getStatus());
assertEquals(3, mockWebServer.getRequestCount());
}

static class ServerErrorDispatcher extends Dispatcher {
@Override
public MockResponse dispatch(RecordedRequest request) {
return new MockResponse().setResponseCode(500);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import com.pinterest.deployservice.allowlists.BuildAllowlistImpl;
import com.pinterest.deployservice.buildtags.BuildTagsManagerImpl;
import com.pinterest.deployservice.common.Jenkins;
import com.pinterest.deployservice.db.DBAgentCountDAOImpl;
import com.pinterest.deployservice.db.DBAgentDAOImpl;
import com.pinterest.deployservice.db.DBAgentErrorDAOImpl;
Expand Down Expand Up @@ -191,8 +192,13 @@ public static TeletraanServiceContext setupContext(

JenkinsFactory jenkinsFactory = configuration.getJenkinsFactory();
if (jenkinsFactory != null) {
context.setJenkinsUrl(jenkinsFactory.getJenkinsUrl());
context.setJenkinsRemoteToken(jenkinsFactory.getRemoteToken());
context.setJenkins(
new Jenkins(
jenkinsFactory.getJenkinsUrl(),
jenkinsFactory.getRemoteToken(),
jenkinsFactory.getUseProxy(),
jenkinsFactory.getHttpProxyAddr(),
jenkinsFactory.getHttpProxyPort()));
}

LOG.info("External alert factory is {}", configuration.getExternalAlertsConfigs());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2017 Pinterest, Inc.
* Copyright (c) 2024 Pinterest, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,6 +22,12 @@ public class JenkinsFactory {

@JsonProperty private String remoteToken;

@JsonProperty private boolean useProxy;

@JsonProperty private String httpProxyAddr;

@JsonProperty private String httpProxyPort;

public String getJenkinsUrl() {
return jenkinsUrl;
}
Expand All @@ -37,4 +43,28 @@ public String getRemoteToken() {
public void setRemoteToken(String remoteToken) {
this.remoteToken = remoteToken;
}

public boolean getUseProxy() {
return useProxy;
}

public void setUseProxy(boolean useProxy) {
this.useProxy = useProxy;
}

public String getHttpProxyAddr() {
return httpProxyAddr;
}

public void setHttpProxyAddr(String httpProxyAddr) {
this.httpProxyAddr = httpProxyAddr;
}

public String getHttpProxyPort() {
return httpProxyPort;
}

public void setHttpProxyPort(String httpProxyPort) {
this.httpProxyPort = httpProxyPort;
}
}
Loading

0 comments on commit 61b650e

Please sign in to comment.