Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2013-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nextflow.script

import groovy.transform.Canonical
import groovy.transform.CompileStatic
import groovy.transform.EqualsAndHashCode
import groovy.transform.ToString
import groovy.util.logging.Slf4j

/**
* Store the workflow platform-related metadata
*
* @author Jorge Ejarque <[email protected]>
*/
@Slf4j
@CompileStatic
@ToString(includeNames = true, includePackage = false)
@EqualsAndHashCode
class PlatformMetadata {

@Canonical
static class User {
String id
String userName
String email
String firstName
String lastName
String organization

User(Map opts) {
id = opts.id as String
userName = opts.userName as String
email = opts.email as String
firstName = opts.firstName as String
lastName = opts.lastName as String
organization = opts.organization as String
}
}

@Canonical
static class Workspace {
String id
String name
String fullName
String organization

Workspace(Map opts) {
id = opts.workspaceId as String
name = opts.workspaceName as String
fullName = opts.workspaceFullName
organization = opts.orgName as String
}
}

@Canonical
static class ComputeEnv {
String id
String name
String platform

ComputeEnv(Map opts) {
id = opts.id as String
name = opts.name as String
platform = opts.platform as String
}
}

@Canonical
static class Pipeline {
String id
String name
String revision
String commitId

Pipeline(Map opts) {
id = opts.id as String
name = opts.name as String
revision = opts.revision as String
commitId = opts.commitId as String
}
}

String workflowId
User user
Workspace workspace
ComputeEnv computeEnv
Pipeline pipeline
List labels

PlatformMetadata(String id, User user, Workspace workspace, ComputeEnv computeEnv, Pipeline pipeline, List labels) {
this.workflowId = id
this.user = user
this.workspace = workspace
this.computeEnv = computeEnv
this.pipeline = pipeline
this.labels = labels
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ class WorkflowMetadata {
*/
FusionMetadata fusion

/**
* Workflow metadata associated to the Seqera Platform execution.
*/
PlatformMetadata seqeraPlatform

/**
* The list of files that concurred to create the config object
*/
Expand Down
24 changes: 23 additions & 1 deletion modules/nf-lineage/src/main/nextflow/lineage/LinObserver.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package nextflow.lineage

import nextflow.NextflowMeta
import nextflow.extension.FilesEx
import nextflow.lineage.exception.OutputRelativePathException

import static nextflow.lineage.fs.LinPath.*
Expand Down Expand Up @@ -71,6 +73,9 @@ import nextflow.util.TestOnly
@Slf4j
@CompileStatic
class LinObserver implements TraceObserverV2 {
private static List<String> workflowMetadataPropertiesToRemove = [
"stats", "success" // End
]
private static Map<Class<? extends BaseParam>, String> taskParamToValue = [
(StdOutParam) : "stdout",
(StdInParam) : "stdin",
Expand Down Expand Up @@ -163,7 +168,8 @@ class LinObserver implements TraceObserverV2 {
session.uniqueId.toString(),
session.runName,
getNormalizedParams(session.params, normalizer),
SecretHelper.hideSecrets(session.config.deepClone()) as Map
SecretHelper.hideSecrets(session.config.deepClone()) as Map,
addOtherMetadata(normalizer)
)
final executionHash = CacheHelper.hasher(value).hash().toString()
store.save(executionHash, value)
Expand Down Expand Up @@ -474,4 +480,20 @@ class LinObserver implements TraceObserverV2 {
}
return paths
}

private Map addOtherMetadata(PathNormalizer normalizer) {
try {
def metadata = session.workflowMetadata.toMap()
.collectEntries { it.value instanceof Path ? [it.key, FilesEx.toUriString(it.value as Path) ] : [it.key, it.value] }
metadata.removeAll {it.key.toString() in workflowMetadataPropertiesToRemove }
if( metadata.containsKey("nextflow") )
metadata["nextflow"] = (metadata["nextflow"] as NextflowMeta).toJsonMap()
if( metadata.containsKey("configFiles") )
metadata["configFiles"] = (metadata["configFiles"] as List<Path>).collect {normalizer.normalizePath(it)}
return metadata
}catch( Throwable e){
log.debug("Error creating metadata", e)
return [:]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,9 @@ class WorkflowRun implements LinSerializable {
* Resolved Configuration
*/
Map config
/**
* Raw metadata
*/
Map metadata

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package nextflow.lineage

import nextflow.extension.FilesEx
import nextflow.lineage.exception.OutputRelativePathException

import java.nio.file.Files
Expand Down Expand Up @@ -165,13 +166,26 @@ class LinObserverTest extends Specification {
def store = new DefaultLinStore();
def uniqueId = UUID.randomUUID()
def scriptFile = folder.resolve("main.nf")
def map = [
repository: "https://nextflow.io/nf-test/",
commitId: "123456",
scriptId: "78910",
scriptFile: scriptFile,
projectDir: folder.resolve("projectDir"),
revision: "main",
projectName: "nextflow.io/nf-test",
workDir: folder.resolve("workDir")
]
def metadata = Mock(WorkflowMetadata){
getRepository() >> "https://nextflow.io/nf-test/"
getCommitId() >> "123456"
getScriptId() >> "78910"
getScriptFile() >> scriptFile
getProjectDir() >> folder.resolve("projectDir")
getWorkDir() >> folder.resolve("workDir")
getRepository() >> map.repository
getCommitId() >> map.commitId
getScriptId() >> map.scriptId
getScriptFile() >> map.scriptFile
getProjectDir() >> map.projectDir
getRevision() >> map.revision
getProjectName() >> map.projectName
getWorkDir() >> map.workDir
toMap() >> map
}
def session = Mock(Session) {
getConfig() >> config
Expand All @@ -183,8 +197,8 @@ class LinObserverTest extends Specification {
store.open(LineageConfig.create(session))
def observer = new LinObserver(session, store)
def mainScript = new DataPath("file://${scriptFile.toString()}", new Checksum("78910", "nextflow", "standard"))
def workflow = new Workflow([mainScript],"https://nextflow.io/nf-test/", "123456" )
def workflowRun = new WorkflowRun(workflow, uniqueId.toString(), "test_run", [], config)
def workflow = new Workflow([mainScript], map.repository, map.commitId)
def workflowRun = new WorkflowRun(workflow, uniqueId.toString(), "test_run", [], config, map)
when:
observer.onFlowCreate(session)
observer.onFlowBegin()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
/*
* Copyright 2013-2025, Seqera Labs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package io.seqera.tower.plugin

import groovy.json.JsonSlurper
Expand All @@ -11,7 +28,7 @@ import java.net.http.HttpResponse
import java.time.Duration

@Slf4j
class BaseCommandImpl {
class BaseCommandImpl extends TowerCommonApi {

private static final int API_TIMEOUT_MS = 10_000

Expand Down Expand Up @@ -42,76 +59,7 @@ class BaseCommandImpl {
return builder.buildConfigObject().flatten()
}

/**
* Calls the Seqera Platform user-info API to retrieve user information.
*
* @param accessToken Authentication token
* @param apiUrl Seqera Platform API endpoint
* @return Map containing user information (id, userName, email, etc.)
* @throws RuntimeException if the API call fails
*/
protected Map getUserInfo(String accessToken, String apiUrl) {
final client = createHttpClient(accessToken)
final url = "${apiUrl}/user-info"
log.debug "Platform get user info - GET ${url}"
final request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build()

final response = client.send(request, HttpResponse.BodyHandlers.ofString())

if (response.statusCode() != 200) {
final error = response.body() ?: "HTTP ${response.statusCode()}"
throw new RuntimeException("Failed to get user info: ${error}")
}

final json = new JsonSlurper().parseText(response.body()) as Map
return json.user as Map
}

protected Map getWorkspaceDetails(String accessToken, String endpoint, String workspaceId) {
try {
final userInfo = getUserInfo(accessToken, endpoint)
final userId = userInfo.id as String

final client = createHttpClient(accessToken)
final url = "${endpoint}/user/${userId}/workspaces"
log.debug "Platform get workdspace - GET ${url}"
final request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build()

final response = client.send(request, HttpResponse.BodyHandlers.ofString())

if (response.statusCode() != 200) {
return null
}

final json = new JsonSlurper().parseText(response.body()) as Map
final orgsAndWorkspaces = json.orgsAndWorkspaces as List

final workspace = orgsAndWorkspaces.find { ((Map)it).workspaceId?.toString() == workspaceId }
if (workspace) {
final ws = workspace as Map
return [
orgName: ws.orgName,
workspaceName: ws.workspaceName,
workspaceFullName: ws.workspaceFullName,
roles: ws.roles
]
}

return null
} catch (Exception e) {
log.debug("Failed to get workspace details for workspace ${workspaceId}: ${e.message}", e)
return null
}
}

protected List listUserWorkspaces(String accessToken, String endpoint, String userId) {
final client = createHttpClient(accessToken)
protected List listUserWorkspaces(HxClient client, String endpoint, String userId) {
final url = "${endpoint}/user/${userId}/workspaces"
log.debug "Platform list workspaces - GET ${url}"
final request = HttpRequest.newBuilder()
Expand All @@ -132,8 +80,7 @@ class BaseCommandImpl {
return orgsAndWorkspaces.findAll { ((Map) it).workspaceId != null }
}

protected List listComputeEnvironments(String accessToken, String endpoint, String workspaceId) {
final client = createHttpClient(accessToken)
protected List listComputeEnvironments(HxClient client, String endpoint, String workspaceId) {
final uri = workspaceId
? "${endpoint}/compute-envs?workspaceId=${workspaceId}"
: "${endpoint}/compute-envs"
Expand All @@ -155,8 +102,7 @@ class BaseCommandImpl {
return json.computeEnvs as List ?: []
}

protected Map getComputeEnvironment(String accessToken, String endpoint, String computeEnvId, String workspaceId) {
final client = createHttpClient(accessToken)
protected Map getComputeEnvironment(HxClient client, String endpoint, String computeEnvId, String workspaceId) {
final uri = workspaceId ?
"${endpoint}/compute-envs/${computeEnvId}?workspaceId=${workspaceId}" :
"${endpoint}/compute-envs"
Expand Down
Loading
Loading