diff --git a/functionaltest-jenkins-plugin/resources/template.xml b/functionaltest-jenkins-plugin/resources/template.xml
index 92b15b9b..3784b273 100644
--- a/functionaltest-jenkins-plugin/resources/template.xml
+++ b/functionaltest-jenkins-plugin/resources/template.xml
@@ -21,6 +21,7 @@
+
diff --git a/functionaltest-jenkins-plugin/resources/templateNoFile.xml b/functionaltest-jenkins-plugin/resources/templateNoFile.xml
index 734528f7..240771ec 100644
--- a/functionaltest-jenkins-plugin/resources/templateNoFile.xml
+++ b/functionaltest-jenkins-plugin/resources/templateNoFile.xml
@@ -18,6 +18,7 @@
+
diff --git a/functionaltest-jenkins-plugin/src/main/groovy/JenkinsClient.groovy b/functionaltest-jenkins-plugin/src/main/groovy/JenkinsClient.groovy
index d8a5a3fb..a474b2b9 100644
--- a/functionaltest-jenkins-plugin/src/main/groovy/JenkinsClient.groovy
+++ b/functionaltest-jenkins-plugin/src/main/groovy/JenkinsClient.groovy
@@ -15,6 +15,63 @@ class JenkinsClient {
public static final String TEMPLATE_WITHOUT_IMAGE_NAMES = "resources/template.xml"
private final JenkinsServer jenkins
+ static class Config {
+ String imageName
+ String portalAddress
+ String token
+ Boolean policyEvalCheck
+ Boolean failOnCriticalPluginError
+ Integer readTimeoutSeconds = null
+
+ String createJobConfig() {
+ Map param = createConfigMap()
+ // parse the xml
+ String path = TEMPLATE_WITHOUT_IMAGE_NAMES
+ return createJobConfigFromPath(path, param)
+ }
+
+ String createJobConfigNoFile() {
+ Map param = createConfigMap()
+ // parse the xml
+ String path = JOB_TEMPLATE_WITH_IMAGE_NAMES
+ return createJobConfigFromPath(path, param)
+ }
+
+ //TODO(ROX-8458): add tests for pipeline
+ Map createConfigMap() {
+ Map configMap = [ // codenarc-disable UnnecessaryCast
+ command : """mkdir \$BUILD_TAG
+ cd \$BUILD_TAG
+ echo '${imageName}' >> rox_images_to_scan""",
+ portalAddress : portalAddress,
+ apiToken : token,
+ failOnPolicyEvalFailure : policyEvalCheck,
+ failOnCriticalPluginError: failOnCriticalPluginError,
+ enableTLSVerification : false,
+ imageNames : imageName,
+ ] as Map
+
+ if (readTimeoutSeconds != null) {
+ configMap.readTimeoutSeconds = readTimeoutSeconds
+ }
+
+ return configMap
+ }
+
+ @CompileStatic(TypeCheckingMode.SKIP)
+ private static String createJobConfigFromPath(String path, Map param) {
+ def parsexml = new XmlSlurper().parse(new File(path))
+ param.each { key, value ->
+ parsexml.breadthFirst().findAll { NodeChild it ->
+ if (it.name() == key) {
+ it.replaceBody value
+ }
+ }
+ }
+ return XmlUtil.serialize(parsexml)
+ }
+ }
+
JenkinsClient() {
def env = System.getenv()
String jenkinsAddress = env.getOrDefault('JENKINS_ADDRESS', "http://localhost:8080/jenkins/")
@@ -45,52 +102,4 @@ class JenkinsClient {
println result.consoleOutputText
return result.result
}
-
- static String createJobConfig(String imageName, String portalAddress, String token, Boolean policyEvalCheck,
- Boolean failOnCriticalPluginError) {
- Map param = createConfigMap(
- imageName, portalAddress, token, policyEvalCheck, failOnCriticalPluginError)
- // parse the xml
- String path = TEMPLATE_WITHOUT_IMAGE_NAMES
- return createJobConfigFromPath(path, param)
- }
-
- static String createJobConfigNoFile(String imageName, String portalAddress, String token, Boolean policyEvalCheck,
- Boolean failOnCriticalPluginError) {
- Map param = createConfigMap(
- imageName, portalAddress, token, policyEvalCheck, failOnCriticalPluginError)
- // parse the xml
- String path = JOB_TEMPLATE_WITH_IMAGE_NAMES
- return createJobConfigFromPath(path, param)
- }
-
- //TODO(ROX-8458): add tests for pipeline
- private static Map createConfigMap(String imageName, String portalAddress, String token,
- boolean policyEvalCheck,
- boolean failOnCriticalPluginError) {
- return [ // codenarc-disable UnnecessaryCast
- command : """mkdir \$BUILD_TAG
- cd \$BUILD_TAG
- echo '${imageName}' >> rox_images_to_scan""",
- portalAddress : portalAddress,
- apiToken : token,
- failOnPolicyEvalFailure : policyEvalCheck,
- failOnCriticalPluginError: failOnCriticalPluginError,
- enableTLSVerification : false,
- imageNames : imageName,
- ] as Map
- }
-
- @CompileStatic(TypeCheckingMode.SKIP)
- private static String createJobConfigFromPath(String path, Map param) {
- def parsexml = new XmlSlurper().parse(new File(path))
- param.each { key, value ->
- parsexml.breadthFirst().findAll { NodeChild it ->
- if (it.name() == key) {
- it.replaceBody value
- }
- }
- }
- return XmlUtil.serialize(parsexml)
- }
}
diff --git a/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTest.groovy b/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTest.groovy
index 4902a903..44e978f2 100644
--- a/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTest.groovy
+++ b/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTest.groovy
@@ -1,4 +1,3 @@
-import static JenkinsClient.createJobConfig
import static com.offbytwo.jenkins.model.BuildResult.FAILURE
import static com.offbytwo.jenkins.model.BuildResult.SUCCESS
import static com.stackrox.model.StorageEnforcementAction.FAIL_BUILD_ENFORCEMENT
@@ -22,6 +21,15 @@ class ImageScanningTest extends BaseSpecification {
protected static final String CENTRAL_URI = Config.roxEndpoint
protected static final String QUAY_REPO = "quay.io/openshifttest/"
+ def "Test read timeout with minimal timeout should fail"() {
+ when:
+ BuildResult status = jenkins.createAndRunJob(
+ getJobConfig("nginx-alpine:latest", false, true, 1))
+
+ then:
+ assert status == FAILURE
+ }
+
@Unroll
def "image scanning test with toggle enforcement(#imageName, #policyName, #enforcements, #endStatus)"() {
given:
@@ -90,8 +98,18 @@ class ImageScanningTest extends BaseSpecification {
"mis-spelled:lts" | false | SUCCESS
}
- String getJobConfig(String imageName, Boolean policyEvalCheck, Boolean failOnCriticalPluginError) {
- return createJobConfig(QUAY_REPO + imageName, CENTRAL_URI, token, policyEvalCheck, failOnCriticalPluginError)
+ String getJobConfig(String imageName,
+ Boolean policyEvalCheck,
+ Boolean failOnCriticalPluginError,
+ Integer readTimeoutSeconds = null) {
+ return new JenkinsClient.Config(
+ imageName: QUAY_REPO + imageName,
+ portalAddress: CENTRAL_URI,
+ token: token,
+ policyEvalCheck: policyEvalCheck,
+ failOnCriticalPluginError: failOnCriticalPluginError,
+ readTimeoutSeconds: readTimeoutSeconds)
+ .createJobConfig()
}
StoragePolicy updatePolicy(String policyName, String tag, List enforcements) {
diff --git a/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTestNoFileTest.groovy b/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTestNoFileTest.groovy
index 2de63299..00abb3d2 100644
--- a/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTestNoFileTest.groovy
+++ b/functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTestNoFileTest.groovy
@@ -1,9 +1,12 @@
-import static JenkinsClient.createJobConfigNoFile
-
class ImageScanningTestNoFileTest extends ImageScanningTest {
@Override
String getJobConfig(String imageName, Boolean policyEvalCheck, Boolean failOnCriticalPluginError) {
- String image = QUAY_REPO + imageName
- return createJobConfigNoFile(image, CENTRAL_URI, token, policyEvalCheck, failOnCriticalPluginError)
+ return new JenkinsClient.Config(
+ imageName: QUAY_REPO + imageName,
+ portalAddress: CENTRAL_URI,
+ token: token,
+ policyEvalCheck: policyEvalCheck,
+ failOnCriticalPluginError: failOnCriticalPluginError,
+ ).createJobConfigNoFile()
}
}
diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java
index dd4bf51e..8c145774 100644
--- a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java
+++ b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java
@@ -75,6 +75,8 @@ public class StackroxBuilder extends Builder implements SimpleBuildStep {
private String caCertPEM;
@DataBoundSetter
private String cluster;
+ @DataBoundSetter
+ private int readTimeoutSeconds = ApiClientFactory.DEFAULT_READ_TIMEOUT_SECONDS;
private RunConfig runConfig;
@@ -150,7 +152,7 @@ private List checkImages() throws IOException {
List results = Lists.newArrayList();
ApiClient apiClient = ApiClientFactory.newApiClient(
- getPortalAddress(), getApiToken().getPlainText(), getCaCertPEM(), getTLSValidationMode());
+ getPortalAddress(), getApiToken().getPlainText(), getCaCertPEM(), getTLSValidationMode(), getReadTimeoutSeconds());
ImageService imageService = new ImageService(apiClient);
DetectionService detectionService = new DetectionService(apiClient);
@@ -249,6 +251,16 @@ public FormValidation doCheckApiToken(@QueryParameter final String apiToken) {
}
}
+ @SuppressWarnings("unused")
+ public FormValidation doCheckReadTimeoutSeconds(@QueryParameter final int readTimeoutSeconds) {
+ Jenkins.get().checkPermission(Jenkins.ADMINISTER);
+ if (readTimeoutSeconds > 0 && readTimeoutSeconds <= 3600) {
+ return FormValidation.ok();
+ } else {
+ return FormValidation.error("Read timeout must be between 1 and 3600 seconds.");
+ }
+ }
+
@SuppressWarnings("unused")
@POST
public FormValidation doTestConnection(@QueryParameter("portalAddress") final String portalAddress, @QueryParameter("apiToken") final String apiToken,
@@ -275,7 +287,7 @@ public FormValidation doTestConnection(@QueryParameter("portalAddress") final St
}
private boolean checkRoxAuthStatus(final String portalAddress, final String apiToken, final boolean tlsVerify, final String caCertPEM) throws IOException {
- ApiClient apiClient = ApiClientFactory.newApiClient(portalAddress, apiToken, caCertPEM, validationMode(tlsVerify));
+ ApiClient apiClient = ApiClientFactory.newApiClient(portalAddress, apiToken, caCertPEM, validationMode(tlsVerify), 10);
try {
V1AuthStatus status = new AuthServiceApi(apiClient).authServiceGetAuthStatus();
return !Strings.isNullOrEmpty(status.getUserId());
diff --git a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/services/ApiClientFactory.java b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/services/ApiClientFactory.java
index 238ad7ba..d236bed4 100644
--- a/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/services/ApiClientFactory.java
+++ b/stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/services/ApiClientFactory.java
@@ -42,14 +42,15 @@ public enum StackRoxTlsValidationMode {
INSECURE_ACCEPT_ANY
}
+ public static final int DEFAULT_READ_TIMEOUT_SECONDS = 60;
private static final Duration TIMEOUT = Duration.ofSeconds(30);
- private static final Duration READ_TIMEOUT = Duration.ofMinutes(10);
private static final int MAXIMUM_CACHE_SIZE = 5; // arbitrary chosen as there are no data to support this decision
@Data
private static class CacheKey {
private final String caCert;
private final StackRoxTlsValidationMode tlsValidationMode;
+ private final int readTimeoutSeconds;
}
// It is good practice to avoid creating OkHttpClient on each request.
@@ -61,13 +62,13 @@ private static class CacheKey {
new CacheLoader() {
@Override
public OkHttpClient load(@Nonnull CacheKey key) throws IOException {
- return newHttpClient(key.caCert, key.tlsValidationMode);
+ return newHttpClient(key.caCert, key.tlsValidationMode, key.readTimeoutSeconds);
}
});
- public static ApiClient newApiClient(String basePath, String apiKey, @Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode) throws IOException {
- OkHttpClient client = getClient(tlsValidationMode, caCert);
+ public static ApiClient newApiClient(String basePath, String apiKey, @Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode, int readTimeoutSeconds) throws IOException {
+ OkHttpClient client = getClient(tlsValidationMode, caCert, readTimeoutSeconds);
ApiClient apiClient = new ApiClient(client);
apiClient.setBearerToken(apiKey);
apiClient.setBasePath(basePath);
@@ -75,16 +76,19 @@ public static ApiClient newApiClient(String basePath, String apiKey, @Nullable S
}
@Nonnull
- static OkHttpClient getClient(StackRoxTlsValidationMode tlsValidationMode, @Nullable String caCert) throws IOException {
+ static OkHttpClient getClient(StackRoxTlsValidationMode tlsValidationMode, @Nullable String caCert, int readTimeoutSeconds) throws IOException {
try {
- return CLIENT_CACHE.get(new CacheKey(caCert, tlsValidationMode));
+ return CLIENT_CACHE.get(new CacheKey(caCert, tlsValidationMode, readTimeoutSeconds));
} catch (ExecutionException e) {
throw new IOException("Could not get HTTP client from cache", e);
}
}
@Nonnull
- private static OkHttpClient newHttpClient(@Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode) throws IOException {
+ private static OkHttpClient newHttpClient(@Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode, int readTimeoutSeconds) throws IOException {
+ if (readTimeoutSeconds < 1) {
+ readTimeoutSeconds = DEFAULT_READ_TIMEOUT_SECONDS;
+ }
OkHttpClient.Builder builder;
try {
if (tlsValidationMode == INSECURE_ACCEPT_ANY) {
@@ -101,7 +105,7 @@ private static OkHttpClient newHttpClient(@Nullable String caCert, StackRoxTlsVa
}
builder.retryOnConnectionFailure(true);
builder.connectTimeout(TIMEOUT);
- builder.readTimeout(READ_TIMEOUT);
+ builder.readTimeout(Duration.ofSeconds(readTimeoutSeconds));
builder.writeTimeout(TIMEOUT);
builder.addNetworkInterceptor(new UserAgentInterceptor());
return builder.build();
diff --git a/stackrox-container-image-scanner/src/main/resources/com/stackrox/jenkins/plugins/StackroxBuilder/config.jelly b/stackrox-container-image-scanner/src/main/resources/com/stackrox/jenkins/plugins/StackroxBuilder/config.jelly
index b703eaa2..4d76cbf3 100644
--- a/stackrox-container-image-scanner/src/main/resources/com/stackrox/jenkins/plugins/StackroxBuilder/config.jelly
+++ b/stackrox-container-image-scanner/src/main/resources/com/stackrox/jenkins/plugins/StackroxBuilder/config.jelly
@@ -16,6 +16,10 @@
help="/plugin/stackrox-container-image-scanner/help/help-cluster.html">
+
+
+
+ HTTP read timeout in seconds for API requests to StackRox portal.
+ Increase this value if you experience timeout errors during image scans.
+
diff --git a/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/AbstractServiceTest.java b/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/AbstractServiceTest.java
index 8e63d06c..8eb793c9 100644
--- a/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/AbstractServiceTest.java
+++ b/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/AbstractServiceTest.java
@@ -24,7 +24,7 @@ public abstract class AbstractServiceTest {
@BeforeAll
static void setup() throws IOException {
MOCK_SERVER.start();
- client = ApiClientFactory.newApiClient(MOCK_SERVER.baseUrl(), MOCK_TOKEN.getPlainText(), "", INSECURE_ACCEPT_ANY);
+ client = ApiClientFactory.newApiClient(MOCK_SERVER.baseUrl(), MOCK_TOKEN.getPlainText(), "", INSECURE_ACCEPT_ANY, 1);
}
@AfterAll
diff --git a/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/ApiClientFactoryTest.java b/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/ApiClientFactoryTest.java
index 1355dd7b..1e9e4f6b 100644
--- a/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/ApiClientFactoryTest.java
+++ b/stackrox-container-image-scanner/src/test/java/com/stackrox/jenkins/plugins/services/ApiClientFactoryTest.java
@@ -54,7 +54,7 @@ void shouldHandleTLSOptions(ApiClientFactory.StackRoxTlsValidationMode tlsVerify
File caPemFile = Paths.get("src", "test", "resources", "cert", "localhost.pem").toFile();
String pem = useCaCert ? FileUtils.readFileToString(caPemFile, StandardCharsets.UTF_8) : null;
- OkHttpClient client = ApiClientFactory.getClient(tlsVerify, pem);
+ OkHttpClient client = ApiClientFactory.getClient(tlsVerify, pem, 1);
Request request = new Request.Builder().url(SERVER.baseUrl()).build();
Response response = client.newCall(request).execute();
@@ -65,7 +65,7 @@ void shouldHandleTLSOptions(ApiClientFactory.StackRoxTlsValidationMode tlsVerify
@Test
@DisplayName("TLS should FAIL when tlsVerify: true and custom PEM: false")
void shouldThrowWhenTLSCouldNotBeVerified() throws IOException {
- OkHttpClient client = ApiClientFactory.getClient(VALIDATE, "");
+ OkHttpClient client = ApiClientFactory.getClient(VALIDATE, "", 1);
Request request = new Request.Builder().url(SERVER.baseUrl()).build();
Exception exception = assertThrows(IOException.class, () -> client.newCall(request).execute());
@@ -81,7 +81,7 @@ void shouldThrowWhenHostIsInvalid() throws IOException {
File clientPem = Paths.get("src", "test", "resources", "cert", "client.pem").toFile();
String pem = FileUtils.readFileToString(clientPem, StandardCharsets.UTF_8);
- OkHttpClient client = ApiClientFactory.getClient(VALIDATE, pem);
+ OkHttpClient client = ApiClientFactory.getClient(VALIDATE, pem, 1);
WireMockServer server = new WireMockServer(wireMockConfig().httpDisabled(true)
.dynamicHttpsPort().keystorePath(keyStorePath).keystorePassword(KEY_STORE_PASSWORD));