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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,9 @@ test {
environment 'QUAY_PAT', project.findProperty('QUAY_PAT') ?: environment['QUAY_PAT']

systemProperty 'logback.configurationFile', 'src/test/resources/logback-test.xml'
// enable Basic authentication for HTTPS proxy tunnelling (CONNECT requests),
// required by `HttpClientFactoryProxyTest` - see also `HttpClientConfig.enableProxyAuthSchemes`
systemProperty 'jdk.http.auth.tunneling.disabledSchemes', ''

}

Expand Down
34 changes: 34 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,40 @@ Configure the HTTP client with the following options:
`wave.httpclient.retry.multiplier` *(optional)*
: Multiplier for HTTP client retries (default: `1.75`).

### Egress proxy

When Wave is deployed behind a corporate egress proxy, configure the proxy used by Wave's internal HTTP clients — container registry requests (authentication, manifests, blobs served by the Wave process) and Seqera Platform API requests — with the following options.

These settings apply to the connections made by the Wave service process itself. They do not apply to the Micronaut declarative HTTP clients (configurable with the standard `micronaut.http.client.*`/`micronaut.http.services.*` proxy settings), to AWS SDK clients such as ECR and S3 (configurable with the AWS SDK proxy system properties), or to build, scan, and blob-cache jobs that run as separate containers.

`wave.httpclient.proxy.uri` *(optional)*
: URI of the egress proxy in the form `[http://][username:password@]host[:port]`, for example `http://proxy.example.com:3128`.
Credentials can be embedded in the URI or provided separately with the options below.
When this option is not set, Wave falls back to the `HTTPS_PROXY`/`HTTP_PROXY` (and `NO_PROXY`) environment variables, if defined.

`wave.httpclient.proxy.username` *(optional)*
: Username to authenticate against the proxy. Takes precedence over credentials embedded in the proxy URI.

`wave.httpclient.proxy.password` *(optional)*
: Password to authenticate against the proxy.

`wave.httpclient.proxy.no-proxy` *(optional)*
: Comma-separated list of hosts that must be accessed directly, bypassing the proxy.
Entries can be exact host names (`registry.example.com`), domain suffixes (`.example.com` or `*.example.com`), IPv4 CIDR blocks (`10.0.0.0/8`), or `*` to bypass the proxy for all hosts.
Loopback addresses such as `localhost` and `127.0.0.1` always bypass the proxy, unless the proxy itself is a loopback address.

When no proxy is configured — neither via the settings above nor via environment variables — the behavior is unchanged and outbound connections are made directly.

:::note
By default, the JVM disables the `Basic` scheme for proxy authentication on HTTPS tunneling (`CONNECT`) requests via the `jdk.http.auth.tunneling.disabledSchemes` system property.
When a proxy with credentials is configured, Wave automatically defaults `jdk.http.auth.tunneling.disabledSchemes` and `jdk.http.auth.proxying.disabledSchemes` to an empty value at startup, unless they are already set.
Alternatively — for example, to guarantee that the properties are set before any outbound connection is made — set them explicitly on the JVM. For containerized deployments this can be done with the `JAVA_TOOL_OPTIONS` environment variable:

```bash
JAVA_TOOL_OPTIONS="-Djdk.http.auth.tunneling.disabledSchemes= -Djdk.http.auth.proxying.disabledSchemes="
```
:::

## Container build process

Configure how Wave builds container images and manages associated logs for monitoring, troubleshooting, and delivery with the following options:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ import javax.annotation.PostConstruct

