diff --git a/nextflow/resources/views/begin.html b/nextflow/resources/views/begin.html deleted file mode 100644 index 9ba623a2..00000000 --- a/nextflow/resources/views/begin.html +++ /dev/null @@ -1,17 +0,0 @@ -<html> -<button id="enable-button">Enable/Disable Nextflow</button> -<button id="run-button">Nextflow Pipeline</button> -</html> - -<script type = "text/javascript" nonce="<%=scriptNonce%>"> - - $(document).ready(function() { - $('#enable-button').click(function() { - window.location = LABKEY.ActionURL.buildURL('nextflow', 'nextFlowEnable.view'); - }); - - $('#run-button').click(function() { - window.location = LABKEY.ActionURL.buildURL('nextflow', 'nextFlowRun.api'); - }); - }) -</script> \ No newline at end of file diff --git a/nextflow/resources/views/nextFlowConfiguration.html b/nextflow/resources/views/nextFlowConfiguration.html deleted file mode 100644 index fd2a895c..00000000 --- a/nextflow/resources/views/nextFlowConfiguration.html +++ /dev/null @@ -1,88 +0,0 @@ -<table> - <tr> - <td class="labkey-form-label">NextFlow Config File Path</td> - <td><input type="text" name="nextFlowConfigFilePath" size=64></td> - </tr> - <tr> - <td class="labkey-form-label">AWS Account Name</td> - <td><input type="text" name="name" size=64></td> - </tr> - <tr> - <td class="labkey-form-label">AWS Identity</td> - <td><input type="text" name="identity" size=64></td> - </tr> - <tr> - <td class="labkey-form-label">AWS S3 Bucket Path</td> - <td><input type="text" name="s3BucketPath" size=64></td> - </tr> - <tr> - <td class="labkey-form-label">AWS Credential</td> - <td><input type="password" name="credential" size=64></td> - </tr> -</table> -<button id="submit-button">Submit</button> -<button id="delete-button">Delete</button> -<button id="cancel-button">Cancel</button> - -<script type="text/javascript" nonce="<%=scriptNonce%>"> - $(document).ready(function() { - // load the current configuration on page load - LABKEY.Ajax.request({ - url: LABKEY.ActionURL.buildURL('nextflow', 'GetNextFlowConfiguration.api'), - method: 'GET', - success: function(response) { - const parsed = JSON.parse(response.responseText); - if (parsed.config) { - const response = parsed.config; - $('input[name=nextFlowConfigFilePath]').val(response.nextFlowConfigFilePath); - $('input[name=name]').val(response.accountName); - $('input[name=identity]').val(response.identity); - $('input[name=s3BucketPath]').val(response.s3BucketPath); - $('input[name=credential]').val(response.credential); - } - - }, - failure: function(response) { - alert('Failed to load configuration'); - } - }); - - $('#submit-button').click(function() { - let data = { - nextFlowConfigFilePath: $('input[name=nextFlowConfigFilePath]').val(), - accountName: $('input[name=name]').val(), - identity: $('input[name=identity]').val(), - s3BucketPath: $('input[name=s3BucketPath]').val(), - credential: $('input[name=credential]').val() - }; - LABKEY.Ajax.request({ - url: LABKEY.ActionURL.buildURL('nextflow', 'nextFlowConfiguration.api'), - method: 'POST', - params: data, - success: function(response) { - window.location = LABKEY.ActionURL.buildURL('project', 'start'); - }, - failure: function(response) { - alert('Failed to save configuration'); - } - }); - }); - - $('#delete-button').click(function() { - LABKEY.Ajax.request({ - url: LABKEY.ActionURL.buildURL('nextflow', 'DeleteNextFlowConfiguration.api'), - method: 'POST', - success: function(response) { - window.location = LABKEY.ActionURL.buildURL('project', 'start'); - }, - failure: function(response) { - alert('Failed to delete configuration'); - } - }); - }); - - $('#cancel-button').click(function() { - window.location = LABKEY.ActionURL.buildURL('project', 'start'); - }); - }); -</script> diff --git a/nextflow/resources/views/nextFlowConfiguration.view.xml b/nextflow/resources/views/nextFlowConfiguration.view.xml deleted file mode 100644 index 8a589d3c..00000000 --- a/nextflow/resources/views/nextFlowConfiguration.view.xml +++ /dev/null @@ -1,5 +0,0 @@ -<view xmlns="http://labkey.org/data/xml/view" title="NextFlow Configuration"> - <dependencies> - <dependency path="internal/jQuery"/> - </dependencies> -</view> \ No newline at end of file diff --git a/nextflow/src/org/labkey/nextflow/NextFlowConfiguration.java b/nextflow/src/org/labkey/nextflow/NextFlowConfiguration.java new file mode 100644 index 00000000..e8480095 --- /dev/null +++ b/nextflow/src/org/labkey/nextflow/NextFlowConfiguration.java @@ -0,0 +1,71 @@ +package org.labkey.nextflow; + +public class NextFlowConfiguration +{ + private String nextFlowConfigFilePath; + private String accountName; + private String identity; + private String s3BucketPath; + private String credential; + private String apiKey; + + public String getNextFlowConfigFilePath() + { + return nextFlowConfigFilePath; + } + + public void setNextFlowConfigFilePath(String nextFlowConfigFilePath) + { + this.nextFlowConfigFilePath = nextFlowConfigFilePath; + } + + public String getAccountName() + { + return accountName; + } + + public void setAccountName(String accountName) + { + this.accountName = accountName; + } + + public String getIdentity() + { + return identity; + } + + public void setIdentity(String identity) + { + this.identity = identity; + } + + public String getS3BucketPath() + { + return s3BucketPath; + } + + public void setS3BucketPath(String s3BucketPath) + { + this.s3BucketPath = s3BucketPath; + } + + public String getCredential() + { + return credential; + } + + public void setCredential(String credential) + { + this.credential = credential; + } + + public String getApiKey() + { + return apiKey; + } + + public void setApiKey(String apiKey) + { + this.apiKey = apiKey; + } +} diff --git a/nextflow/src/org/labkey/nextflow/NextFlowController.java b/nextflow/src/org/labkey/nextflow/NextFlowController.java index f22af232..b6b285d1 100644 --- a/nextflow/src/org/labkey/nextflow/NextFlowController.java +++ b/nextflow/src/org/labkey/nextflow/NextFlowController.java @@ -1,26 +1,25 @@ package org.labkey.nextflow; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.labkey.api.action.ApiResponse; import org.labkey.api.action.ApiSimpleResponse; import org.labkey.api.action.FormViewAction; import org.labkey.api.action.MutatingApiAction; -import org.labkey.api.action.ReadOnlyApiAction; +import org.labkey.api.action.SimpleViewAction; import org.labkey.api.action.SpringActionController; +import org.labkey.api.admin.AdminUrls; import org.labkey.api.data.PropertyManager; import org.labkey.api.data.PropertyStore; -import org.labkey.api.module.Module; -import org.labkey.api.module.ModuleHtmlView; -import org.labkey.api.module.ModuleLoader; import org.labkey.api.pipeline.PipeRoot; import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.pipeline.PipelineService; import org.labkey.api.pipeline.PipelineStatusUrls; import org.labkey.api.security.AdminConsoleAction; import org.labkey.api.security.RequiresPermission; -import org.labkey.api.security.SecurityManager; import org.labkey.api.security.permissions.AdminOperationsPermission; -import org.labkey.api.security.permissions.AdminPermission; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.security.permissions.ReadPermission; import org.labkey.api.security.permissions.SiteAdminPermission; import org.labkey.api.util.Button; import org.labkey.api.util.PageFlowUtil; @@ -28,20 +27,23 @@ import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ActionURL; import org.labkey.api.view.HtmlView; +import org.labkey.api.view.JspView; import org.labkey.api.view.NavTree; +import org.labkey.api.view.UnauthorizedException; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.nextflow.pipeline.NextFlowPipelineJob; import org.springframework.validation.BindException; import org.springframework.validation.Errors; import org.springframework.web.servlet.ModelAndView; -import java.util.HashSet; -import java.util.Set; - +import static org.labkey.api.util.DOM.Attribute.checked; import static org.labkey.api.util.DOM.Attribute.method; +import static org.labkey.api.util.DOM.Attribute.name; +import static org.labkey.api.util.DOM.Attribute.type; +import static org.labkey.api.util.DOM.Attribute.value; import static org.labkey.api.util.DOM.DIV; +import static org.labkey.api.util.DOM.INPUT; import static org.labkey.api.util.DOM.LK.FORM; -import static org.labkey.api.util.DOM.P; import static org.labkey.api.util.DOM.at; import static org.labkey.nextflow.NextFlowManager.NEXTFLOW_CONFIG; @@ -49,7 +51,6 @@ public class NextFlowController extends SpringActionController { private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(NextFlowController.class); public static final String NAME = "nextflow"; - private static final String IS_NEXTFLOW_ENABLED = "enabled"; private static final Logger LOG = LogHelper.getLogger(NextFlowController.class, NAME); @@ -58,22 +59,42 @@ public NextFlowController() setActionResolver(_actionResolver); } - @RequiresPermission(AdminPermission.class) - public class GetNextFlowConfigurationAction extends ReadOnlyApiAction<Object> + @RequiresPermission(ReadPermission.class) + public static class BeginAction extends SimpleViewAction<Object> { @Override - public ApiResponse execute(Object form, BindException errors) throws Exception + public ModelAndView getView(Object o, BindException errors) + { + boolean enabled = NextFlowManager.get().isEnabled(getContainer()); + return new HtmlView("NextFlow", + DIV( + DIV("NextFlow integration is " + (enabled ? "enabled" : "disabled") + " in this " + (getContainer().isProject() ? "project" : "folder") + "."), + DIV( + getContainer().hasPermission(getUser(), SiteAdminPermission.class) ? + new Button.ButtonBuilder("Enable/Disable").href(new ActionURL(NextFlowEnableAction.class, getContainer())).build() : null, + " ", + enabled && getContainer().hasPermission(getUser(), InsertPermission.class) ? + new Button.ButtonBuilder("Run NextFlow Analysis").href(new ActionURL(NextFlowRunAction.class, getContainer())).build() : null))); + } + + @Override + public void addNavTrail(NavTree root) { - return new ApiSimpleResponse("config", PropertyManager.getEncryptedStore().getProperties(NEXTFLOW_CONFIG)); + root.addChild("NextFlow"); } } - @RequiresPermission(AdminPermission.class) - public class DeleteNextFlowConfigurationAction extends MutatingApiAction<Object> + + @RequiresPermission(SiteAdminPermission.class) + public static class DeleteNextFlowConfigurationAction extends MutatingApiAction<Object> { @Override - public ApiResponse execute(Object form, BindException errors) throws Exception + public ApiResponse execute(Object form, BindException errors) { + if (!getContainer().isRoot()) + { + throw new UnauthorizedException(); + } PropertyStore store = PropertyManager.getEncryptedStore(); store.deletePropertySet(NEXTFLOW_CONFIG); return new ApiSimpleResponse("success", true); @@ -93,175 +114,151 @@ public void validateCommand(NextFlowConfiguration target, Errors errors) } @Override - public ModelAndView getView(NextFlowConfiguration nextFlowConfiguration, boolean reshow, BindException errors) throws Exception + public ModelAndView getView(NextFlowConfiguration newConfig, boolean reshow, BindException errors) { - return ModuleHtmlView.get(ModuleLoader.getInstance().getModule(NextFlowModule.class), "nextFlowConfiguration"); + NextFlowConfiguration existingConfig = NextFlowManager.get().getConfiguration(); + if (existingConfig != null) + { + if (StringUtils.isEmpty(newConfig.getNextFlowConfigFilePath())) + { + newConfig.setNextFlowConfigFilePath(existingConfig.getNextFlowConfigFilePath()); + } + if (StringUtils.isEmpty(newConfig.getAccountName())) + { + newConfig.setAccountName(existingConfig.getAccountName()); + } + if (StringUtils.isEmpty(newConfig.getIdentity())) + { + newConfig.setIdentity(existingConfig.getIdentity()); + } + if (StringUtils.isEmpty(newConfig.getCredential())) + { + newConfig.setCredential(existingConfig.getCredential()); + } + if (StringUtils.isEmpty(newConfig.getS3BucketPath())) + { + newConfig.setS3BucketPath(existingConfig.getS3BucketPath()); + } + if (StringUtils.isEmpty(newConfig.getApiKey())) + { + newConfig.setApiKey(existingConfig.getApiKey()); + } + } + + return new JspView<>("/org/labkey/nextflow/nextFlowConfiguration.jsp", newConfig, errors); } @Override - public boolean handlePost(NextFlowConfiguration nextFlowConfiguration, BindException errors) throws Exception + public boolean handlePost(NextFlowConfiguration newConfig, BindException errors) { - NextFlowManager.get().addConfiguration(nextFlowConfiguration, errors); + NextFlowConfiguration existingConfig = NextFlowManager.get().getConfiguration(); + if (existingConfig != null) + { + if (StringUtils.isEmpty(newConfig.getApiKey())) + { + newConfig.setApiKey(existingConfig.getApiKey()); + } + if (StringUtils.isEmpty(newConfig.getCredential())) + { + newConfig.setCredential(existingConfig.getCredential()); + } + } + NextFlowManager.get().saveConfig(newConfig, errors); return !errors.hasErrors(); } @Override public URLHelper getSuccessURL(NextFlowConfiguration nextFlowConfiguration) { - return getContainer().getStartURL(getUser()); + return PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL(); } @Override public void addNavTrail(NavTree root) { - + root.addChild("Admin Console", PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL()); + root.addChild("Configure NextFlow"); } } - public static class NextFlowConfiguration + public static class EnabledForm { - private String nextFlowConfigFilePath; - private String accountName; - private String identity; - private String s3BucketPath; - private String credential; - - public String getNextFlowConfigFilePath() - { - return nextFlowConfigFilePath; - } - - public void setNextFlowConfigFilePath(String nextFlowConfigFilePath) - { - this.nextFlowConfigFilePath = nextFlowConfigFilePath; - } + Boolean _enabled; - public String getAccountName() + public Boolean getEnabled() { - return accountName; + return _enabled; } - public void setAccountName(String accountName) + public void setEnabled(Boolean enabled) { - this.accountName = accountName; - } - - public String getIdentity() - { - return identity; - } - - public void setIdentity(String identity) - { - this.identity = identity; - } - - public String getS3BucketPath() - { - return s3BucketPath; - } - - public void setS3BucketPath(String s3BucketPath) - { - this.s3BucketPath = s3BucketPath; - } - - public String getCredential() - { - return credential; - } - - public void setCredential(String credential) - { - this.credential = credential; + _enabled = enabled; } } @RequiresPermission(SiteAdminPermission.class) - public static class NextFlowEnableAction extends FormViewAction + public static class NextFlowEnableAction extends FormViewAction<EnabledForm> { - @Override - public void validateCommand(Object target, Errors errors) + public void validateCommand(EnabledForm target, Errors errors) { } @Override - public ModelAndView getView(Object form, boolean reshow, BindException errors) throws Exception + public ModelAndView getView(EnabledForm form, boolean reshow, BindException errors) { - PropertyStore store = PropertyManager.getNormalStore(); - PropertyManager.PropertyMap map = store.getProperties(NextFlowManager.NEXTFLOW_ENABLE); - String btnTxt = "Enable NextFlow"; - // check if nextflow is enabled - if (Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED))) - { - btnTxt = "Disable NextFlow"; - } - else - { - btnTxt = "Enable NextFlow"; - } + Boolean status = NextFlowManager.get().getEnabledState(getContainer()); + boolean inheritedStatus = NextFlowManager.get().isEnabled(getContainer().getParent()); - return new HtmlView("Enable/Disable Nextflow", DIV( P("NextFlow is currently " + (Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)) ? "enabled" : "disabled")), + return new HtmlView("Enable/Disable NextFlow", FORM(at(method, "POST"), - new Button.ButtonBuilder(btnTxt).submit(true).build()))); + DIV(INPUT(at(type, "radio", name, "enabled", value, Boolean.TRUE.toString(), (status == Boolean.TRUE ? checked : null), null)), + "Enabled"), + DIV(INPUT(at(type, "radio", name, "enabled", value, Boolean.FALSE.toString(), (status == Boolean.FALSE ? checked : null), null)), + "Disabled"), + DIV(INPUT(at(type, "radio", name, "enabled", value, "", (status == null ? checked : null), null)), + getContainer().isRoot() ? + "Unset" : + "Inherited from " + getContainer().getParent().getPath() + " (currently " + (inheritedStatus ? "enabled" : "disabled") + ")"), + new Button.ButtonBuilder("Save").submit(true).build(), " ", + new Button.ButtonBuilder("Cancel").href(getContainer().getStartURL(getUser())).build())); } @Override - public boolean handlePost(Object form, BindException errors) throws Exception + public boolean handlePost(EnabledForm form, BindException errors) { - PropertyStore store = PropertyManager.getNormalStore(); - PropertyManager.WritablePropertyMap map = store.getWritableProperties(NextFlowManager.NEXTFLOW_ENABLE, true); - if (map.isEmpty()) - { - map.put(IS_NEXTFLOW_ENABLED, Boolean.TRUE.toString()); - } - else - { - if (Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED))) - { - map.put(IS_NEXTFLOW_ENABLED, Boolean.FALSE.toString()); - } - else - { - map.put(IS_NEXTFLOW_ENABLED, Boolean.TRUE.toString()); - } - } - map.save(); + NextFlowManager.get().saveEnabledState(getContainer(), form.getEnabled()); return true; } @Override public void addNavTrail(NavTree root) { - + root.addChild("Enable/Disable NextFlow"); } @Override - public URLHelper getSuccessURL(Object o) + public URLHelper getSuccessURL(EnabledForm o) { return getContainer().getStartURL(getUser()); } } @RequiresPermission(AdminOperationsPermission.class) - public class NextFlowRunAction extends FormViewAction + public class NextFlowRunAction extends FormViewAction<Object> { - private ActionURL _successURL; @Override public void validateCommand(Object o, Errors errors) { - PropertyStore store = PropertyManager.getNormalStore(); - PropertyManager.PropertyMap map = store.getProperties(NextFlowManager.NEXTFLOW_ENABLE); - if (!Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED))) + if (!NextFlowManager.get().isEnabled(getContainer())) { errors.reject(ERROR_MSG, "NextFlow is not enabled"); } } @Override - public ModelAndView getView(Object o, boolean b, BindException errors) throws Exception + public ModelAndView getView(Object o, boolean b, BindException errors) { return new HtmlView("NextFlow Runner", DIV("Run NextFlow Pipeline", FORM(at(method, "POST"), @@ -271,24 +268,10 @@ public ModelAndView getView(Object o, boolean b, BindException errors) throws Ex @Override public boolean handlePost(Object o, BindException errors) throws Exception { - // check if nextflow is enabled - PropertyStore store = PropertyManager.getNormalStore(); - PropertyManager.PropertyMap map = store.getProperties(NextFlowManager.NEXTFLOW_ENABLE); - if (map == null || !Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED))) - { - errors.reject(ERROR_MSG, "NextFlow is not enabled"); - return false; - } - - try (SecurityManager.TransformSession session = SecurityManager.createTransformSession(getViewContext())) - { - // TODO: pass the apiKey to Nextflow job - String apiKey = session.getApiKey(); - ViewBackgroundInfo info = getViewBackgroundInfo(); - PipeRoot root = PipelineService.get().findPipelineRoot(info.getContainer()); - PipelineJob job = new NextFlowPipelineJob(info, root, apiKey); - PipelineService.get().queueJob(job); - } + ViewBackgroundInfo info = getViewBackgroundInfo(); + PipeRoot root = PipelineService.get().findPipelineRoot(info.getContainer()); + PipelineJob job = new NextFlowPipelineJob(info, root); + PipelineService.get().queueJob(job); return !errors.hasErrors(); } diff --git a/nextflow/src/org/labkey/nextflow/NextFlowManager.java b/nextflow/src/org/labkey/nextflow/NextFlowManager.java index 23684b32..485380d7 100644 --- a/nextflow/src/org/labkey/nextflow/NextFlowManager.java +++ b/nextflow/src/org/labkey/nextflow/NextFlowManager.java @@ -1,10 +1,10 @@ package org.labkey.nextflow; import org.apache.commons.lang3.StringUtils; +import org.labkey.api.data.Container; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DbScope; import org.labkey.api.data.PropertyManager; -import org.labkey.api.data.PropertyStore; import org.springframework.validation.BindException; import java.util.HashMap; @@ -15,21 +15,18 @@ public class NextFlowManager { public static final String NEXTFLOW_CONFIG = "nextflow-config"; - public static final String NEXTFLOW_ENABLE = "nextflow-enable"; + private static final String NEXTFLOW_ENABLE_PROP_CATEGORY = "nextflow-enable"; private static final String NEXTFLOW_ACCOUNT_NAME = "accountName"; private static final String NEXTFLOW_CONFIG_FILE_PATH = "nextFlowConfigFilePath"; private static final String NEXTFLOW_IDENTITY = "identity"; private static final String NEXTFLOW_CREDENTIAL = "credential"; private static final String NEXTFLOW_S3_BUCKET_PATH = "s3BucketPath"; + private static final String NEXTFLOW_API_KEY = "apiKey"; - private static final NextFlowManager _instance = new NextFlowManager(); - - // Normal store is used for enabled/disabled module - private static final PropertyStore _normalStore = PropertyManager.getNormalStore(); + private static final String IS_NEXTFLOW_ENABLED = "enabled"; - // Encrypted store is used for aws settings & nextflow file configuration - private static final PropertyStore _encryptedStore = PropertyManager.getEncryptedStore(); + private static final NextFlowManager _instance = new NextFlowManager(); private NextFlowManager() { @@ -42,48 +39,83 @@ public static NextFlowManager get() } - private void checkArgs(String nextFlowConfigFilePath, String name, String identity, String credential,String s3BucketPath, BindException errors) + private void checkArgs(NextFlowConfiguration config, BindException errors) { - if (StringUtils.isEmpty(nextFlowConfigFilePath)) + if (StringUtils.isEmpty(config.getNextFlowConfigFilePath())) errors.rejectValue("nextFlowConfigFilePath", ERROR_MSG, "NextFlow config file path is required"); - if (StringUtils.isEmpty(name)) - errors.rejectValue("name", ERROR_MSG, "AWS account name is required"); - - if (StringUtils.isEmpty(identity)) - errors.rejectValue("identity", ERROR_MSG, "AWS identity is required"); - - if (StringUtils.isEmpty(credential)) - errors.rejectValue("credential", ERROR_MSG, "AWS credential is required"); + // Not yet used +// if (StringUtils.isEmpty(config.getAccountName())) +// errors.rejectValue("accountName", ERROR_MSG, "AWS account name is required"); +// if (StringUtils.isEmpty(config.getIdentity())) +// errors.rejectValue("identity", ERROR_MSG, "AWS identity is required"); +// if (StringUtils.isEmpty(config.getCredential())) +// errors.rejectValue("credential", ERROR_MSG, "AWS credential is required"); + if (StringUtils.isEmpty(config.getS3BucketPath())) + errors.rejectValue("credential", ERROR_MSG, "S3 bucket path is required"); } - public NextFlowController.NextFlowConfiguration getConfiguration() + public NextFlowConfiguration getConfiguration() { - PropertyManager.PropertyMap props = _encryptedStore.getWritableProperties(NEXTFLOW_CONFIG, false); + PropertyManager.PropertyMap props = PropertyManager.getEncryptedStore().getWritableProperties(NEXTFLOW_CONFIG, false); if (props != null) { - NextFlowController.NextFlowConfiguration configuration = new NextFlowController.NextFlowConfiguration(); + NextFlowConfiguration configuration = new NextFlowConfiguration(); configuration.setAccountName(props.get(NEXTFLOW_ACCOUNT_NAME)); configuration.setNextFlowConfigFilePath(props.get(NEXTFLOW_CONFIG_FILE_PATH)); configuration.setIdentity(props.get(NEXTFLOW_IDENTITY)); configuration.setCredential(props.get(NEXTFLOW_CREDENTIAL)); configuration.setS3BucketPath(props.get(NEXTFLOW_S3_BUCKET_PATH)); + configuration.setApiKey(props.get(NEXTFLOW_API_KEY)); return configuration; } return null; } - public void addConfiguration(NextFlowController.NextFlowConfiguration configuration, BindException errors) + /** + * Checks in the specified container and traverses up the container tree to determine if NextFlow integration + * is enabled directly or in a parent container. + */ + public boolean isEnabled(Container c) { - checkArgs(configuration.getNextFlowConfigFilePath(), configuration.getAccountName(), configuration.getIdentity(), configuration.getCredential(), configuration.getS3BucketPath(), errors); + do + { + PropertyManager.PropertyMap map = PropertyManager.getProperties(c, NEXTFLOW_ENABLE_PROP_CATEGORY); + if (map.containsKey(IS_NEXTFLOW_ENABLED)) + { + return Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)); + } + c = c.getParent(); + } + while (c != null); + + return false; + } + + /** + * @return configured state for the container (or null if not configured there), for whether NextFlow is enabled + */ + public Boolean getEnabledState(Container c) + { + PropertyManager.PropertyMap map = PropertyManager.getProperties(c, NEXTFLOW_ENABLE_PROP_CATEGORY); + if (map.containsKey(IS_NEXTFLOW_ENABLED)) + { + return Boolean.parseBoolean(map.get(IS_NEXTFLOW_ENABLED)); + } + return null; + } + + public void saveConfig(NextFlowConfiguration configuration, BindException errors) + { + checkArgs(configuration, errors); if (!errors.hasErrors()) saveConfiguration(configuration); } - private void saveConfiguration( NextFlowController.NextFlowConfiguration configuration) + private void saveConfiguration( NextFlowConfiguration configuration) { try (DbScope.Transaction tx = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) { @@ -93,8 +125,9 @@ private void saveConfiguration( NextFlowController.NextFlowConfiguration configu properties.put(NEXTFLOW_CREDENTIAL, configuration.getCredential()); properties.put(NEXTFLOW_S3_BUCKET_PATH, configuration.getS3BucketPath()); properties.put(NEXTFLOW_ACCOUNT_NAME, configuration.getAccountName()); + properties.put(NEXTFLOW_API_KEY, configuration.getApiKey()); - PropertyManager.WritablePropertyMap props = _encryptedStore.getWritableProperties(NEXTFLOW_CONFIG, true); + PropertyManager.WritablePropertyMap props = PropertyManager.getEncryptedStore().getWritableProperties(NEXTFLOW_CONFIG, true); props.clear(); props.putAll(properties); props.save(); @@ -103,4 +136,17 @@ private void saveConfiguration( NextFlowController.NextFlowConfiguration configu } } + public void saveEnabledState(Container container, Boolean enabled) + { + PropertyManager.WritablePropertyMap map = PropertyManager.getWritableProperties(container, NEXTFLOW_ENABLE_PROP_CATEGORY, true); + if (enabled == null) + { + map.delete(); + } + else + { + map.put(IS_NEXTFLOW_ENABLED, enabled.toString()); + map.save(); + } + } } diff --git a/nextflow/src/org/labkey/nextflow/NextFlowModule.java b/nextflow/src/org/labkey/nextflow/NextFlowModule.java index a962764d..68b35cd0 100644 --- a/nextflow/src/org/labkey/nextflow/NextFlowModule.java +++ b/nextflow/src/org/labkey/nextflow/NextFlowModule.java @@ -1,14 +1,15 @@ package org.labkey.nextflow; import org.jetbrains.annotations.NotNull; -import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; import org.labkey.api.module.ModuleContext; import org.labkey.api.module.SpringModule; +import org.labkey.api.pipeline.PipelineService; import org.labkey.api.security.permissions.AdminPermission; import org.labkey.api.settings.AdminConsole; import org.labkey.api.view.ActionURL; import org.labkey.api.view.WebPartFactory; +import org.labkey.nextflow.pipeline.NextFlowPipelineProvider; import java.util.Collection; import java.util.List; @@ -26,6 +27,8 @@ protected void startupAfterSpringConfig(ModuleContext moduleContext) protected void init() { addController(NextFlowController.NAME, NextFlowController.class); + + PipelineService.get().registerPipelineProvider(new NextFlowPipelineProvider(this)); } @Override diff --git a/nextflow/src/org/labkey/nextflow/nextFlowConfiguration.jsp b/nextflow/src/org/labkey/nextflow/nextFlowConfiguration.jsp new file mode 100644 index 00000000..f2f738b6 --- /dev/null +++ b/nextflow/src/org/labkey/nextflow/nextFlowConfiguration.jsp @@ -0,0 +1,59 @@ +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.nextflow.NextFlowConfiguration" %> +<%@ page import="org.labkey.api.util.Button" %> +<%@ page import="org.labkey.api.util.PageFlowUtil" %> +<%@ page import="org.labkey.api.admin.AdminUrls" %> +<%@ page import="org.labkey.api.security.permissions.AdminOperationsPermission" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + NextFlowConfiguration form = (NextFlowConfiguration) HttpView.currentModel(); + boolean hasAdminOpsPerms = getContainer().hasPermission(getUser(), AdminOperationsPermission.class); +%> +<labkey:form method="POST"> +<labkey:errors /> +<table class="lk-fields-table"> + <tr> + <td class="labkey-form-label"><label for="nextFlowConfigFilePath">NextFlow Config File Path</label></td> + <td><labkey:input type="text" name="nextFlowConfigFilePath" id="nextFlowConfigFilePath" size="64" value="<%=h(form.getNextFlowConfigFilePath())%>" /></td> + </tr> + <tr> + <td class="labkey-form-label"><label for="accountNameInput">AWS Account Name</label></td> + <td><labkey:input type="text" name="accountName" id="accountNameInput" size="64" value="<%=h(form.getAccountName())%>" /></td> + </tr> + <tr> + <td class="labkey-form-label"><label for="identityInput">AWS Identity</label></td> + <td><labkey:input type="text" name="identity" id="identityInput" size="64" value="<%= h(form.getIdentity()) %>" /></td> + </tr> + <tr> + <td class="labkey-form-label"><label for="credentialInput">AWS Credential</label></td> + <td><labkey:input type="password" name="credential" id="credentialInput" size="64" placeholder='<%= form.getCredential() != null ? "value already set, overwrite to replace" : "" %>' /></td> + </tr> + <tr> + <td class="labkey-form-label"><label for="s3BucketPathInput">AWS S3 Bucket Path</label></td> + <td><labkey:input type="text" name="s3BucketPath" id="s3BucketPathInput" size="64" value="<%=h(form.getS3BucketPath())%>" /></td> + </tr> + <tr> + <td class="labkey-form-label"><label for="apiKeyInput">API Key (optional)</label></td> + <td><labkey:input type="password" name="apiKey" id="apiKeyInput" size="64" placeholder='<%= form.getApiKey() != null ? "value already set, overwrite to replace" : "" %>' /></td> + </tr> +</table> + <%= new Button.ButtonBuilder("Save").submit(true).primary(true).enabled(hasAdminOpsPerms) %> + <%= new Button.ButtonBuilder("Delete").onClick("deleteConfig()").enabled(hasAdminOpsPerms) %> + <%= new Button.ButtonBuilder("Cancel").href(PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL()) %> +</labkey:form> + +<script type="text/javascript" nonce="<%=getScriptNonce()%>"> + function deleteConfig() { + LABKEY.Ajax.request({ + url: LABKEY.ActionURL.buildURL('nextflow', 'DeleteNextFlowConfiguration.api'), + method: 'POST', + success: function (response) { + window.location = <%= q(PageFlowUtil.urlProvider(AdminUrls.class).getAdminConsoleURL()) %> + }, + failure: function (response) { + alert('Failed to delete configuration'); + } + }); + } +</script> diff --git a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java index 42c2882f..92d952b5 100644 --- a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java +++ b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineJob.java @@ -1,7 +1,6 @@ package org.labkey.nextflow.pipeline; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.labkey.api.pipeline.PipeRoot; import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.pipeline.PipelineJobService; @@ -9,32 +8,23 @@ import org.labkey.api.pipeline.TaskPipeline; import org.labkey.api.util.FileUtil; import org.labkey.api.util.URLHelper; -import org.labkey.api.util.UnexpectedException; import org.labkey.api.view.ViewBackgroundInfo; import java.io.File; -import java.io.IOException; public class NextFlowPipelineJob extends PipelineJob { - private String _apiKey; - // For serialization + @SuppressWarnings("unused") // For serialization protected NextFlowPipelineJob() {} - public NextFlowPipelineJob(ViewBackgroundInfo info, @NotNull PipeRoot root, String apiKey) + public NextFlowPipelineJob(ViewBackgroundInfo info, @NotNull PipeRoot root) { super(null, info, root); - this._apiKey = apiKey; setLogFile(new File(String.valueOf(root.getLogDirectory()), FileUtil.makeFileNameWithTimestamp("NextFlowPipelineJob", "log")).toPath()); } - public String getApiKey() - { - return _apiKey; - } - - @Override + @Override public URLHelper getStatusHref() { return null; @@ -47,7 +37,7 @@ public String getDescription() } @Override - public TaskPipeline getTaskPipeline() + public TaskPipeline<?> getTaskPipeline() { return PipelineJobService.get().getTaskPipeline(new TaskId(NextFlowPipelineJob.class)); } diff --git a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineProvider.java b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineProvider.java new file mode 100644 index 00000000..449a3154 --- /dev/null +++ b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowPipelineProvider.java @@ -0,0 +1,29 @@ +package org.labkey.nextflow.pipeline; + +import org.labkey.api.module.Module; +import org.labkey.api.pipeline.PipeRoot; +import org.labkey.api.pipeline.PipelineDirectory; +import org.labkey.api.pipeline.PipelineProvider; +import org.labkey.api.security.permissions.InsertPermission; +import org.labkey.api.view.ViewContext; +import org.labkey.nextflow.NextFlowManager; +import org.labkey.nextflow.NextFlowModule; + +public class NextFlowPipelineProvider extends PipelineProvider +{ + public NextFlowPipelineProvider(NextFlowModule owningModule) + { + super("NextFlow", owningModule); + } + + @Override + public void updateFileProperties(ViewContext context, PipeRoot pr, PipelineDirectory directory, boolean includeAll) + { + if (!context.getContainer().hasPermission(context.getUser(), InsertPermission.class)) + return; + if (!NextFlowManager.get().isEnabled(context.getContainer())) + return; + } + + +} diff --git a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java index a75312a3..2934ea2e 100644 --- a/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java +++ b/nextflow/src/org/labkey/nextflow/pipeline/NextFlowRunTask.java @@ -2,19 +2,24 @@ import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.NotNull; -import org.labkey.api.files.FileContentService; import org.labkey.api.pipeline.AbstractTaskFactory; import org.labkey.api.pipeline.AbstractTaskFactorySettings; import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.pipeline.PipelineJobException; -import org.labkey.api.pipeline.PipelineJobService; import org.labkey.api.pipeline.RecordedActionSet; +import org.labkey.api.security.SecurityManager; import org.labkey.api.util.FileType; -import org.labkey.api.util.FileUtil; -import org.labkey.nextflow.NextFlowController; +import org.labkey.nextflow.NextFlowConfiguration; import org.labkey.nextflow.NextFlowManager; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -29,19 +34,109 @@ public NextFlowRunTask(Factory factory, PipelineJob job) public @NotNull RecordedActionSet run() throws PipelineJobException { Logger log = getJob().getLogger(); - NextFlowController.NextFlowConfiguration config = NextFlowManager.get().getConfiguration(); + + SecurityManager.TransformSession session = null; + + try + { + NextFlowConfiguration config = NextFlowManager.get().getConfiguration(); + if (config == null) + { + throw new PipelineJobException("No NextFlow configuration found"); + } + + // Use the configured API key if set + String apiKey = config.getApiKey(); + if (apiKey == null) + { + session = SecurityManager.createTransformSession(getJob().getUser()); + apiKey = session.getApiKey(); + } + + // Need to pass to the main process directly in the future to allow concurrent execution for different users + ProcessBuilder secretsPB = new ProcessBuilder("nextflow", "secrets", "set", "PANORAMA_API_KEY", apiKey); + log.info("Job Started"); + File dir = getJob().getLogFile().getParentFile(); + getJob().runSubProcess(secretsPB, dir); + + ProcessBuilder executionPB = new ProcessBuilder(getArgs()); + getJob().runSubProcess(executionPB, dir); + log.info("Job Finished"); + return new RecordedActionSet(); + } + finally + { + if (session != null) + { + session.close(); + } + } + } + + private boolean hasAwsSection(File configFile) throws PipelineJobException + { + try (FileInputStream fIn = new FileInputStream(configFile); + InputStreamReader isReader = new InputStreamReader(fIn, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(isReader)) + { + String line; + while ((line = reader.readLine()) != null) + { + line = line.trim(); + // Ignore comments + if (!line.startsWith("//")) + { + if (line.startsWith("aws")) + { + return true; + } + } + } + return false; + } + catch (IOException e) + { + throw new PipelineJobException(e); + } + } + + + private @NotNull List<String> getArgs() throws PipelineJobException + { + NextFlowConfiguration config = NextFlowManager.get().getConfiguration(); String nextFlowConfigFilePath = config.getNextFlowConfigFilePath(); - String s3BucketPath = config.getS3BucketPath(); - String s3Path = "s3://" + s3BucketPath; - String apiKey = getJob().getApiKey(); - ProcessBuilder pb = new ProcessBuilder( "nextflow" , "secrets", "set", "PANORAMA_API_KEY", apiKey); - log.info("Job Started"); - File dir = FileContentService.get().getDefaultRootInfo(getJob().getContainer()).getPath().toFile(); - getJob().runSubProcess(pb, dir); - pb.command("nextflow" , "run", "-resume", "-r", "main", "-profile", "aws", "mriffle/nf-skyline-dia-ms", "-bucket-dir", s3Path, "-c", nextFlowConfigFilePath); - getJob().runSubProcess(pb, dir); - log.info("Job Finished"); - return new RecordedActionSet(); + + if (nextFlowConfigFilePath == null) + { + throw new PipelineJobException("No NextFlow config file specified"); + } + + File configFile = new File(nextFlowConfigFilePath); + if (!configFile.isFile()) + { + throw new PipelineJobException("NextFlow config file not found"); + } + + boolean aws = hasAwsSection(configFile); + + List<String> args = new ArrayList<>(Arrays.asList("nextflow", "run", "-resume", "-r", "main")); + if (aws) + { + args.add("-profile"); + args.add("aws"); + } + args.add("mriffle/nf-skyline-dia-ms"); + if (aws) + { + String s3BucketPath = config.getS3BucketPath(); + String s3Path = "s3://" + s3BucketPath; + + args.add("-bucket-dir"); + args.add(s3Path); + } + args.add("-c"); + args.add(nextFlowConfigFilePath); + return args; } @Override