Skip to content

Commit 498125d

Browse files
authored
Fix Fusion license JWT token refresh by reusing TowerClient
This change addresses an issue where the Fusion license validation would fail after the Tower access token was refreshed. Previously, TowerFusionToken created a separate HTTP client with its own token manager, which meant it didn't receive updated refresh tokens when the main TowerClient refreshed the access token. Since OAuth refresh tokens are single-use, the old token held by the Fusion client would become invalid, causing authentication failures. The fix modifies TowerFusionToken to reuse the shared TowerClient instance instead of creating its own HTTP client. This ensures that both workflow reporting and Fusion license validation share the same token manager and stay synchronized when tokens are refreshed.
1 parent 0989d31 commit 498125d

File tree

3 files changed

+135
-109
lines changed

3 files changed

+135
-109
lines changed

plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionToken.groovy

Lines changed: 15 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
package io.seqera.tower.plugin
22

3-
import java.net.http.HttpClient
4-
import java.net.http.HttpRequest
53
import java.time.Duration
64
import java.time.Instant
75
import java.time.temporal.ChronoUnit
8-
import java.util.concurrent.Executors
96

107
import com.google.common.cache.Cache
118
import com.google.common.cache.CacheBuilder
129
import com.google.common.util.concurrent.UncheckedExecutionException
1310
import com.google.gson.JsonSyntaxException
1411
import groovy.transform.CompileStatic
1512
import groovy.util.logging.Slf4j
16-
import io.seqera.http.HxClient
17-
import io.seqera.http.HxConfig
1813
import io.seqera.tower.plugin.exception.BadResponseException
1914
import io.seqera.tower.plugin.exception.UnauthorizedException
2015
import io.seqera.tower.plugin.exchange.GetLicenseTokenRequest
2116
import io.seqera.tower.plugin.exchange.GetLicenseTokenResponse
22-
import io.seqera.util.trace.TraceUtils
2317
import nextflow.SysEnv
2418
import nextflow.exception.AbortOperationException
2519
import nextflow.exception.ReportWarningException
@@ -28,8 +22,6 @@ import nextflow.fusion.FusionToken
2822
import nextflow.platform.PlatformHelper
2923
import nextflow.plugin.Priority
3024
import nextflow.serde.gson.GsonEncoder
31-
import nextflow.util.RetryConfig
32-
import nextflow.util.Threads
3325
import org.pf4j.Extension
3426
/**
3527
* Environment provider for Platform-specific environment variables.
@@ -45,14 +37,8 @@ class TowerFusionToken implements FusionToken {
4537
// The path relative to the Platform endpoint where license-scoped JWT tokens are obtained
4638
private static final String LICENSE_TOKEN_PATH = 'license/token/'
4739

48-
// Server errors that should trigger a retry
49-
private static final Set<Integer> SERVER_ERRORS = Set.of(408, 429, 500, 502, 503, 504)
50-
51-
// Default connection timeout for HTTP requests
52-
private static final Duration DEFAULT_CONNECTION_TIMEOUT = Duration.of(30, ChronoUnit.SECONDS)
53-
54-
// The HttpClient instance used to send requests
55-
private HxClient httpClient
40+
// The TowerClient instance used to send requests
41+
private TowerClient client
5642

5743
// Time-to-live for cached tokens
5844
private Duration tokenTTL = Duration.of(1, ChronoUnit.HOURS)
@@ -84,7 +70,7 @@ class TowerFusionToken implements FusionToken {
8470
this.refreshToken = PlatformHelper.getRefreshToken(config, env)
8571
this.workflowId = env.get('TOWER_WORKFLOW_ID')
8672
this.workspaceId = PlatformHelper.getWorkspaceId(config, env)
87-
this.httpClient = newDefaultHttpClient(accessToken, refreshToken)
73+
this.client = TowerFactory.client()
8874
}
8975

9076
protected void validateConfig() {
@@ -168,51 +154,6 @@ class TowerFusionToken implements FusionToken {
168154
* Helper methods
169155
*************************************************************************/
170156