import groovy.transform.CompileStatic
import groovy.util.logging.Slf4j
import io.micronaut.context.annotation.Context
import io.micronaut.context.annotation.Value
import io.micronaut.core.annotation.Nullable
import io.seqera.util.retry.Retryable
import jakarta.inject.Singleton
import io.seqera.wave.http.HttpClientFactory
import io.seqera.wave.http.HttpProxyConfig
/**
* Model Http Client settings
*
* @author Paolo Di Tommaso <paolo.ditommaso@gmail.com>
*/
@CompileStatic
@Singleton
@Context
@Slf4j
class HttpClientConfig implements Retryable.Config {

Expand All @@ -59,9 +61,64 @@ class HttpClientConfig implements Retryable.Config {
@Value('${wave.httpclient.streamThreshold:65536}')
private int streamThreshold

@Value('${wave.httpclient.proxy.uri}')
@Nullable
private String proxyUri

@Value('${wave.httpclient.proxy.username}')
@Nullable
private String proxyUsername

@Value('${wave.httpclient.proxy.password}')
@Nullable
private String proxyPassword

@Value('${wave.httpclient.proxy.no-proxy}')
@Nullable
private String proxyNoProxy

/**
* Resolve the egress proxy settings from the {@code wave.httpclient.proxy.*} configuration,
* falling back to the {@code HTTPS_PROXY}/{@code HTTP_PROXY}/{@code NO_PROXY} environment
* variables when no explicit setting is provided
*
* @return The resolved {@link HttpProxyConfig} or {@code null} when no proxy is defined
*/
HttpProxyConfig proxyConfig() {
return proxyUri
? HttpProxyConfig.parse(proxyUri, proxyUsername, proxyPassword, proxyNoProxy)
: HttpProxyConfig.fromEnvironment()
}

@PostConstruct
private void init() {
log.info "Http client config: connectTimeout=$connectTimeout; retryAttempts=$retryAttempts; retryDelay=$retryDelay; retryMaxDelay=$retryMaxDelay; retryMultiplier=$retryMultiplier; streamThreshold=$streamThreshold"
final proxy = proxyConfig()
HttpClientFactory.setProxyConfig(proxy)
if( proxy ) {
log.info "Http client proxy config: $proxy"
if( proxy.username )
enableProxyAuthSchemes()
}
}

/**
* By default the JDK disables the Basic scheme for proxy authentication over HTTPS
* tunnelling (CONNECT requests) via the {@code jdk.http.auth.tunneling.disabledSchemes}
* system property. When an authenticating proxy is configured, default these properties
* to empty so that Basic credentials can be sent to the proxy, unless the operator has
* already set them e.g. via {@code JAVA_TOOL_OPTIONS}
*/
static protected void enableProxyAuthSchemes() {
for( String name : List.of('jdk.http.auth.tunneling.disabledSchemes', 'jdk.http.auth.proxying.disabledSchemes') ) {
if( System.getProperty(name) == null ) {
System.setProperty(name, '')
log.info "Setting system property '$name' to empty string to enable Basic proxy authentication"
}
else {
log.debug "System property '$name' already set to '${System.getProperty(name)}'"
}
}
}

Duration getDelay() { retryDelay }
Expand Down
66 changes: 48 additions & 18 deletions src/main/groovy/io/seqera/wave/http/HttpClientFactory.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,47 @@ class HttpClientFactory {

private static HttpClient client2

private static volatile HttpProxyConfig proxyConfig


/**
* Set the egress proxy configuration to be used by the clients created by this factory.
* The proxy is resolved at bootstrap by {@link io.seqera.wave.configuration.HttpClientConfig}.
* Cached client instances are discarded so that the new settings are applied to clients
* obtained after this call
*
* @param config The {@link HttpProxyConfig} to be applied, or {@code null} to use no proxy
*/
static void setProxyConfig(HttpProxyConfig config) {
if( config == null && proxyConfig == null )
return
l1.lock()
try {
proxyConfig = config
client1 = null
}
finally {
l1.unlock()
}
l2.lock()
try {
client2 = null
}
finally {
l2.unlock()
}
}

static private HttpClient.Builder applyProxyConfig(HttpClient.Builder builder) {
final proxy = proxyConfig
if( proxy ) {
builder.proxy(proxy.proxySelector())
final auth = proxy.authenticator()
if( auth )
builder.authenticator(auth)
}
return builder
}

static HttpClient followRedirectsHttpClient() {
if( client1!=null )
Expand All @@ -56,7 +97,7 @@ class HttpClientFactory {
try {
if( client1!=null )
return client1
return client1=followRedirectsHttpClient0()
return client1=newHttpClient0(HttpClient.Redirect.NORMAL)
} finally {
l1.unlock()
}
Expand All @@ -69,7 +110,7 @@ class HttpClientFactory {
try {
if( client2!=null )
return client2
return client2=neverRedirectsHttpClient0()
return client2=newHttpClient0(HttpClient.Redirect.NEVER)
} finally {
l2.unlock()
}
Expand All @@ -79,25 +120,14 @@ class HttpClientFactory {
return followRedirectsHttpClient()
}

static private HttpClient followRedirectsHttpClient0() {
final result = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(timeout)
.executor(threadPool)
.build()
log.debug "Creating new followRedirectsHttpClient: $result"
return result
}

static private HttpClient neverRedirectsHttpClient0() {
final result = HttpClient.newBuilder()
static private HttpClient newHttpClient0(HttpClient.Redirect redirect) {
final builder = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NEVER)
.followRedirects(redirect)
.connectTimeout(timeout)
.executor(threadPool)
.build()
log.debug "Creating new neverRedirectsHttpClient: $result"
final result = applyProxyConfig(builder).build()
log.debug "Creating new httpClient with $redirect redirects policy: $result"
return result
}

Expand Down
Loading
Loading