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));