Skip to content

Commit c01e56b

Browse files
committed
New nextflow auth status command
Signed-off-by: Phil Ewels <[email protected]>
1 parent 582d70b commit c01e56b

File tree

1 file changed

+176
-6
lines changed

1 file changed

+176
-6
lines changed

modules/nextflow/src/main/groovy/nextflow/cli/CmdAuth.groovy

Lines changed: 176 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class CmdAuth extends CmdBase implements UsageAware {
6666
commands.add(new LoginCmd())
6767
commands.add(new LogoutCmd())
6868
commands.add(new ConfigCmd())
69+
commands.add(new StatusCmd())
6970
}
7071

7172
void usage() {
@@ -387,7 +388,7 @@ class CmdAuth extends CmdBase implements UsageAware {
387388
<head>
388389
<style>
389390
body {
390-
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
391+
font-family: Inter, sans-serif;
391392
margin: 0;
392393
padding: 0;
393394
display: flex;
@@ -396,13 +397,16 @@ class CmdAuth extends CmdBase implements UsageAware {
396397
min-height: 100vh;
397398
}
398399
.container {
399-
text-align: center;
400400
padding: 2rem;
401401
background: white;
402-
border-radius: 8px;
403-
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
402+
border: 1px solid rgba(69, 63, 81, .2);
403+
}
404+
h1 {
405+
color: #1e293b;
406+
margin: 0 0 1rem;
407+
font-size: 21px;
408+
font-weight: 600;
404409
}
405-
h1 { color: #1e293b; margin: 0 0 0.5rem; }
406410
p { color: #64748b; margin: 0; }
407411
</style>
408412
</head>
@@ -412,7 +416,8 @@ class CmdAuth extends CmdBase implements UsageAware {
412416
<p>You can now close this window.</p>
413417
</div>
414418
</body>
415-
</html>""")
419+
</html>
420+
""")
416421
output.flush()
417422

418423
future.complete(code)
@@ -1019,4 +1024,169 @@ class CmdAuth extends CmdBase implements UsageAware {
10191024
result << ''
10201025
}
10211026
}
1027+
1028+
class StatusCmd implements SubCmd {
1029+
1030+
@Override
1031+
String getName() { 'status' }
1032+
1033+
@Override
1034+
void apply(List<String> args) {
1035+
if (args.size() > 0) {
1036+
throw new AbortOperationException("Too many arguments for status command")
1037+
}
1038+
1039+
def config = readConfig()
1040+
1041+
println "Nextflow Seqera Platform authentication status"
1042+
println ""
1043+
1044+
// API endpoint
1045+
def endpointInfo = getConfigValue(config, 'tower.endpoint', 'TOWER_API_ENDPOINT', 'https://api.cloud.seqera.io')
1046+
println "API endpoint: ${endpointInfo.value} (${endpointInfo.source})"
1047+
1048+
// API connection check
1049+
def apiConnectionOk = checkApiConnection(endpointInfo.value as String)
1050+
println "API connection check: ${apiConnectionOk ? 'OK' : 'ERROR'}"
1051+
1052+
// Access token status
1053+
def tokenInfo = getConfigValue(config, 'tower.accessToken', 'TOWER_ACCESS_TOKEN')
1054+
if (tokenInfo.value) {
1055+
println "Access token: Configured (${tokenInfo.source})"
1056+
} else {
1057+
println "Access token: Not configured"
1058+
}
1059+
1060+
// Authentication check
1061+
if (tokenInfo.value) {
1062+
try {
1063+
def userInfo = callUserInfoApi(tokenInfo.value as String, endpointInfo.value as String)
1064+
def currentUser = userInfo.userName
1065+
println "Authentication: Success (${currentUser})"
1066+
} catch (Exception e) {
1067+
println "Authentication: Error (${e})"
1068+
}
1069+
} else {
1070+
println "Authentication: ERROR (no token)"
1071+
}
1072+
1073+
// Monitoring enabled
1074+
def enabledInfo = getConfigValue(config, 'tower.enabled', null, 'false')
1075+
def enabledValue = enabledInfo.value?.toString()?.toLowerCase() in ['true', '1', 'yes'] ? 'Yes' : 'No'
1076+
println "Local workflow monitoring enabled: ${enabledValue} (${enabledInfo.source ?: 'default'})"
1077+
1078+
// Default workspace
1079+
def workspaceInfo = getConfigValue(config, 'tower.workspaceId', 'TOWER_WORKFLOW_ID')
1080+
if (workspaceInfo.value) {
1081+
// Try to get workspace name from API if we have a token
1082+
def workspaceName = null
1083+
if (tokenInfo.value) {
1084+
workspaceName = getWorkspaceNameFromApi(tokenInfo.value as String, endpointInfo.value as String, workspaceInfo.value as String)
1085+
}
1086+
1087+
if (workspaceName) {
1088+
println "Default workspace: '${workspaceName}' [${workspaceInfo.value}] (${workspaceInfo.source})"
1089+
} else {
1090+
println "Default workspace: ${workspaceInfo.value} (${workspaceInfo.source})"
1091+
}
1092+
} else {
1093+
println "Default workspace: Personal workspace (default)"
1094+
}
1095+
}
1096+
1097+
private String shortenPath(String path) {
1098+
def userHome = System.getProperty('user.home')
1099+
if (path.startsWith(userHome)) {
1100+
return '~' + path.substring(userHome.length())
1101+
}
1102+
return path
1103+
}
1104+
1105+
private Map getConfigValue(Map config, String configKey, String envVarName, String defaultValue = null) {
1106+
def configValue = config[configKey]
1107+
def envValue = envVarName ? System.getenv(envVarName) : null
1108+
def effectiveValue = configValue ?: envValue ?: defaultValue
1109+
1110+
def source = null
1111+
if (configValue) {
1112+
source = shortenPath(getConfigFile().toString())
1113+
} else if (envValue) {
1114+
source = "env var \$${envVarName}"
1115+
} else if (defaultValue) {
1116+
source = "default"
1117+
}
1118+
1119+
return [
1120+
value: effectiveValue,
1121+
source: source,
1122+
fromConfig: configValue != null,
1123+
fromEnv: envValue != null,
1124+
isDefault: !configValue && !envValue
1125+
]
1126+
}
1127+
1128+
private String getWorkspaceNameFromApi(String accessToken, String endpoint, String workspaceId) {
1129+
try {
1130+
// Get user info to get user ID
1131+
def userInfo = callUserInfoApi(accessToken, endpoint)
1132+
def userId = userInfo.id as String
1133+
1134+
// Get workspaces for the user
1135+
def workspacesUrl = "${endpoint}/user/${userId}/workspaces"
1136+
def connection = new URL(workspacesUrl).openConnection() as HttpURLConnection
1137+
connection.requestMethod = 'GET'
1138+
connection.connectTimeout = 10000 // 10 second timeout
1139+
connection.readTimeout = 10000
1140+
connection.setRequestProperty('Authorization', "Bearer ${accessToken}")
1141+
1142+
if (connection.responseCode != 200) {
1143+
return null
1144+
}
1145+
1146+
def response = connection.inputStream.text
1147+
def json = new groovy.json.JsonSlurper().parseText(response) as Map
1148+
def orgsAndWorkspaces = json.orgsAndWorkspaces as List
1149+
1150+
// Find the workspace with matching ID
1151+
def workspace = orgsAndWorkspaces.find { ((Map)it).workspaceId?.toString() == workspaceId }
1152+
if (workspace) {
1153+
def ws = workspace as Map
1154+
return "${ws.orgName} / ${ws.workspaceFullName}"
1155+
}
1156+
1157+
return null
1158+
} catch (Exception e) {
1159+
return null
1160+
}
1161+
}
1162+
1163+
private boolean checkApiConnection(String endpoint) {
1164+
try {
1165+
def serviceInfoUrl = "${endpoint}/service-info"
1166+
def connection = new URL(serviceInfoUrl).openConnection() as HttpURLConnection
1167+
connection.requestMethod = 'GET'
1168+
connection.connectTimeout = 10000 // 10 second timeout
1169+
connection.readTimeout = 10000
1170+
1171+
return connection.responseCode == 200
1172+
} catch (Exception e) {
1173+
return false
1174+
}
1175+
}
1176+
1177+
1178+
@Override
1179+
void usage(List<String> result) {
1180+
result << 'Show authentication status and configuration'
1181+
result << "Usage: nextflow auth $name".toString()
1182+
result << ''
1183+
result << 'This command shows:'
1184+
result << ' - Authentication status (yes/no) and source'
1185+
result << ' - API endpoint and source'
1186+
result << ' - Monitoring enabled status and source'
1187+
result << ' - Default workspace and source'
1188+
result << ' - System health status (API connection and authentication)'
1189+
result << ''
1190+
}
1191+
}
10221192
}

0 commit comments

Comments
 (0)