Skip to content

Commit c300cac

Browse files
authored
configurable read timeout for image scanning (#452)
1 parent 00ef9ca commit c300cac

File tree

12 files changed

+126
-69
lines changed

12 files changed

+126
-69
lines changed

functionaltest-jenkins-plugin/resources/template.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<failOnPolicyEvalFailure></failOnPolicyEvalFailure>
2222
<failOnCriticalPluginError></failOnCriticalPluginError>
2323
<enableTLSVerification></enableTLSVerification>
24+
<readTimeoutSeconds></readTimeoutSeconds>
2425
</com.stackrox.jenkins.plugins.StackroxBuilder>
2526
</builders>
2627
<publishers/>

functionaltest-jenkins-plugin/resources/templateNoFile.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<failOnPolicyEvalFailure></failOnPolicyEvalFailure>
1919
<failOnCriticalPluginError></failOnCriticalPluginError>
2020
<enableTLSVerification></enableTLSVerification>
21+
<readTimeoutSeconds></readTimeoutSeconds>
2122
<imageNames></imageNames>
2223
</com.stackrox.jenkins.plugins.StackroxBuilder>
2324
</builders>

functionaltest-jenkins-plugin/src/main/groovy/JenkinsClient.groovy

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,63 @@ class JenkinsClient {
1515
public static final String TEMPLATE_WITHOUT_IMAGE_NAMES = "resources/template.xml"
1616
private final JenkinsServer jenkins
1717

18+
static class Config {
19+
String imageName
20+
String portalAddress
21+
String token
22+
Boolean policyEvalCheck
23+
Boolean failOnCriticalPluginError
24+
Integer readTimeoutSeconds = null
25+
26+
String createJobConfig() {
27+
Map<String, Serializable> param = createConfigMap()
28+
// parse the xml
29+
String path = TEMPLATE_WITHOUT_IMAGE_NAMES
30+
return createJobConfigFromPath(path, param)
31+
}
32+
33+
String createJobConfigNoFile() {
34+
Map<String, Serializable> param = createConfigMap()
35+
// parse the xml
36+
String path = JOB_TEMPLATE_WITH_IMAGE_NAMES
37+
return createJobConfigFromPath(path, param)
38+
}
39+
40+
//TODO(ROX-8458): add tests for pipeline
41+
Map<String, Serializable> createConfigMap() {
42+
Map<String, Serializable> configMap = [ // codenarc-disable UnnecessaryCast
43+
command : """mkdir \$BUILD_TAG
44+
cd \$BUILD_TAG
45+
echo '${imageName}' >> rox_images_to_scan""",
46+
portalAddress : portalAddress,
47+
apiToken : token,
48+
failOnPolicyEvalFailure : policyEvalCheck,
49+
failOnCriticalPluginError: failOnCriticalPluginError,
50+
enableTLSVerification : false,
51+
imageNames : imageName,
52+
] as Map<String, Serializable>
53+
54+
if (readTimeoutSeconds != null) {
55+
configMap.readTimeoutSeconds = readTimeoutSeconds
56+
}
57+
58+
return configMap
59+
}
60+
61+
@CompileStatic(TypeCheckingMode.SKIP)
62+
private static String createJobConfigFromPath(String path, Map<String, Serializable> param) {
63+
def parsexml = new XmlSlurper().parse(new File(path))
64+
param.each { key, value ->
65+
parsexml.breadthFirst().findAll { NodeChild it ->
66+
if (it.name() == key) {
67+
it.replaceBody value
68+
}
69+
}
70+
}
71+
return XmlUtil.serialize(parsexml)
72+
}
73+
}
74+
1875
JenkinsClient() {
1976
def env = System.getenv()
2077
String jenkinsAddress = env.getOrDefault('JENKINS_ADDRESS', "http://localhost:8080/jenkins/")
@@ -45,52 +102,4 @@ class JenkinsClient {
45102
println result.consoleOutputText
46103
return result.result
47104
}
48-
49-
static String createJobConfig(String imageName, String portalAddress, String token, Boolean policyEvalCheck,
50-
Boolean failOnCriticalPluginError) {
51-
Map<String, Serializable> param = createConfigMap(
52-
imageName, portalAddress, token, policyEvalCheck, failOnCriticalPluginError)
53-
// parse the xml
54-
String path = TEMPLATE_WITHOUT_IMAGE_NAMES
55-
return createJobConfigFromPath(path, param)
56-
}
57-
58-
static String createJobConfigNoFile(String imageName, String portalAddress, String token, Boolean policyEvalCheck,
59-
Boolean failOnCriticalPluginError) {
60-
Map<String, Serializable> param = createConfigMap(
61-
imageName, portalAddress, token, policyEvalCheck, failOnCriticalPluginError)
62-
// parse the xml
63-
String path = JOB_TEMPLATE_WITH_IMAGE_NAMES
64-
return createJobConfigFromPath(path, param)
65-
}
66-
67-
//TODO(ROX-8458): add tests for pipeline
68-
private static Map<String, Serializable> createConfigMap(String imageName, String portalAddress, String token,
69-
boolean policyEvalCheck,
70-
boolean failOnCriticalPluginError) {
71-
return [ // codenarc-disable UnnecessaryCast
72-
command : """mkdir \$BUILD_TAG
73-
cd \$BUILD_TAG
74-
echo '${imageName}' >> rox_images_to_scan""",
75-
portalAddress : portalAddress,
76-
apiToken : token,
77-
failOnPolicyEvalFailure : policyEvalCheck,
78-
failOnCriticalPluginError: failOnCriticalPluginError,
79-
enableTLSVerification : false,
80-
imageNames : imageName,
81-
] as Map<String, Serializable>
82-
}
83-
84-
@CompileStatic(TypeCheckingMode.SKIP)
85-
private static String createJobConfigFromPath(String path, Map<String, Serializable> param) {
86-
def parsexml = new XmlSlurper().parse(new File(path))
87-
param.each { key, value ->
88-
parsexml.breadthFirst().findAll { NodeChild it ->
89-
if (it.name() == key) {
90-
it.replaceBody value
91-
}
92-
}
93-
}
94-
return XmlUtil.serialize(parsexml)
95-
}
96105
}

functionaltest-jenkins-plugin/src/test/groovy/ImageScanningTest.groovy

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import static JenkinsClient.createJobConfig
21
import static com.offbytwo.jenkins.model.BuildResult.FAILURE
32
import static com.offbytwo.jenkins.model.BuildResult.SUCCESS
43
import static com.stackrox.model.StorageEnforcementAction.FAIL_BUILD_ENFORCEMENT
@@ -22,6 +21,15 @@ class ImageScanningTest extends BaseSpecification {
2221
protected static final String CENTRAL_URI = Config.roxEndpoint
2322
protected static final String QUAY_REPO = "quay.io/openshifttest/"
2423

24+
def "Test read timeout with minimal timeout should fail"() {
25+
when:
26+
BuildResult status = jenkins.createAndRunJob(
27+
getJobConfig("nginx-alpine:latest", false, true, 1))
28+
29+
then:
30+
assert status == FAILURE
31+
}
32+
2533
@Unroll
2634
def "image scanning test with toggle enforcement(#imageName, #policyName, #enforcements, #endStatus)"() {
2735
given:
@@ -90,8 +98,18 @@ class ImageScanningTest extends BaseSpecification {
9098
"mis-spelled:lts" | false | SUCCESS
9199
}
92100
93-
String getJobConfig(String imageName, Boolean policyEvalCheck, Boolean failOnCriticalPluginError) {
94-
return createJobConfig(QUAY_REPO + imageName, CENTRAL_URI, token, policyEvalCheck, failOnCriticalPluginError)
101+
String getJobConfig(String imageName,
102+
Boolean policyEvalCheck,
103+
Boolean failOnCriticalPluginError,
104+
Integer readTimeoutSeconds = null) {
105+
return new JenkinsClient.Config(
106+
imageName: QUAY_REPO + imageName,
107+
portalAddress: CENTRAL_URI,
108+
token: token,
109+
policyEvalCheck: policyEvalCheck,
110+
failOnCriticalPluginError: failOnCriticalPluginError,
111+
readTimeoutSeconds: readTimeoutSeconds)
112+
.createJobConfig()
95113
}
96114
97115
StoragePolicy updatePolicy(String policyName, String tag, List<StorageEnforcementAction> enforcements) {
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import static JenkinsClient.createJobConfigNoFile
2-
31
class ImageScanningTestNoFileTest extends ImageScanningTest {
42
@Override
53
String getJobConfig(String imageName, Boolean policyEvalCheck, Boolean failOnCriticalPluginError) {
6-
String image = QUAY_REPO + imageName
7-
return createJobConfigNoFile(image, CENTRAL_URI, token, policyEvalCheck, failOnCriticalPluginError)
4+
return new JenkinsClient.Config(
5+
imageName: QUAY_REPO + imageName,
6+
portalAddress: CENTRAL_URI,
7+
token: token,
8+
policyEvalCheck: policyEvalCheck,
9+
failOnCriticalPluginError: failOnCriticalPluginError,
10+
).createJobConfigNoFile()
811
}
912
}

stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/StackroxBuilder.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ public class StackroxBuilder extends Builder implements SimpleBuildStep {
7575
private String caCertPEM;
7676
@DataBoundSetter
7777
private String cluster;
78+
@DataBoundSetter
79+
private int readTimeoutSeconds = ApiClientFactory.DEFAULT_READ_TIMEOUT_SECONDS;
7880

7981
private RunConfig runConfig;
8082

@@ -150,7 +152,7 @@ private List<ImageCheckResults> checkImages() throws IOException {
150152
List<ImageCheckResults> results = Lists.newArrayList();
151153

152154
ApiClient apiClient = ApiClientFactory.newApiClient(
153-
getPortalAddress(), getApiToken().getPlainText(), getCaCertPEM(), getTLSValidationMode());
155+
getPortalAddress(), getApiToken().getPlainText(), getCaCertPEM(), getTLSValidationMode(), getReadTimeoutSeconds());
154156
ImageService imageService = new ImageService(apiClient);
155157
DetectionService detectionService = new DetectionService(apiClient);
156158

@@ -249,6 +251,16 @@ public FormValidation doCheckApiToken(@QueryParameter final String apiToken) {
249251
}
250252
}
251253

254+
@SuppressWarnings("unused")
255+
public FormValidation doCheckReadTimeoutSeconds(@QueryParameter final int readTimeoutSeconds) {
256+
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
257+
if (readTimeoutSeconds > 0 && readTimeoutSeconds <= 3600) {
258+
return FormValidation.ok();
259+
} else {
260+
return FormValidation.error("Read timeout must be between 1 and 3600 seconds.");
261+
}
262+
}
263+
252264
@SuppressWarnings("unused")
253265
@POST
254266
public FormValidation doTestConnection(@QueryParameter("portalAddress") final String portalAddress, @QueryParameter("apiToken") final String apiToken,
@@ -275,7 +287,7 @@ public FormValidation doTestConnection(@QueryParameter("portalAddress") final St
275287
}
276288

277289
private boolean checkRoxAuthStatus(final String portalAddress, final String apiToken, final boolean tlsVerify, final String caCertPEM) throws IOException {
278-
ApiClient apiClient = ApiClientFactory.newApiClient(portalAddress, apiToken, caCertPEM, validationMode(tlsVerify));
290+
ApiClient apiClient = ApiClientFactory.newApiClient(portalAddress, apiToken, caCertPEM, validationMode(tlsVerify), 10);
279291
try {
280292
V1AuthStatus status = new AuthServiceApi(apiClient).authServiceGetAuthStatus();
281293
return !Strings.isNullOrEmpty(status.getUserId());

stackrox-container-image-scanner/src/main/java/com/stackrox/jenkins/plugins/services/ApiClientFactory.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,15 @@ public enum StackRoxTlsValidationMode {
4242
INSECURE_ACCEPT_ANY
4343
}
4444

45+
public static final int DEFAULT_READ_TIMEOUT_SECONDS = 60;
4546
private static final Duration TIMEOUT = Duration.ofSeconds(30);
46-
private static final Duration READ_TIMEOUT = Duration.ofMinutes(10);
4747
private static final int MAXIMUM_CACHE_SIZE = 5; // arbitrary chosen as there are no data to support this decision
4848

4949
@Data
5050
private static class CacheKey {
5151
private final String caCert;
5252
private final StackRoxTlsValidationMode tlsValidationMode;
53+
private final int readTimeoutSeconds;
5354
}
5455

5556
// It is good practice to avoid creating OkHttpClient on each request.
@@ -61,30 +62,33 @@ private static class CacheKey {
6162
new CacheLoader<CacheKey, OkHttpClient>() {
6263
@Override
6364
public OkHttpClient load(@Nonnull CacheKey key) throws IOException {
64-
return newHttpClient(key.caCert, key.tlsValidationMode);
65+
return newHttpClient(key.caCert, key.tlsValidationMode, key.readTimeoutSeconds);
6566
}
6667
});
6768

6869

69-
public static ApiClient newApiClient(String basePath, String apiKey, @Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode) throws IOException {
70-
OkHttpClient client = getClient(tlsValidationMode, caCert);
70+
public static ApiClient newApiClient(String basePath, String apiKey, @Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode, int readTimeoutSeconds) throws IOException {
71+
OkHttpClient client = getClient(tlsValidationMode, caCert, readTimeoutSeconds);
7172
ApiClient apiClient = new ApiClient(client);
7273
apiClient.setBearerToken(apiKey);
7374
apiClient.setBasePath(basePath);
7475
return apiClient;
7576
}
7677

7778
@Nonnull
78-
static OkHttpClient getClient(StackRoxTlsValidationMode tlsValidationMode, @Nullable String caCert) throws IOException {
79+
static OkHttpClient getClient(StackRoxTlsValidationMode tlsValidationMode, @Nullable String caCert, int readTimeoutSeconds) throws IOException {
7980
try {
80-
return CLIENT_CACHE.get(new CacheKey(caCert, tlsValidationMode));
81+
return CLIENT_CACHE.get(new CacheKey(caCert, tlsValidationMode, readTimeoutSeconds));
8182
} catch (ExecutionException e) {
8283
throw new IOException("Could not get HTTP client from cache", e);
8384
}
8485
}
8586

8687
@Nonnull
87-
private static OkHttpClient newHttpClient(@Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode) throws IOException {
88+
private static OkHttpClient newHttpClient(@Nullable String caCert, StackRoxTlsValidationMode tlsValidationMode, int readTimeoutSeconds) throws IOException {
89+
if (readTimeoutSeconds < 1) {
90+
readTimeoutSeconds = DEFAULT_READ_TIMEOUT_SECONDS;
91+
}
8892
OkHttpClient.Builder builder;
8993
try {
9094
if (tlsValidationMode == INSECURE_ACCEPT_ANY) {
@@ -101,7 +105,7 @@ private static OkHttpClient newHttpClient(@Nullable String caCert, StackRoxTlsVa
101105
}
102106
builder.retryOnConnectionFailure(true);
103107
builder.connectTimeout(TIMEOUT);
104-
builder.readTimeout(READ_TIMEOUT);
108+
builder.readTimeout(Duration.ofSeconds(readTimeoutSeconds));
105109
builder.writeTimeout(TIMEOUT);
106110
builder.addNetworkInterceptor(new UserAgentInterceptor());
107111
return builder.build();

stackrox-container-image-scanner/src/main/resources/com/stackrox/jenkins/plugins/StackroxBuilder/config.jelly

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616
help="/plugin/stackrox-container-image-scanner/help/help-cluster.html">
1717
<f:textbox field="cluster"/>
1818
</f:entry>
19+
<f:entry title="${%ReadTimeoutTitle}"
20+
help="/plugin/stackrox-container-image-scanner/help/help-readTimeoutSeconds.html">
21+
<f:number field="readTimeoutSeconds" min="1" max="3600" step="1" default="60"/>
22+
</f:entry>
1923
<f:entry>
2024
<j:if test="${instance != null}">
2125
<f:optionalBlock title="${%EnableTLSVerification}" name="enableTLSVerification"

stackrox-container-image-scanner/src/main/resources/com/stackrox/jenkins/plugins/StackroxBuilder/config.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ EnableTLSVerification=Enable TLS verification
77
CACertificate=CA Certificate
88
ImageNames=Image Names
99
ClusterTitle=Cluster
10+
ReadTimeoutTitle=Read Timeout (seconds)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div>
2+
HTTP read timeout in seconds for API requests to StackRox portal.
3+
Increase this value if you experience timeout errors during image scans.
4+
</div>

0 commit comments

Comments
 (0)