-
Notifications
You must be signed in to change notification settings - Fork 642
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FSN-13] feat(nf-tower): Retrieve and set Fusion license tokens in a process environment #5614
base: master
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for nextflow-docs-staging canceled.
|
modules/nextflow/src/main/groovy/nextflow/fusion/FusionClient.groovy
Outdated
Show resolved
Hide resolved
bee1e99
to
6fc8fbe
Compare
@pditommaso I have refactored the implementation into
|
I agree, sorry if I may have suggested in that direction. My point was to move this logic in the tower client module
don't worry about that, use the java provider client for example nextflow/plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerXAuth.groovy Lines 56 to 62 in 6183fdf
|
Signed-off-by: Alberto Miranda <[email protected]>
6fc8fbe
to
e7b1082
Compare
FusionClient
to send fusion-related HTTP requests to Platform
@pditommaso I refactored the implementation based on our discussions. The PR is ready to review. |
/** | ||
* Get the configured Platform API endpoint: if the endpoint is not provided in the configuration, we fallback to the | ||
* environment variable `TOWER_API_ENDPOINT`. If neither is provided, we fallback to the default endpoint. | ||
* | ||
* @param opts the configuration options for Platform | ||
* @param env the applicable environment variables | ||
* @return the Platform API endpoint | ||
*/ | ||
protected static String endpoint0(Map opts, Map<String, String> env) { | ||
def result = opts.endpoint as String | ||
if (!result || result == '-') { | ||
result = env.get('TOWER_API_ENDPOINT') ?: TowerClient.DEF_ENDPOINT_URL | ||
} | ||
return result.stripEnd('/') | ||
} | ||
|
||
/** | ||
* Get the configured Platform access token: if `TOWER_WORKFLOW_ID` is provided in the environment, we are running | ||
* in a Platform-made run and we should ONLY retrieve the token from the environment. Otherwise, check | ||
* the configuration file or fallback to the environment. If no token is found, returns null. | ||
* | ||
* @param opts the configuration options for Platform | ||
* @param env the applicable environment variables | ||
* @return the Platform access token | ||
*/ | ||
protected static String accessToken0(Map opts, Map<String, String> env) { | ||
def token = env.get('TOWER_WORKFLOW_ID') | ||
? env.get('TOWER_ACCESS_TOKEN') | ||
: opts.containsKey('accessToken') ? opts.accessToken as String : env.get('TOWER_ACCESS_TOKEN') | ||
return token | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This basically replicates what TowerConfig
does in nf-wave
, but I didn't want to add a explicit dependency on that. If adding this dependency is not a problem, it would be better to reuse the code there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could make sense to turn those into PlatformHelper
class shared across modules & plugins
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. Do you want it done in the context of this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it could be better
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored with c1147cd. I created the PlatformHelper
class under modules/nextflow/src/main/groovy/nextflow/platform
since it seemed to fit with the existing conventions for common code. Please double check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, left some comments. Question, what happens when using Fusion and Tower plugin is not enabled?
plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionEnv.groovy
Outdated
Show resolved
Hide resolved
try { | ||
final token = getLicenseToken(product, version) | ||
return [ | ||
'FUSION_LICENSE_TOKEN': token, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'FUSION_LICENSE_TOKEN': token, | |
FUSION_LICENSE_TOKEN: token, |
Groovy magic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with f32f0ca
plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionEnv.groovy
Outdated
Show resolved
Hide resolved
plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionEnv.groovy
Outdated
Show resolved
Hide resolved
new Gson().toJson( | ||
new LicenseTokenRequest( | ||
product: product, | ||
version: version | ||
), | ||
LicenseTokenRequest.class | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Built-in JsonOutout.toJson(Map)
can be easier
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it this way for symmetry with the deserialization but I don't have a preference for one or the other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine keeping gson, then it would be better to isolate into its own method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done with 2f11171
/** | ||
* Get the configured Platform API endpoint: if the endpoint is not provided in the configuration, we fallback to the | ||
* environment variable `TOWER_API_ENDPOINT`. If neither is provided, we fallback to the default endpoint. | ||
* | ||
* @param opts the configuration options for Platform | ||
* @param env the applicable environment variables | ||
* @return the Platform API endpoint | ||
*/ | ||
protected static String endpoint0(Map opts, Map<String, String> env) { | ||
def result = opts.endpoint as String | ||
if (!result || result == '-') { | ||
result = env.get('TOWER_API_ENDPOINT') ?: TowerClient.DEF_ENDPOINT_URL | ||
} | ||
return result.stripEnd('/') | ||
} | ||
|
||
/** | ||
* Get the configured Platform access token: if `TOWER_WORKFLOW_ID` is provided in the environment, we are running | ||
* in a Platform-made run and we should ONLY retrieve the token from the environment. Otherwise, check | ||
* the configuration file or fallback to the environment. If no token is found, returns null. | ||
* | ||
* @param opts the configuration options for Platform | ||
* @param env the applicable environment variables | ||
* @return the Platform access token | ||
*/ | ||
protected static String accessToken0(Map opts, Map<String, String> env) { | ||
def token = env.get('TOWER_WORKFLOW_ID') | ||
? env.get('TOWER_ACCESS_TOKEN') | ||
: opts.containsKey('accessToken') ? opts.accessToken as String : env.get('TOWER_ACCESS_TOKEN') | ||
return token | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could make sense to turn those into PlatformHelper
class shared across modules & plugins
Nothing special, if |
Will Fusion be able to run? |
The license is not yet enforced in the Fusion side, so yes. A side effect we didn't consider is that if EDIT: Even if not enforced by nextflow, fusion will still fail once licenses become mandatory, but the error received by the user will probably be uglier. |
…per` Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
and slower, ie. after the job started. we should consider adding a "default" The "default" See here for example nextflow/plugins/nf-wave/src/main/io/seqera/wave/plugin/resolver/WaveContainerResolver.groovy Line 41 in 9eefd20
|
It's a great idea, but if I understand correctly the mechanism you propose, it prioritizes between implementations of a specific interface doesn't it? For our case, we need to override one implementer of |
You are right, it may need some customisation in the plugins handling. Ignore it for now. |
plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionEnv.groovy
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, left a few comments but nothing blocking
plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionEnv.groovy
Outdated
Show resolved
Hide resolved
plugins/nf-tower/src/main/io/seqera/tower/plugin/TowerFusionEnv.groovy
Outdated
Show resolved
Hide resolved
() -> { | ||
log.debug "Request: method:=${req.method()}; uri:=${req.uri()}; request:=${req}" | ||
final resp = httpClient.send(req, HttpResponse.BodyHandlers.ofString()) | ||
log.debug "Response: statusCode:=${resp.statusCode()}; body:=${resp.body()}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This response is going to contain an authentication token right? Do we want to log it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I'm not sure about what the preferred policy is in Nextflow logs (cc @pditommaso)
private static final String LICENSE_TOKEN_PATH = 'license/token/' | ||
|
||
// Server errors that should trigger a retry | ||
private static final List<Integer> SERVER_ERRORS = [429, 500, 502, 503, 504] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how I feel about retrying 503 and 504 errors, if Seqera Platform is under heavy load wouldn't this make it worse?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took this from Wave's client to follow the existing approach. I don't have a specific preference 🤷♂️ (cc @pditommaso)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why it's used an exponential backoff. Actually think it should be added 408 (connection timeout) to that list
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, consider this resolved 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error 408 added with 6ce5be3
* Send a request to Platform to obtain a license-scoped JWT for Fusion. The request is authenticated using the | ||
* Platform access token provided in the configuration of the current session. | ||
* | ||
* @throws AbortOperationException if a Platform access token cannot be found |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also throws other exceptions if the token is not valid or malformed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
Signed-off-by: Alberto Miranda <[email protected]>
As discussed this morning, I have added caching of license token responses with a9849f5 and re-scoped the license-related error messages as Having said that, I'm not at all convinced that the caching is working as it should, so I would appreciate more experienced eyes looking into it. |
|
Signed-off-by: Alberto Miranda <[email protected]>
Description
This PR adds a new
TowerFusionEnv
class tonf-tower
that enables setting Platform-specific environment variables to Nextflow processes running with Fusion enabled. The class implements theFusionEnv
interface and, as such, provides agetEnvironment()
method that returns the relevant key-value pairs.Currently, the provider is only being used to set the
FUSION_LICENSE_TOKEN
environment variable, the value of which is retrieved by sending a specific HTTP request to Platform in order to validate the run. Once the run is validated, Platform will reply with a corresponding token which will be injected into the process environment.Note
Since fusion licenses are still not mandatory, if the license cannot be validated (or there's a configuration error affecting this validation process) a warning is emitted:
Testing
This PR can be tested by following this guide.