171-
/**
172-
* Create a new HttpClient instance with default settings
173-
* @return The new HttpClient instance
174-
*/
175-
private HxClient newDefaultHttpClient(String accessToken, String refreshToken) {
176-
final refreshUrl = refreshToken ? "$endpoint/oauth/access_token" : null
177-
// the client config
178-
final config = HxConfig.newBuilder()
179-
.bearerToken(accessToken)
180-
.refreshToken(refreshToken)
181-
.refreshTokenUrl(refreshUrl)
182-
.refreshCookiePolicy(CookiePolicy.ACCEPT_ALL)
183-
.retryStatusCodes(SERVER_ERRORS)
184-
.retryConfig(RetryConfig.config())
185-
.build()
186-
// the client builder
187-
final builder = HxClient.newBuilder()
188-
.version(HttpClient.Version.HTTP_1_1)
189-
.connectTimeout(DEFAULT_CONNECTION_TIMEOUT)
190-
.config(config)
191-
// use virtual threads executor if enabled
192-
if ( Threads.useVirtual() ) {
193-
builder.executor(Executors.newVirtualThreadPerTaskExecutor())
194-
}
195-
// build and return the new client
196-
return builder.build()
197-
}
198-
199-
/**
200-
* Create a {@link HttpRequest} representing a {@link GetLicenseTokenRequest} object
201-
*
202-
* @param req The LicenseTokenRequest object
203-
* @return The resulting HttpRequest object
204-
*/
205-
private HttpRequest makeHttpRequest(GetLicenseTokenRequest req) {
206-
final gson = new GsonEncoder<GetLicenseTokenRequest>() {}
207-
final body = HttpRequest.BodyPublishers.ofString( gson.encode(req) )
208-
return HttpRequest.newBuilder()
209-
.uri(URI.create("${endpoint}/${LICENSE_TOKEN_PATH}").normalize())
210-
.header('Content-Type', 'application/json')
211-
.header('Traceparent', TraceUtils.rndTrace())
212-
.POST(body)
213-
.build()
214-
}
215-
216157
/**
217158
* Parse a JSON string into a {@link GetLicenseTokenResponse} object
218159
*
@@ -233,28 +174,20 @@ class TowerFusionToken implements FusionToken {
233174
* @return The LicenseTokenResponse object
234175
*/
235176
private GetLicenseTokenResponse sendRequest(GetLicenseTokenRequest request) {
236-
final httpReq = makeHttpRequest(request)
237-
try {
238-
final resp = httpClient.sendAsString(httpReq)
239-
if( log.isTraceEnabled() || resp.statusCode()!=200 )
240-
log.debug "Fusion license request ${httpReq} ${request}; status=${resp.statusCode()}; body: ${resp.body()}"
241-
else
242-
log.debug "Fusion license request ${httpReq}; status=${resp.statusCode()}"
243-
// check ok response
244-
if( resp.statusCode() == 200 ) {
245-
final ret = parseLicenseTokenResponse(resp.body())
246-
return ret
247-
}
248-
// check for unauthorized error
249-
if( resp.statusCode() == 401 ) {
250-
throw new UnauthorizedException("Unauthorized [401] - Verify you have provided a Seqera Platform valid access token")
251-
}
252-
// unpexted error
253-
throw new BadResponseException("Invalid response: ${httpReq.method()} ${httpReq.uri()} [${resp.statusCode()}] ${resp.body()}")
177+
final url = "${client.getEndpoint()}/${LICENSE_TOKEN_PATH}"
178+
final resp = client.sendHttpMessage(url, request.toMap())
179+
180+
if( resp.code == 200 ) {
181+
final ret = parseLicenseTokenResponse(resp.message)
182+
return ret
254183
}
255-
catch (IOException e) {
256-
throw new IllegalStateException("Unable to send request to '${httpReq.uri()}' : ${e.message}")
184+
185+
if( resp.code == 401 ) {
186+
throw new UnauthorizedException("Unauthorized [401] - Verify you have provided a Seqera Platform valid access token")
257187
}
188+
189+
throw new BadResponseException("Invalid response: ${url} [${resp.code}] ${resp.message} -- ${resp.cause}")
190+
258191
}
259192

260193
}

plugins/nf-tower/src/main/io/seqera/tower/plugin/exchange/GetLicenseTokenRequest.groovy

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,16 @@ class GetLicenseTokenRequest {
2929
* The Platform workspace ID associated with this request
3030
*/
3131
String workspaceId
32+
33+
/**
34+
* @return a Map representation of the request
35+
*/
36+
Map<String, String> toMap() {
37+
final map = new HashMap<String, String>()
38+
map.product = this.product
39+
map.version = this.version
40+
map.workflowId = this.workflowId
41+
map.workspaceId = this.workspaceId
42+
return map
43+
}
3244
}

0 commit comments

Comments
 (0)