Skip to content
Open
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
7 changes: 4 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<groovy.version>4.0.12</groovy.version>
<jsonschema.version>4.35.0</jsonschema.version>
<kubernetes.client.java.version>24.0.0</kubernetes.client.java.version>
<wiremock.version>3.13.2</wiremock.version>
</properties>

<scm>
Expand Down Expand Up @@ -205,9 +206,9 @@
</dependency>

<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>mockwebserver</artifactId>
<version>${okhttpVersion}</version>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>${wiremock.version}</version>
<scope>test</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import org.jetbrains.annotations.NotNull
import org.slf4j.LoggerFactory

import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
Expand All @@ -35,6 +36,8 @@ class HttpClientFactory {
builder.sslSocketFactory(context.socketFactory, context.trustManager)
}

builder.hostnameVerifier({ hostname, session -> true } as HostnameVerifier)

return builder.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ class JenkinsApiClient {
Config config,
@Named("jenkins") OkHttpClient client
) {
this.client = client

if (config.application.insecure) {
this.client = client.newBuilder()
.hostnameVerifier({ hostname, session -> true })
.build()
} else {
this.client = client
}
this.config = config
}

Expand Down
51 changes: 35 additions & 16 deletions src/main/groovy/com/cloudogu/gitops/okhttp/RetryInterceptor.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,65 @@ class RetryInterceptor implements Interceptor {
private int retries
private int waitPeriodInMs

// Number of retries in uncommonly high, because we might have to outlive a unexpected Jenkins restart
// Number of retries in uncommonly high, because we might have to outlive a unexpected Jenkins restart
RetryInterceptor(int retries = 180, int waitPeriodInMs = 2000) {
this.waitPeriodInMs = waitPeriodInMs
this.retries = retries
}

@Override
Response intercept(@NotNull Chain chain) throws IOException {
def i = 0;
def i = 0
Response response = null
IOException lastException = null

do {
try {
response = chain.proceed(chain.request())

if (response.code() !in getStatusCodesToRetry()) {
break
// Success or non-retriable error - return the response
return response
}

log.trace("Retry HTTP Request to {} due to status code {}", chain.request().url().toString(), response.code())
response.close()

} catch (SocketTimeoutException e) {
// fallthrough to retry
lastException = e
log.trace("Retry HTTP Request to {} due to SocketTimeoutException: {}", chain.request().url().toString(), e.message)
}
response?.close()
Thread.sleep(waitPeriodInMs)

// Wait before next retry (but not after the last attempt)
if (i < retries) {
Thread.sleep(waitPeriodInMs)
}
++i
} while(i < retries)

return response
} while(i <= retries)

// If we got here, all retries failed
if (response != null) {
// Return the last failed response
return response
} else if (lastException != null) {
// All attempts resulted in timeout - throw the last exception
throw lastException
} else {
// This should never happen, but as a safety net
throw new IOException("Request failed after ${retries} retries")
}
}

private List<Integer> getStatusCodesToRetry() {
return [
// list of codes if from curl --retry
408, // Request Timeout
429, // Too Many Requests
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504, // Gateway Timeout
// list of codes from curl --retry
408, // Request Timeout
429, // Too Many Requests
500, // Internal Server Error
502, // Bad Gateway
503, // Service Unavailable
504, // Gateway Timeout
]
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,59 +1,66 @@
package com.cloudogu.gitops.git.providers.scmmanager.api

import com.cloudogu.gitops.common.MockWebServerHttpsFactory
import com.cloudogu.gitops.config.Credentials
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.jupiter.api.AfterEach
import com.github.tomakehurst.wiremock.junit5.WireMockExtension
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension

import javax.net.ssl.SSLHandshakeException

import static com.github.tomakehurst.wiremock.client.WireMock.*
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import static groovy.test.GroovyAssert.shouldFail
import static org.assertj.core.api.Assertions.assertThat

class UsersApiTest {
private MockWebServer webServer = new MockWebServer()
private Credentials credentials = new Credentials("user", "pass")

@AfterEach
void tearDown() {
webServer.shutdown()
}
@RegisterExtension
static WireMockExtension wireMock = WireMockExtension.newInstance()
.options(wireMockConfig()
.dynamicPort()
.dynamicHttpsPort())
.build()

private Credentials credentials = new Credentials("user", "pass")

@Test
void 'allows self-signed certificates when using insecure option'() {
webServer.useHttps(MockWebServerHttpsFactory.createSocketFactory().sslSocketFactory(), false)

def api = usersApi(true)
webServer.enqueue(new MockResponse().setResponseCode(204))
wireMock.stubFor(delete(urlPathEqualTo("/scm/api/v2/users/test-user"))
.willReturn(aResponse().withStatus(204)))

def api = usersApi(true, true) // insecure=true, useHttps=true
def resp = api.delete('test-user').execute()

assertThat(resp.isSuccessful()).isTrue()
assertThat(webServer.requestCount).isEqualTo(1)
wireMock.verify(1, deleteRequestedFor(urlPathEqualTo("/scm/api/v2/users/test-user")))
}

@Test
void 'does not allow self-signed certificates by default'() {
webServer.useHttps(MockWebServerHttpsFactory.createSocketFactory().sslSocketFactory(), false)
wireMock.stubFor(delete(urlPathEqualTo("/scm/api/v2/users/test-user"))
.willReturn(aResponse().withStatus(204)))

def api = usersApi(false)
def api = usersApi(false, true) // insecure=false, useHttps=true

shouldFail(SSLHandshakeException) {
api.delete('test-user').execute()
}
assertThat(webServer.requestCount).isEqualTo(0)
}

wireMock.verify(0, deleteRequestedFor(urlPathEqualTo("/scm/api/v2/users/test-user")))
}

private UsersApi usersApi(boolean insecure) {
def client = new ScmManagerApiClient(apiBaseUrl(), credentials, insecure)
private UsersApi usersApi(boolean insecure, boolean useHttps = false) {
def client = new ScmManagerApiClient(apiBaseUrl(useHttps), credentials, insecure)
return client.usersApi()
}

private String apiBaseUrl() {
return "${webServer.url('scm')}/api/"
private String apiBaseUrl(boolean useHttps) {
if (useHttps) {
// Use the proper HTTPS port from WireMock
def httpsPort = wireMock.httpsPort
return "https://localhost:${httpsPort}/scm/api/"
} else {
return "${wireMock.baseUrl()}/scm/api/"
}
}

}
Loading