├── settings.gradle ├── src ├── test │ ├── resources │ │ ├── commandLaunchTest │ │ │ ├── fakeResource │ │ │ │ └── aFile.txt │ │ │ ├── project │ │ │ │ ├── project-file.txt │ │ │ │ ├── session │ │ │ │ │ ├── scan │ │ │ │ │ │ ├── scan-file.txt │ │ │ │ │ │ └── resource │ │ │ │ │ │ │ └── scan-resource-file.txt │ │ │ │ │ ├── session-file.txt │ │ │ │ │ └── resource │ │ │ │ │ │ └── session-resource-file.txt │ │ │ │ └── resource │ │ │ │ │ └── project-resource-file.txt │ │ │ ├── testAssessorUpload │ │ │ │ └── fakeDirForCopy │ │ │ │ │ ├── DATA │ │ │ │ │ ├── 1.txt │ │ │ │ │ ├── 2.txt │ │ │ │ │ └── 3.txt │ │ │ │ │ └── assessor.xml │ │ │ ├── testScanUpload │ │ │ │ └── fakeDirForCopy │ │ │ │ │ ├── DICOM │ │ │ │ │ ├── 1.dcm │ │ │ │ │ ├── 2.dcm │ │ │ │ │ └── 3.dcm │ │ │ │ │ └── scan.xml │ │ │ ├── testEntrypointIsPreserved │ │ │ │ ├── entrypoint.sh │ │ │ │ ├── no-entrypoint.sh │ │ │ │ ├── Dockerfile │ │ │ │ └── command.json │ │ │ ├── testEntrypointIsRemoved │ │ │ │ ├── entrypoint.sh │ │ │ │ ├── no-entrypoint.sh │ │ │ │ ├── Dockerfile │ │ │ │ └── command.json │ │ │ ├── project.json │ │ │ ├── testDeleteCommandWhenDeleteImageAfterLaunchingContainer │ │ │ │ ├── Dockerfile │ │ │ │ └── command.json │ │ │ ├── session.json │ │ │ ├── testCommandWithGenericResourcesGpu │ │ │ │ └── debug_command_with_gpu.json │ │ │ └── project-mount-command.json │ │ ├── commandResolutionTest │ │ │ ├── mountTests │ │ │ │ ├── data │ │ │ │ │ ├── hello1.txt │ │ │ │ │ └── subdir │ │ │ │ │ │ └── hello2.txt │ │ │ │ ├── resource.json │ │ │ │ ├── command.json │ │ │ │ └── command-writable-mount.json │ │ │ ├── testProject │ │ │ │ └── project.json │ │ │ ├── testProjectSubject │ │ │ │ └── project.json │ │ │ ├── testSessionAssessor │ │ │ │ └── session.json │ │ │ ├── testPathTranslation │ │ │ │ ├── resource.json │ │ │ │ └── command.json │ │ │ ├── testResourceFile │ │ │ │ └── scan.json │ │ │ ├── illegal-args-command.json │ │ │ ├── testSessionScanResource │ │ │ │ └── session.json │ │ │ ├── testSessionScanMult │ │ │ │ └── session.json │ │ │ ├── params-command.json │ │ │ └── select-command.json │ │ ├── wrapupCommand │ │ │ ├── main-command-script.sh │ │ │ ├── wrapup-command-script.sh │ │ │ ├── wrapup-command.json │ │ │ ├── Dockerfile.wrapup │ │ │ ├── session.json │ │ │ ├── Dockerfile.main │ │ │ └── command-with-wrapup-command.json │ │ ├── setupCommand │ │ │ ├── resource.json │ │ │ ├── setup-command-script.sh │ │ │ ├── setup-command.json │ │ │ ├── Dockerfile │ │ │ └── command-with-setup-command.json │ │ ├── dockerRestApiTest │ │ │ └── commands.json │ │ ├── log4j.properties │ │ └── ecatHeaderDump │ │ │ └── command.json │ └── java │ │ └── org │ │ └── nrg │ │ └── containers │ │ ├── utils │ │ ├── BackendConfig.java │ │ ├── LoggingBuildCallback.java │ │ └── ShellSplitterTest.java │ │ ├── config │ │ ├── CommandLabelServiceTestConfig.java │ │ ├── CommandConfigurationTestConfig.java │ │ ├── ObjectMapperConfig.java │ │ ├── DockerServerEntityTestConfig.java │ │ ├── CommandTestConfig.java │ │ ├── SpringJUnit4ClassRunnerFactory.java │ │ ├── DockerRestApiTestConfig.java │ │ └── ContainerEntityTestConfig.java │ │ ├── model │ │ └── xnat │ │ │ ├── OuterTestPojo.java │ │ │ └── InnerTestPojo.java │ │ ├── services │ │ └── CommandLabelServiceTest.java │ │ └── api │ │ └── KubernetesClientTest.java └── main │ ├── resources │ ├── META-INF │ │ ├── resources │ │ │ ├── templates │ │ │ │ └── screens │ │ │ │ │ ├── header │ │ │ │ │ └── csHeaderIncludes.vm │ │ │ │ │ ├── xnat_projectData │ │ │ │ │ ├── actionsBox │ │ │ │ │ │ └── Run.vm │ │ │ │ │ └── report │ │ │ │ │ │ └── postData │ │ │ │ │ │ └── Injector.vm │ │ │ │ │ ├── xnat_subjectData │ │ │ │ │ └── actionsBox │ │ │ │ │ │ └── Run.vm │ │ │ │ │ ├── xnat_experimentData │ │ │ │ │ └── actionsBox │ │ │ │ │ │ └── Run.vm │ │ │ │ │ ├── xnat_imageAssessorData │ │ │ │ │ └── actionsBox │ │ │ │ │ │ └── Run.vm │ │ │ │ │ ├── xnat_imageSessionData │ │ │ │ │ └── actionsBox │ │ │ │ │ │ └── Run.vm │ │ │ │ │ ├── xnat_imageScanData │ │ │ │ │ └── scanActions │ │ │ │ │ │ └── runContainers.vm │ │ │ │ │ └── project │ │ │ │ │ └── project_bundle_tabs.vm │ │ │ ├── log4j.properties │ │ │ └── scripts │ │ │ │ └── xnat │ │ │ │ └── plugin │ │ │ │ └── containerService │ │ │ │ ├── containerConfig.css │ │ │ │ └── commandUI.css │ │ └── xnat │ │ │ └── migration │ │ │ └── 1.9.0 │ │ │ └── secrets-jsonb-migrations.ini │ └── config │ │ └── roles │ │ └── container-role-definition.properties │ └── java │ └── org │ └── nrg │ ├── containers │ ├── exceptions │ │ ├── TaskNotFoundException.java │ │ ├── ServiceNotFoundException.java │ │ ├── ContainerServiceSecretException.java │ │ ├── ContainerException.java │ │ ├── ImageMetadataException.java │ │ ├── CommandResolutionException.java │ │ ├── InvalidDefinitionException.java │ │ ├── CommandPreResolutionException.java │ │ ├── NotUniqueException.java │ │ ├── NoHubException.java │ │ ├── BadRequestException.java │ │ ├── UnauthorizedException.java │ │ ├── DockerServerException.java │ │ ├── IllegalInputException.java │ │ ├── NoContainerServerException.java │ │ ├── ContainerBackendException.java │ │ ├── NoDockerServerException.java │ │ ├── CommandWrapperInputResolutionException.java │ │ ├── ContainerFinalizationException.java │ │ ├── CommandMountResolutionException.java │ │ ├── CommandValidationException.java │ │ └── CommandInputResolutionException.java │ ├── api │ │ ├── LogType.java │ │ ├── KubernetesClientFactory.java │ │ ├── KubernetesInformer.java │ │ ├── KubernetesClient.java │ │ └── KubernetesClientFactoryImpl.java │ ├── services │ │ ├── DockerServerEntityService.java │ │ ├── ContainerSecretService.java │ │ ├── CommandLabelService.java │ │ ├── OrchestrationProjectEntityService.java │ │ ├── DockerServerService.java │ │ ├── impl │ │ │ ├── HibernateDockerServerEntityService.java │ │ │ └── OrchestrationProjectEntityServiceImpl.java │ │ ├── CommandEventMappingService.java │ │ ├── ContainerFinalizeService.java │ │ ├── CommandEntityService.java │ │ ├── OrchestrationService.java │ │ ├── OrchestrationEntityService.java │ │ ├── ContainerEntityService.java │ │ ├── DockerHubService.java │ │ └── ContainerConfigService.java │ ├── events │ │ └── model │ │ │ ├── KubernetesContainerState.java │ │ │ ├── ContainerEvent.java │ │ │ ├── ServiceTaskEvent.java │ │ │ ├── ScanArchiveEventToLaunchCommands.java │ │ │ ├── SessionMergeOrArchiveEvent.java │ │ │ ├── KubernetesStatusChangeEvent.java │ │ │ └── DockerContainerEvent.java │ ├── model │ │ ├── container │ │ │ ├── auto │ │ │ │ ├── ContainerPaginatedRequest.java │ │ │ │ └── ContainerMessage.java │ │ │ └── ContainerInputType.java │ │ ├── server │ │ │ └── docker │ │ │ │ ├── DockerClientCacheKey.java │ │ │ │ └── Backend.java │ │ ├── command │ │ │ ├── entity │ │ │ │ ├── DockerSetupCommandEntity.java │ │ │ │ ├── DockerWrapupCommandEntity.java │ │ │ │ ├── CommandType.java │ │ │ │ └── CommandVisibility.java │ │ │ └── auto │ │ │ │ ├── ResolvedInputValue.java │ │ │ │ └── PreresolvedInputTreeNode.java │ │ ├── orchestration │ │ │ ├── entity │ │ │ │ ├── OrchestrationProjectEntity.java │ │ │ │ └── OrchestratedWrapperEntity.java │ │ │ └── auto │ │ │ │ └── OrchestrationProject.java │ │ ├── configuration │ │ │ ├── ProjectEnabledReport.java │ │ │ └── PluginVersionCheck.java │ │ └── kubernetes │ │ │ └── KubernetesPodPhase.java │ ├── daos │ │ ├── OrchestrationProjectEntityDao.java │ │ ├── DockerHubDao.java │ │ └── OrchestrationEntityDao.java │ ├── security │ │ ├── WorkflowId.java │ │ ├── ContainerId.java │ │ └── ContainerManagerUserAuthorization.java │ ├── secrets │ │ ├── ResolverFor.java │ │ ├── SecretValueObtainer.java │ │ ├── SecretDestination.java │ │ ├── SecretSource.java │ │ ├── EnvironmentVariableResolvedSecret.java │ │ ├── Secret.java │ │ ├── ContainerPropertiesWithSecretValues.java │ │ ├── SecretUtils.java │ │ ├── EnvironmentVariableSecretDestination.java │ │ └── SystemPropertySecretSource.java │ ├── jms │ │ ├── requests │ │ │ ├── ContainerFinalizingRequest.java │ │ │ ├── ContainerStagingRequest.java │ │ │ └── ContainerRequest.java │ │ ├── tasks │ │ │ └── QueueManager.java │ │ ├── utils │ │ │ └── QueueUtils.java │ │ └── listeners │ │ │ └── ContainerFinalizingRequestListener.java │ ├── utils │ │ ├── JsonDateSerializer.java │ │ ├── ShellSplitter.java │ │ └── JsonStringToDateSerializer.java │ ├── rest │ │ └── ContainerLogPollResponse.java │ └── initialization │ │ └── tasks │ │ ├── CheckContainerServiceVersion.java │ │ └── UpdateVisibilityOfExistingCommands.java │ └── xnat │ └── turbine │ └── modules │ └── screens │ └── AdminContainerService.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitlab-ci.yml ├── docs ├── make-container-service-api-md.py ├── enable-a-command-in-your-project.md ├── container-service-api.md.template ├── jms-queues.md ├── md2confluencehtml.sh ├── launching-containers-for-users.md ├── html2confluence.py ├── upload-docs.sh ├── set-up-vms-for-swarm-dev.md └── swarm-mode-vs-container-mode.md ├── .gitignore ├── .circleci └── config.yml ├── README.md ├── scripts └── set_up_kubernetes.sh ├── LICENSE.md └── CONTRIBUTING.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "container-service" 2 | 3 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/fakeResource/aFile.txt: -------------------------------------------------------------------------------- 1 | File contents -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project/project-file.txt: -------------------------------------------------------------------------------- 1 | No content -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/mountTests/data/hello1.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project/session/scan/scan-file.txt: -------------------------------------------------------------------------------- 1 | No content -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project/session/session-file.txt: -------------------------------------------------------------------------------- 1 | No content -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testAssessorUpload/fakeDirForCopy/DATA/1.txt: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testAssessorUpload/fakeDirForCopy/DATA/2.txt: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testAssessorUpload/fakeDirForCopy/DATA/3.txt: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testScanUpload/fakeDirForCopy/DICOM/1.dcm: -------------------------------------------------------------------------------- 1 | 1 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testScanUpload/fakeDirForCopy/DICOM/2.dcm: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testScanUpload/fakeDirForCopy/DICOM/3.dcm: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/mountTests/data/subdir/hello2.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project/resource/project-resource-file.txt: -------------------------------------------------------------------------------- 1 | No content -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project/session/resource/session-resource-file.txt: -------------------------------------------------------------------------------- 1 | No content -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project/session/scan/resource/scan-resource-file.txt: -------------------------------------------------------------------------------- 1 | No content -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NrgXnat/container-service/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsPreserved/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "entrypoint.sh" 4 | echo "I am doing great!" 5 | exit 0 6 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsRemoved/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "entrypoint.sh" 4 | echo "I am going to fail now" 5 | exit 1 6 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsRemoved/no-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "no-entrypoint.sh" 4 | echo "I am going to not fail now" 5 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsPreserved/no-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "no-entrypoint.sh" 4 | echo "I am going to fail now" 5 | exit 1 6 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/header/csHeaderIncludes.vm: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testAssessorUpload/fakeDirForCopy/assessor.xml: -------------------------------------------------------------------------------- 1 | This is a fake assessor.xml file. Since we don't actually parse/upload it, the contents don't matter 2 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testProject/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Project", 3 | "id": "aProject", 4 | "uri": "/projects/aProject", 5 | "label": "aProject" 6 | } -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/main-command-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | die(){ 4 | echo >&2 "$@" 5 | exit 1 6 | } 7 | 8 | cp /input/* /output || die "FAILED cp /input/* /output" 9 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsRemoved/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | ADD entrypoint.sh /usr/local/bin/ 3 | ADD no-entrypoint.sh /usr/local/bin/ 4 | ENTRYPOINT entrypoint.sh 5 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsPreserved/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | ADD entrypoint.sh /usr/local/bin/ 3 | ADD no-entrypoint.sh /usr/local/bin/ 4 | ENTRYPOINT entrypoint.sh 5 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "project", 3 | "type": "Project", 4 | "label": "project", 5 | "uri": "/projects/project", 6 | "xsiType": "xnat:projectData" 7 | } -------------------------------------------------------------------------------- /src/test/resources/setupCommand/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0", 3 | "type": "Resource", 4 | "label": "this is the resource label", 5 | "directory": "change this at runtime", 6 | "uri": "/scans/0/resources/0" 7 | } -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/wrapup-command-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | die(){ 4 | echo >&2 "$@" 5 | exit 1 6 | } 7 | 8 | find /input > /output/found-files.txt || die "FAILED find /input > /output/found-files.txt" 9 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/utils/BackendConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.utils; 2 | 3 | import lombok.Value; 4 | 5 | @Value 6 | public class BackendConfig { 7 | String containerHost; 8 | String certPath; 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/TaskNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class TaskNotFoundException extends Exception { 4 | public TaskNotFoundException(Throwable e) { 5 | super(e); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/api/LogType.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.api; 2 | 3 | public enum LogType { 4 | STDOUT, 5 | STDERR; 6 | 7 | public String logName() { 8 | return this.name().toLowerCase() + ".log"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/ServiceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class ServiceNotFoundException extends Exception { 4 | public ServiceNotFoundException(Throwable e) { 5 | super(e); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/setupCommand/setup-command-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | die(){ 4 | echo >&2 "$@" 5 | exit 1 6 | } 7 | 8 | cp /input/* /output || die "FAILED cp /input/* /output" 9 | 10 | touch /output/another-file || die "FAILED touch /output/another-file" 11 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/mountTests/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0", 3 | "type": "Resource", 4 | "label": "this is the resource label", 5 | "directory": "change this at runtime", 6 | "uri": "/scans/0/resources/0", 7 | "files": [ 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testDeleteCommandWhenDeleteImageAfterLaunchingContainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | LABEL org.nrg.commands="[{\"xnat\": [{\"name\": \"placeholder\"}], \"image\": \"xnat/testy-test:latest\", \"version\": \"0\", \"name\": \"command\", \"command-line\": \"echo hello world\"}]" 3 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_projectData/actionsBox/Run.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/xnat/migration/1.9.0/secrets-jsonb-migrations.ini: -------------------------------------------------------------------------------- 1 | [columns] 2 | xhbm_command_entity.secrets=transform:convertJsonToJsonb 3 | xhbm_container_entity.secrets=transform:convertJsonToJsonb 4 | 5 | [transforms] 6 | convertJsonToJsonb=org.nrg.xnat.initialization.tasks.transforms.ConvertJsonToJsonb 7 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/api/KubernetesClientFactory.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.api; 2 | 3 | import org.nrg.containers.exceptions.NoContainerServerException; 4 | 5 | public interface KubernetesClientFactory { 6 | KubernetesClient getKubernetesClient() throws NoContainerServerException; 7 | void shutdown(); 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/setupCommand/setup-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "setup-command", 3 | "description": "A setup command", 4 | "type": "docker-setup", 5 | "image": "xnat/test-setup-command:latest", 6 | "command-line": "setup-command-script.sh", 7 | "inputs": [], 8 | "mounts": [], 9 | "xnat": [] 10 | } -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/wrapup-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wrapup-command", 3 | "description": "A wrapup command", 4 | "type": "docker-wrapup", 5 | "image": "xnat/test-wrapup-command:latest", 6 | "command-line": "wrapup-command-script.sh", 7 | "inputs": [], 8 | "mounts": [], 9 | "xnat": [] 10 | } -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testDeleteCommandWhenDeleteImageAfterLaunchingContainer/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "command", 3 | "image": "xnat/testy-test:latest", 4 | "version": "0", 5 | "command-line": "echo hello world", 6 | "xnat": [ 7 | { 8 | "name": "placeholder" 9 | } 10 | ] 11 | } -------------------------------------------------------------------------------- /src/test/resources/setupCommand/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | COPY setup-command-script.sh /usr/local/bin/ 3 | LABEL org.nrg.commands="[{\"inputs\": [], \"name\": \"setup-command\", \"command-line\": \"setup-command-script.sh\", \"xnat\": [], \"image\": \"xnat/test-setup-command:latest\", \"mounts\": [], \"type\": \"docker-setup\", \"description\": \"A setup command\"}]" 4 | -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/Dockerfile.wrapup: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | COPY wrapup-command-script.sh /usr/local/bin/ 3 | LABEL org.nrg.commands="[{\"inputs\": [], \"name\": \"wrapup-command\", \"command-line\": \"wrapup-command-script.sh\", \"xnat\": [], \"image\": \"xnat/test-wrapup-command:latest\", \"mounts\": [], \"type\": \"docker-wrapup\", \"description\": \"A wrapup command\"}]" 4 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/DockerServerEntityService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.server.docker.DockerServerEntity; 4 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 5 | 6 | public interface DockerServerEntityService extends BaseHibernateService { 7 | DockerServerEntity getServer(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/api/KubernetesInformer.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.api; 2 | 3 | import io.kubernetes.client.openapi.models.V1Job; 4 | import io.kubernetes.client.openapi.models.V1Pod; 5 | 6 | public interface KubernetesInformer { 7 | void start(); 8 | void stop(); 9 | 10 | V1Job getJob(final String name); 11 | V1Pod getPod(final String name); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/KubernetesContainerState.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | /** 4 | * See kubernetes docs 5 | * Pod Lifecycle: Container States 6 | */ 7 | public enum KubernetesContainerState { 8 | WAITING, 9 | RUNNING, 10 | TERMINATED 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_subjectData/actionsBox/Run.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/container/auto/ContainerPaginatedRequest.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.container.auto; 2 | 3 | import org.nrg.framework.ajax.hibernate.HibernatePaginatedRequest; 4 | 5 | public class ContainerPaginatedRequest extends HibernatePaginatedRequest { 6 | @Override 7 | public String getDefaultSortColumn() { 8 | return "timestamp"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_experimentData/actionsBox/Run.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_imageAssessorData/actionsBox/Run.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_imageSessionData/actionsBox/Run.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/ContainerSecretService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.exceptions.ContainerServiceSecretException; 4 | import org.nrg.containers.secrets.ResolvedSecret; 5 | import org.nrg.containers.secrets.Secret; 6 | 7 | public interface ContainerSecretService { 8 | ResolvedSecret resolve(Secret secret) throws ContainerServiceSecretException; 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testProjectSubject/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "Project", 3 | "id": "aProject", 4 | "uri": "/projects/aProject", 5 | "label": "aProject", 6 | "subjects": [ 7 | { 8 | "id": "aSubject", 9 | "label": "aSubject", 10 | "type": "Subject", 11 | "uri": "/projects/aProject/subjects/aSubject" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_projectData/report/postData/Injector.vm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "session1", 3 | "type": "Session", 4 | "label": "session1", 5 | "uri": "/experiments/session1", 6 | "xsiType": "xnat:aSession", 7 | "resources": [ 8 | { 9 | "id": 0, 10 | "type": "Resource", 11 | "label": "RESOURCE", 12 | "xsiType": "xnat:aResource", 13 | "uri": "/experiments/session1/resources/RESOURCE" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/ContainerServiceSecretException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class ContainerServiceSecretException extends Exception { 4 | public ContainerServiceSecretException(final String message) { 5 | super(message); 6 | } 7 | public ContainerServiceSecretException(final String message, final Throwable t) { 8 | super(message, t); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testSessionAssessor/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "aSession", 3 | "type": "Session", 4 | "label": "aSession", 5 | "uri": "/experiments/aSession", 6 | "assessors": [ 7 | { 8 | "id": "anAssessor", 9 | "type": "Assessor", 10 | "uri": "/experiments/aSession/assessors/anAssessor", 11 | "label": "anAssessor" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - project: flywheel-io/infrastructure/ci-templates 4 | ref: master 5 | file: security-scans.yml 6 | - template: Jobs/SAST.gitlab-ci.yml 7 | 8 | gemnasium-maven-dependency_scanning: 9 | variables: 10 | DS_JAVA_VERSION: 8 11 | 12 | variables: 13 | SAST_EXCLUDED_PATHS: "spec, test, tests, tmp, swagger.json" 14 | 15 | sast: 16 | variables: 17 | SAST_EXCLUDED_PATHS: "$SAST_EXCLUDED_PATHS" 18 | stage: test 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/ContainerException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class ContainerException extends Exception { 4 | public ContainerException(final String message) { 5 | super(message); 6 | } 7 | 8 | public ContainerException(final String message, final Throwable e) { 9 | super(message, e); 10 | } 11 | 12 | public ContainerException(final Throwable e) { 13 | super(e); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/daos/OrchestrationProjectEntityDao.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.daos; 2 | 3 | 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.nrg.containers.model.orchestration.entity.OrchestrationProjectEntity; 6 | import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Slf4j 10 | @Repository 11 | public class OrchestrationProjectEntityDao extends AbstractHibernateDAO { 12 | } 13 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testPathTranslation/resource.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0", 3 | "type": "Resource", 4 | "label": "this is the resource label", 5 | "directory": "change this at runtime", 6 | "uri": "/scans/0/resources/0", 7 | "files": [ 8 | { 9 | "type": "File", 10 | "name": "file.file", 11 | "uri": "/scans/0/resources/0/files/file.file", 12 | "path": "change this at runtime" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/CommandLabelService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.command.auto.Command; 4 | import org.nrg.containers.model.image.docker.DockerImage; 5 | 6 | import java.util.List; 7 | 8 | public interface CommandLabelService { 9 | String LABEL_KEY = "org.nrg.commands"; 10 | 11 | List parseLabels(DockerImage dockerImage); 12 | List parseLabels(String imageName, DockerImage dockerImage); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/ContainerEvent.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | import org.nrg.framework.event.EventI; 4 | 5 | import java.util.Map; 6 | 7 | public interface ContainerEvent extends EventI { 8 | String QUEUE = "containerEventQueue"; 9 | 10 | String backendId(); 11 | String status(); 12 | String details(); 13 | String externalTimestamp(); 14 | Map attributes(); 15 | boolean isExitStatus(); 16 | String exitCode(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/security/WorkflowId.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.security; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Indicates that the parameter can be used to discover workflow ID(s) passed as method parameters. 10 | */ 11 | @Target(ElementType.PARAMETER) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface WorkflowId { 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/OrchestrationProjectEntityService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.orchestration.entity.OrchestrationProjectEntity; 4 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 5 | 6 | public interface OrchestrationProjectEntityService extends BaseHibernateService { 7 | OrchestrationProjectEntity find(String project); 8 | 9 | void checkAndDisable(String project, long wrapperId); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/config/roles/container-role-definition.properties: -------------------------------------------------------------------------------- 1 | org.nrg.Role=ContainerManager 2 | org.nrg.Role.ContainerManager.key=ContainerManager 3 | org.nrg.Role.ContainerManager.name=Container Manager 4 | org.nrg.Role.ContainerManager.warning=WARNING: Granting this permission allows this user control over Container visibility, assigning access to containers for projects across the entire site. 5 | org.nrg.Role.ContainerManager.description=A user with Container Manager role can manage Containers deployed on a site. 6 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/utils/LoggingBuildCallback.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.utils; 2 | 3 | import com.github.dockerjava.api.command.BuildImageResultCallback; 4 | import com.github.dockerjava.api.model.BuildResponseItem; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | @Slf4j 8 | public class LoggingBuildCallback extends BuildImageResultCallback { 9 | @Override 10 | public void onNext(BuildResponseItem item) { 11 | log.debug("{}", item); 12 | super.onNext(item); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/security/ContainerId.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.security; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Indicates that the parameter can be used to discover container ID(s) passed as method parameters. 10 | */ 11 | @Target(ElementType.PARAMETER) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface ContainerId { 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsPreserved/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entrypoint-test", 3 | "description": "Tests whether image entrypoint gets removed or not", 4 | "version": "1.0", 5 | "schema-version": "1.0", 6 | "type": "docker", 7 | "image": "xnat/entrypoint-test:latest", 8 | "command-line": "no-entrypoint.sh", 9 | "xnat": [ 10 | { 11 | "name": "wrapper", 12 | "description": "I have to be here, I guess" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/ImageMetadataException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | @SuppressWarnings("unused") 4 | public class ImageMetadataException extends RuntimeException { 5 | public ImageMetadataException(Throwable cause) { 6 | super(cause); 7 | } 8 | 9 | public ImageMetadataException(String message) { 10 | super(message); 11 | } 12 | 13 | public ImageMetadataException(String message, Throwable cause) { 14 | super(message, cause); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/CommandResolutionException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class CommandResolutionException extends Exception { 4 | public CommandResolutionException(final String message) { 5 | super(message); 6 | } 7 | 8 | public CommandResolutionException(final String message, final Throwable cause) { 9 | super(message, cause); 10 | } 11 | 12 | public CommandResolutionException(final Throwable cause) { 13 | super(cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/InvalidDefinitionException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class InvalidDefinitionException extends Exception { 4 | public InvalidDefinitionException(final String message) { 5 | super(message); 6 | } 7 | 8 | public InvalidDefinitionException(final String message, final Throwable cause) { 9 | super(message, cause); 10 | } 11 | 12 | public InvalidDefinitionException(final Throwable cause) { 13 | super(cause); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/server/docker/DockerClientCacheKey.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.server.docker; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Value; 5 | 6 | @Value 7 | @AllArgsConstructor 8 | public class DockerClientCacheKey { 9 | String host; 10 | String certPath; 11 | Backend backend; 12 | 13 | public DockerClientCacheKey(final DockerServerBase.DockerServer dockerServer) { 14 | this(dockerServer.host(), dockerServer.certPath(), dockerServer.backend()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/CommandPreResolutionException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | public class CommandPreResolutionException extends CommandResolutionException{ 4 | 5 | public CommandPreResolutionException(final String message) { super(message); } 6 | 7 | public CommandPreResolutionException(final String message, final Throwable cause) { 8 | super(message, cause); 9 | } 10 | 11 | public CommandPreResolutionException(final Throwable cause) { 12 | super(cause); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/make-container-service-api-md.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | 5 | swaggerJsonFile = 'swagger.json' 6 | mdTemplateFile = 'container-service-api.md.template' 7 | mdFile = 'container-service-api.md' 8 | 9 | with open(swaggerJsonFile, 'r') as f: 10 | swaggerJson = json.load(f) 11 | swaggerJsonString = json.dumps(swaggerJson) 12 | 13 | with open(mdTemplateFile, 'r') as f: 14 | mdTemplate = f.read() 15 | 16 | md = mdTemplate.replace('%SWAGGER_JSON_HERE%', swaggerJsonString) 17 | 18 | with open(mdFile, 'w') as f: 19 | f.write(md) 20 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/ResolverFor.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface ResolverFor { 11 | Class source() default SecretSource.AnySource.class; 12 | Class destination(); 13 | } 14 | -------------------------------------------------------------------------------- /docs/enable-a-command-in-your-project.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This task needs to be performed by a project owner or a site admin. 4 | 5 | 1. Navigate to your project's page. SCREENSHOT 6 | 2. Click the Project Settings link in the Actions box. SCREENSHOT 7 | 3. Click the something tab. Here you will see a list of command wrappers. 8 | 4. Click the Enable toggle on the wrapper you want to enable. SCREENSHOT 9 | 5. Optionally, you can click the SOMETHING button to customize the default values and matchers for the command and wrapper inputs in your project. SCREENSHOT 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle template 2 | .gradle/ 3 | build/ 4 | .classpath 5 | .project 6 | .settings/ 7 | 8 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 9 | !gradle-wrapper.jar 10 | 11 | /.idea/ 12 | *.iml 13 | *.ipr 14 | *.iws 15 | 16 | # xnat-web exploded war 17 | /out/ 18 | 19 | # Intellij wants to see this file to configure hibernate, but we don't really need it 20 | hibernate.cfg.xml 21 | 22 | # Ignore script-generated .html and .html.confluence documentation 23 | *.html 24 | *.html.confluence 25 | container-service-api.md 26 | /bin/ 27 | 28 | gradle.properties 29 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/NotUniqueException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.BAD_REQUEST) 7 | public class NotUniqueException extends Exception { 8 | public NotUniqueException() { 9 | super(); 10 | } 11 | 12 | public NotUniqueException(final Throwable e) { 13 | super(e); 14 | } 15 | 16 | public NotUniqueException(final String message) { 17 | super(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/SecretValueObtainer.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import java.util.Optional; 4 | 5 | public abstract class SecretValueObtainer { 6 | private final Class sourceType; 7 | protected SecretValueObtainer(final Class sourceType) { 8 | this.sourceType = sourceType; 9 | } 10 | 11 | public Class handledType() { 12 | return sourceType; 13 | } 14 | 15 | public abstract Optional obtainValue(SecretSource.ValueObtainingSecretSource source); 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testEntrypointIsRemoved/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entrypoint-test", 3 | "description": "Tests whether image entrypoint gets removed or not", 4 | "version": "1.0", 5 | "schema-version": "1.0", 6 | "type": "docker", 7 | "image": "xnat/entrypoint-test:latest", 8 | "command-line": "no-entrypoint.sh", 9 | "override-entrypoint": true, 10 | "xnat": [ 11 | { 12 | "name": "wrapper", 13 | "label": "Test wrapper", 14 | "description": "I have to be here, I guess" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /docs/container-service-api.md.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | The Container Service API swagger documentation can also be accessed inside your XNAT at `https://{XNAT_HOME_URL}/xapi/swagger-ui.html`. From that UI, you can execute all the API calls. 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/command/entity/DockerSetupCommandEntity.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.command.entity; 2 | 3 | import javax.persistence.DiscriminatorValue; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Transient; 6 | 7 | @Entity 8 | @DiscriminatorValue("docker-setup") 9 | public class DockerSetupCommandEntity extends CommandEntity { 10 | public static final CommandType type = CommandType.DOCKER_SETUP; 11 | 12 | @Transient 13 | public CommandType getType() { 14 | return type; 15 | } 16 | 17 | public void setType(final CommandType type) {} 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/command/entity/DockerWrapupCommandEntity.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.command.entity; 2 | 3 | import javax.persistence.DiscriminatorValue; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Transient; 6 | 7 | @Entity 8 | @DiscriminatorValue("docker-wrapup") 9 | public class DockerWrapupCommandEntity extends CommandEntity { 10 | public static final CommandType type = CommandType.DOCKER_WRAPUP; 11 | 12 | @Transient 13 | public CommandType getType() { 14 | return type; 15 | } 16 | 17 | public void setType(final CommandType type) {} 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/NoHubException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.FAILED_DEPENDENCY) 7 | public class NoHubException extends Exception { 8 | public NoHubException(final String message) { 9 | super(message); 10 | } 11 | 12 | public NoHubException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public NoHubException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/server/docker/Backend.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.server.docker; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | import java.util.Set; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.Stream; 8 | 9 | public enum Backend { 10 | DOCKER, 11 | SWARM, 12 | KUBERNETES; 13 | 14 | @JsonValue 15 | @Override 16 | public String toString() { 17 | return this.name().toLowerCase(); 18 | } 19 | 20 | public static final Set SUPPORTS_CONSTRAINTS = 21 | Stream.of(Backend.SWARM, Backend.KUBERNETES).collect(Collectors.toSet()); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.BAD_REQUEST) 7 | public class BadRequestException extends Exception { 8 | public BadRequestException(final String message) { 9 | super(message); 10 | } 11 | 12 | public BadRequestException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public BadRequestException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/SecretDestination.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSubTypes; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | import lombok.SneakyThrows; 6 | 7 | import java.util.Map; 8 | 9 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") 10 | @JsonSubTypes({ 11 | @JsonSubTypes.Type(value = EnvironmentVariableSecretDestination.class, name = EnvironmentVariableSecretDestination.JSON_TYPE_NAME) 12 | }) 13 | public interface SecretDestination { 14 | String type(); 15 | String identifier(); 16 | Map otherProperties(); 17 | } 18 | -------------------------------------------------------------------------------- /docs/jms-queues.md: -------------------------------------------------------------------------------- 1 | # JMS Queues and the Container Service 2 | 3 | In the interest of offloading work from the XNAT tomcat server and onto "shadow" servers, we've added two queues, staging and finalizing, to the container service. The former handles command resolution and container launching; the latter handles the uploading of files back to XNAT upon container completion. 4 | 5 | ## Broker 6 | See `WEB-INF/conf/mq-context.xml` for the XNAT default MQ configuration. You may override these properties in `xnat-conf.properties`. 7 | 8 | ## Consumer concurrency 9 | Concurrency settings are dynamic and can be adjusted by the site admin from the `Plugin Settings > Container Service > JMS Queue` panel. -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/UnauthorizedException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.UNAUTHORIZED) 7 | public class UnauthorizedException extends Exception { 8 | public UnauthorizedException(final String message) { 9 | super(message); 10 | } 11 | 12 | public UnauthorizedException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public UnauthorizedException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/templates/screens/xnat_imageScanData/scanActions/runContainers.vm: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/DockerServerException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 7 | public class DockerServerException extends ContainerBackendException { 8 | public DockerServerException(final String message) { 9 | super(message); 10 | } 11 | 12 | public DockerServerException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public DockerServerException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/IllegalInputException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY) 7 | public class IllegalInputException extends CommandResolutionException { 8 | public IllegalInputException(final String message) { 9 | super(message); 10 | } 11 | 12 | public IllegalInputException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public IllegalInputException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/NoContainerServerException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.FAILED_DEPENDENCY) 7 | public class NoContainerServerException extends Exception { 8 | public NoContainerServerException(final String message) { 9 | super(message); 10 | } 11 | 12 | public NoContainerServerException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public NoContainerServerException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/ContainerBackendException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) 7 | public class ContainerBackendException extends Exception { 8 | public ContainerBackendException(final String message) { 9 | super(message); 10 | } 11 | 12 | public ContainerBackendException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public ContainerBackendException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/NoDockerServerException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.FAILED_DEPENDENCY) 7 | public class NoDockerServerException extends NoContainerServerException { 8 | public NoDockerServerException(final String message) { 9 | super(message); 10 | } 11 | 12 | public NoDockerServerException(final String message, final Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public NoDockerServerException(final Throwable cause) { 17 | super(cause); 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/container/ContainerInputType.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.container; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | 6 | public enum ContainerInputType { 7 | RAW("raw"), 8 | WRAPPER_DEPRECATED("wrapper"), 9 | COMMAND("command"), 10 | WRAPPER_EXTERNAL("wrapper-external"), 11 | WRAPPER_DERIVED("wrapper-derived"); 12 | 13 | private final String name; 14 | 15 | @JsonCreator 16 | ContainerInputType(final String name) { 17 | this.name = name; 18 | } 19 | 20 | @JsonValue 21 | public String getName() { 22 | return name; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/DockerServerService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.exceptions.InvalidDefinitionException; 4 | import org.nrg.containers.model.server.docker.DockerServerBase.DockerServer; 5 | import org.nrg.framework.exceptions.NotFoundException; 6 | 7 | import java.util.List; 8 | 9 | public interface DockerServerService { 10 | List getServers(); 11 | DockerServer retrieveServer(); 12 | DockerServer getServer() throws NotFoundException; 13 | DockerServer setServer(DockerServer dockerServer) throws InvalidDefinitionException; 14 | void update(DockerServer dockerServer) throws InvalidDefinitionException; 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testResourceFile/scan.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "0", 3 | "type": "Scan", 4 | "uri": "/scans/0", 5 | "resources": [ 6 | { 7 | "id": "0", 8 | "type": "Resource", 9 | "label": "this is the resource label", 10 | "directory": "change this at runtime", 11 | "uri": "/scans/0/resources/0", 12 | "files": [ 13 | { 14 | "type": "File", 15 | "name": "file.file", 16 | "uri": "/scans/0/resources/0/files/file.file", 17 | "path": "change this at runtime" 18 | } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/CommandLabelServiceTestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.nrg.containers.services.CommandLabelService; 5 | import org.nrg.containers.services.impl.CommandLabelServiceImpl; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Import; 9 | 10 | @Configuration 11 | @Import({ObjectMapperConfig.class}) 12 | public class CommandLabelServiceTestConfig { 13 | @Bean 14 | public CommandLabelService commandLabelService(final ObjectMapper objectMapper) { 15 | return new CommandLabelServiceImpl(objectMapper); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/jms/requests/ContainerFinalizingRequest.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.jms.requests; 2 | 3 | import lombok.Data; 4 | import lombok.EqualsAndHashCode; 5 | 6 | import java.io.Serializable; 7 | 8 | @Data 9 | @EqualsAndHashCode(callSuper = false) 10 | public class ContainerFinalizingRequest extends ContainerRequest implements Serializable { 11 | public static final String DESTINATION = "containerFinalizingRequest"; 12 | 13 | private static final long serialVersionUID = 1388953760707461670L; 14 | 15 | private final String exitCodeString; 16 | private final boolean isSuccessful; 17 | private final String id; 18 | private final String username; 19 | 20 | public String getDestination() { 21 | return DESTINATION; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "session1", 3 | "type": "Session", 4 | "label": "session1", 5 | "uri": "/experiments/session1", 6 | "xsiType": "xnat:aSession", 7 | "scans": [ 8 | { 9 | "id": "scan1", 10 | "type": "Scan", 11 | "xsiType": "xnat:aScan", 12 | "scan-type": "T1_TEST_SCANTYPE", 13 | "uri": "/experiments/session1/scans/scan1", 14 | "resources": [ 15 | { 16 | "id": 0, 17 | "type": "Resource", 18 | "label": "DICOM", 19 | "xsiType": "xnat:aResource", 20 | "uri": "/experiments/session1/scans/scan1/resources/DICOM" 21 | } 22 | ] 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/CommandWrapperInputResolutionException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.nrg.containers.model.command.auto.Command.CommandWrapperInput; 4 | 5 | public class CommandWrapperInputResolutionException extends CommandResolutionException { 6 | private final CommandWrapperInput input; 7 | 8 | public CommandWrapperInputResolutionException(final String message, final CommandWrapperInput input) { 9 | super(message); 10 | this.input = input; 11 | } 12 | 13 | public CommandWrapperInputResolutionException(final String message, final CommandWrapperInput input, final Throwable cause) { 14 | super(message, cause); 15 | this.input = input; 16 | } 17 | 18 | public CommandWrapperInput getInput() { 19 | return input; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/utils/JsonDateSerializer.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | 8 | import java.io.IOException; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | public class JsonDateSerializer extends JsonSerializer { 13 | 14 | public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); 15 | 16 | @Override 17 | public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException, 18 | JsonProcessingException { 19 | jgen.writeString(DATE_FORMAT.format(value)); 20 | } 21 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/SecretSource.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import com.fasterxml.jackson.annotation.JsonSubTypes; 4 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 5 | 6 | import java.util.Map; 7 | 8 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") 9 | @JsonSubTypes({ 10 | // Add new source classes here as they are created 11 | @JsonSubTypes.Type(value = SystemPropertySecretSource.class, name = SystemPropertySecretSource.JSON_TYPE_NAME) 12 | }) 13 | public interface SecretSource { 14 | String type(); 15 | String identifier(); 16 | Map otherProperties(); 17 | 18 | interface ValueObtainingSecretSource extends SecretSource {} 19 | 20 | /** 21 | * Used as default value in {@link ResolverFor} annotation 22 | */ 23 | abstract class AnySource implements SecretSource {} 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/impl/HibernateDockerServerEntityService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services.impl; 2 | 3 | import org.nrg.containers.daos.DockerServerEntityRepository; 4 | import org.nrg.containers.model.server.docker.DockerServerEntity; 5 | import org.nrg.containers.services.DockerServerEntityService; 6 | import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; 7 | import org.springframework.stereotype.Service; 8 | import org.springframework.transaction.annotation.Transactional; 9 | 10 | @Service 11 | @Transactional 12 | public class HibernateDockerServerEntityService 13 | extends AbstractHibernateEntityService 14 | implements DockerServerEntityService { 15 | @Override 16 | public DockerServerEntity getServer() { 17 | return getDao().getUniqueEnabledServer(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/container/auto/ContainerMessage.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.container.auto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.auto.value.AutoValue; 6 | 7 | import javax.annotation.Nullable; 8 | import java.io.Serializable; 9 | 10 | @AutoValue 11 | public abstract class ContainerMessage implements Serializable { 12 | private static final long serialVersionUID = 3977595045080393805L; 13 | 14 | @JsonProperty("id") public abstract String id(); 15 | @Nullable @JsonProperty("status") public abstract String status(); 16 | 17 | @JsonCreator 18 | public static ContainerMessage create(@JsonProperty("id") final String id, 19 | @JsonProperty("status") final String status) { 20 | return new AutoValue_ContainerMessage(id, status); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/ContainerFinalizationException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.nrg.containers.model.container.auto.Container; 4 | 5 | public class ContainerFinalizationException extends ContainerException { 6 | public ContainerFinalizationException(final Container container, final String message) { 7 | super(message); 8 | this.container = container; 9 | } 10 | 11 | public ContainerFinalizationException(final Container container, final String message, final Throwable e) { 12 | super(message, e); 13 | this.container = container; 14 | } 15 | 16 | public ContainerFinalizationException(final Container container, final Throwable e) { 17 | super(e); 18 | this.container = container; 19 | } 20 | 21 | private Container container; 22 | 23 | public Container getContainer() { 24 | return container; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/illegal-args-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "param-test", 3 | "description": "Test resolving params", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "echo #ANYTHING#", 7 | "inputs": [ 8 | { 9 | "name": "ANYTHING", 10 | "type": "string", 11 | "required": true 12 | } 13 | ], 14 | "xnat": [ 15 | { 16 | "name": "identity-wrapper", 17 | "label": "Param test: identity", 18 | "description": "Test resolving the command with wrapper params", 19 | "external-inputs": [ 20 | { 21 | "name": "anything", 22 | "type": "string", 23 | "required": true, 24 | "provides-value-to-command-input": "ANYTHING" 25 | } 26 | ], 27 | "derived-inputs": [] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testSessionScanResource/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "session1", 3 | "type": "Session", 4 | "label": "session1", 5 | "uri": "/experiments/session1", 6 | "scans": [ 7 | { 8 | "id": "scan1", 9 | "type": "Scan", 10 | "scan-type": "SCANTYPE", 11 | "uri": "/experiments/session1/scans/scan1", 12 | "frames": "12", 13 | "series-description": "great!", 14 | "modality": "super!", 15 | "quality": "the best!", 16 | "note": "I love it - me", 17 | "resources": [ 18 | { 19 | "id": 0, 20 | "type": "Resource", 21 | "label": "DICOM", 22 | "directory": "this will be overwritten at runtime", 23 | "uri": "/experiments/session1/scans/scan1/resources/0" 24 | } 25 | ] 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /docs/md2confluencehtml.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die(){ 4 | popd > /dev/null 5 | echo >&2 "$@" 6 | exit -1 7 | } 8 | 9 | BASEDIR=$(dirname "$0") 10 | 11 | MDFILE=$1 12 | if [[ -z "$MDFILE" ]]; then 13 | echo "Missing arg: path to Markdown file" &>2 14 | exit 1 15 | fi 16 | 17 | HTMLFILE=${MDFILE%.md}.html 18 | 19 | pandoc -o $HTMLFILE $MDFILE || die "Failed to convert $MDFILE from markdown to HTML" 20 | 21 | gawk '/
/,/<\/code><\/pre>/ {
22 |     gsub(/"/, "\"");
23 |     gsub(/</, "<");
24 |     gsub(/>/, ">");
25 |     gsub(/'/, "");
26 |     sub(/
/, "<\/pre>/, "]]>");
28 | }
29 | {print}' $HTMLFILE > $HTMLFILE.confluence || die "Failed to convert HTML code blocks to Confluence macros"
30 | 
31 | $BASEDIR/html2confluence.py $HTMLFILE.confluence || die "Failed to run python HTML to Confluence HTML script"
32 | 


--------------------------------------------------------------------------------
/src/main/java/org/nrg/containers/jms/requests/ContainerStagingRequest.java:
--------------------------------------------------------------------------------
 1 | package org.nrg.containers.jms.requests;
 2 | 
 3 | import lombok.Data;
 4 | import lombok.EqualsAndHashCode;
 5 | 
 6 | import java.io.Serializable;
 7 | import java.util.Map;
 8 | 
 9 | @Data
10 | @EqualsAndHashCode(callSuper = false)
11 | public class ContainerStagingRequest extends ContainerRequest implements Serializable {
12 |     public static final String DESTINATION = "containerStagingRequest";
13 | 
14 |     private static final long serialVersionUID = -1608362951558668332L;
15 | 
16 |     private final String              project;
17 |     private final long                wrapperId;
18 |     private final long                commandId;
19 |     private final String              wrapperName;
20 |     private final Map inputValues;
21 |     private final String              username;
22 |     private final String              workflowId;
23 | 
24 |     public String getDestination() {
25 |         return DESTINATION;
26 |     }
27 | }
28 | 


--------------------------------------------------------------------------------
/src/main/resources/META-INF/resources/templates/screens/project/project_bundle_tabs.vm:
--------------------------------------------------------------------------------
 1 | 
2 |
3 |
4 |
5 |
6 | #set($bundles = $project.getBundles()) 7 | 26 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/CommandMountResolutionException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.nrg.containers.model.command.auto.Command.CommandMount; 4 | 5 | public class CommandMountResolutionException extends CommandResolutionException { 6 | final CommandMount mount; 7 | 8 | public CommandMountResolutionException(final String message) { 9 | this(message, null, null); 10 | } 11 | 12 | public CommandMountResolutionException(final String message, final CommandMount mount) { 13 | this(message, mount, null); 14 | } 15 | 16 | public CommandMountResolutionException(final String message, final Throwable cause) { 17 | this(message, null, cause); 18 | } 19 | 20 | public CommandMountResolutionException(final String message, final CommandMount mount, final Throwable cause) { 21 | super(message, cause); 22 | this.mount = mount; 23 | } 24 | 25 | public CommandMount getMount() { 26 | return mount; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/mountTests/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "description": "a dummy command that does nothing", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "echo hello world", 7 | "mounts": [ 8 | { 9 | "name": "mount", 10 | "writable": "false", 11 | "path": "/input", 12 | "required": true 13 | } 14 | ], 15 | "xnat": [ 16 | { 17 | "name": "resource-wrapper", 18 | "label": "Hello world: Resource", 19 | "description": "run the dummy command with a resource, which we mount", 20 | "external-inputs": [ 21 | { 22 | "name": "resource", 23 | "description": "a resource", 24 | "type": "Resource", 25 | "required": true, 26 | "provides-files-for-command-mount": "mount" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/rest/ContainerLogPollResponse.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.rest; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class ContainerLogPollResponse { 8 | @JsonIgnore public static final String LOG_COMPLETE_TIMESTAMP = "-1"; 9 | private final String content; 10 | private final boolean fromFile; 11 | private final String timestamp; 12 | private final long bytesRead; 13 | 14 | public static ContainerLogPollResponse fromFile(final String content) { 15 | return new ContainerLogPollResponse(content, true, LOG_COMPLETE_TIMESTAMP, -1); 16 | } 17 | 18 | public static ContainerLogPollResponse fromLive(final String content, final String timestamp) { 19 | return new ContainerLogPollResponse(content, false, timestamp, -1); 20 | } 21 | 22 | public static ContainerLogPollResponse fromComplete(final String content) { 23 | return new ContainerLogPollResponse(content, false, LOG_COMPLETE_TIMESTAMP, -1); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testPathTranslation/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "description": "a dummy command that does nothing", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "echo hello world", 7 | "mounts": [ 8 | { 9 | "name": "mount", 10 | "writable": "false", 11 | "path": "/a/path", 12 | "required": true 13 | } 14 | ], 15 | "xnat": [ 16 | { 17 | "name": "resource-wrapper", 18 | "label": "Hello world: Resource", 19 | "description": "run the dummy command with a resource, which we mount", 20 | "external-inputs": [ 21 | { 22 | "name": "resource", 23 | "description": "a resource", 24 | "type": "Resource", 25 | "required": true, 26 | "provides-files-for-command-mount": "mount" 27 | } 28 | ] 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/mountTests/command-writable-mount.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "description": "a dummy command that does nothing", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "echo hello world", 7 | "mounts": [ 8 | { 9 | "name": "mount", 10 | "writable": true, 11 | "path": "/input", 12 | "required": true 13 | } 14 | ], 15 | "xnat": [ 16 | { 17 | "name": "resource-wrapper", 18 | "label": "Hello world: Resource", 19 | "description": "run the dummy command with a resource, which we mount", 20 | "external-inputs": [ 21 | { 22 | "name": "resource", 23 | "description": "a resource", 24 | "type": "Resource", 25 | "required": true, 26 | "provides-files-for-command-mount": "mount" 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/container-service 5 | docker: 6 | - image: circleci/openjdk:8 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: cache-{{ .Branch }}-{{ checksum "build.gradle" }} 11 | - run: 12 | name: Refresh dependencies 13 | command: ./gradlew --refresh-dependencies dependencies 14 | - run: 15 | name: Compile 16 | command: ./gradlew compileJava compileTestJava 17 | - save_cache: 18 | key: cache-{{ .Branch }}-{{ checksum "build.gradle" }} 19 | paths: 20 | - ~/.gradle 21 | - setup_remote_docker 22 | - run: 23 | name: Run tests 24 | command: ./gradlew test 25 | - store_test_results: 26 | path: build/test-results/ 27 | - store_artifacts: 28 | path: build/reports/tests/ 29 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/CommandValidationException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | 5 | import java.util.List; 6 | 7 | public class CommandValidationException extends Exception { 8 | private ImmutableList errors; 9 | 10 | public CommandValidationException(final List errors) { 11 | super(); 12 | setErrors(errors); 13 | } 14 | 15 | public CommandValidationException(final String error) { 16 | super(); 17 | this.errors = error == null ? ImmutableList.of() : ImmutableList.of(error); 18 | } 19 | 20 | public CommandValidationException(final List errors, final Throwable e) { 21 | super(e); 22 | setErrors(errors); 23 | } 24 | 25 | private void setErrors(final List errors) { 26 | this.errors = errors == null ? ImmutableList.of() : ImmutableList.copyOf(errors); 27 | } 28 | 29 | public List getErrors() { 30 | return errors; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/daos/DockerHubDao.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.daos; 2 | 3 | import org.nrg.containers.exceptions.NotUniqueException; 4 | import org.nrg.containers.model.dockerhub.DockerHubEntity; 5 | import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public class DockerHubDao extends AbstractHibernateDAO { 10 | public DockerHubEntity findByName(final String name) throws NotUniqueException { 11 | try { 12 | return findByUniqueProperty("name", name); 13 | } catch (RuntimeException e) { 14 | throw new NotUniqueException("More than one result with name " + name + "."); 15 | } 16 | } 17 | 18 | public DockerHubEntity findByUrl(final String url) throws NotUniqueException { 19 | try { 20 | return findByUniqueProperty("url", url); 21 | } catch (RuntimeException e) { 22 | throw new NotUniqueException("More than one result with url " + url + "."); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/CommandEventMappingService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | 4 | import org.nrg.containers.model.CommandEventMapping; 5 | import org.nrg.framework.exceptions.NotFoundException; 6 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 7 | 8 | import java.util.List; 9 | 10 | 11 | public interface CommandEventMappingService extends BaseHibernateService { 12 | void convert(long id) throws Exception; 13 | @Deprecated void enable(long id) throws NotFoundException; 14 | @Deprecated void enable(CommandEventMapping commandEventMapping); 15 | @Deprecated void disable(long id) throws NotFoundException; 16 | @Deprecated void disable(CommandEventMapping commandEventMapping); 17 | 18 | List findByEventType(String eventType); 19 | List findByEventType(String eventType, boolean onlyEnabled); 20 | 21 | List findByProject(String project); 22 | List findByProject(String project, boolean onlyEnabled); 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/dockerRestApiTest/commands.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "name": "label-test", 3 | "description": "Command to test label-parsing and command-importing code", 4 | "type": "docker", 5 | "command-line": "#CMD#", 6 | "inputs": [ 7 | { 8 | "name": "CMD", 9 | "description": "Command to run", 10 | "required": true 11 | } 12 | ] 13 | }, 14 | { 15 | "name": "dcm2niix-scan", 16 | "label": "Label test: dcm2niix", 17 | "description": "Run dcm2niix on a scan's DICOMs", 18 | "type": "docker", 19 | "command-line": "/run/dcm2niix-scan.sh #scanId# #sessionId#", 20 | "mounts": [ 21 | { 22 | "name": "DICOM", 23 | "path": "/input" 24 | }, 25 | { 26 | "name": "NIFTI", 27 | "path": "/output" 28 | } 29 | ], 30 | "inputs": [ 31 | { 32 | "name": "scanId", 33 | "required": true 34 | }, 35 | { 36 | "name": "sessionId", 37 | "required":true 38 | } 39 | ] 40 | }] 41 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/model/xnat/OuterTestPojo.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.xnat; 2 | 3 | import com.google.common.base.MoreObjects; 4 | 5 | import java.util.Objects; 6 | 7 | public class OuterTestPojo { 8 | private InnerTestPojo outerKey1; 9 | 10 | public InnerTestPojo getOuterKey1() { 11 | return outerKey1; 12 | } 13 | 14 | public void setOuterKey1(final InnerTestPojo outerKey1) { 15 | this.outerKey1 = outerKey1; 16 | } 17 | 18 | @Override 19 | public boolean equals(final Object o) { 20 | if (this == o) return true; 21 | if (o == null || getClass() != o.getClass()) return false; 22 | final OuterTestPojo that = (OuterTestPojo) o; 23 | return Objects.equals(this.outerKey1, that.outerKey1); 24 | } 25 | 26 | @Override 27 | public int hashCode() { 28 | return Objects.hash(outerKey1); 29 | } 30 | 31 | @Override 32 | public String toString() { 33 | return MoreObjects.toStringHelper(this) 34 | .add("key1", outerKey1) 35 | .toString(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/orchestration/entity/OrchestrationProjectEntity.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.orchestration.entity; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; 5 | 6 | import javax.persistence.*; 7 | import javax.validation.constraints.NotNull; 8 | 9 | @Entity 10 | @Slf4j 11 | public class OrchestrationProjectEntity extends AbstractHibernateEntity { 12 | private String projectId; 13 | private OrchestrationEntity orchestrationEntity; 14 | 15 | @NotNull 16 | @Column(unique = true) 17 | public String getProjectId() { 18 | return projectId; 19 | } 20 | 21 | public void setProjectId(String projectId) { 22 | this.projectId = projectId; 23 | } 24 | 25 | @ManyToOne(fetch = FetchType.LAZY, optional = false) 26 | public OrchestrationEntity getOrchestrationEntity() { 27 | return orchestrationEntity; 28 | } 29 | 30 | public void setOrchestrationEntity(OrchestrationEntity orchestrationEntity) { 31 | this.orchestrationEntity = orchestrationEntity; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/security/ContainerManagerUserAuthorization.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.security; 2 | 3 | import org.nrg.containers.utils.ContainerUtils; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.aspectj.lang.JoinPoint; 6 | import org.nrg.xapi.authorization.AbstractXapiAuthorization; 7 | import org.nrg.xdat.security.helpers.Roles; 8 | import org.nrg.xft.security.UserI; 9 | import javax.servlet.http.HttpServletRequest; 10 | import org.nrg.xdat.security.helpers.AccessLevel; 11 | 12 | 13 | import org.springframework.stereotype.Component; 14 | 15 | 16 | /** 17 | * Checks whether user has a ContainerManager Role. 18 | */ 19 | @Slf4j 20 | @Component 21 | public class ContainerManagerUserAuthorization extends AbstractXapiAuthorization { 22 | @Override 23 | protected boolean checkImpl(final AccessLevel accessLevel, final JoinPoint joinPoint, final UserI user, final HttpServletRequest request) { 24 | return Roles.checkRole(user, ContainerUtils.CONTAINER_MANAGER_ROLE); 25 | } 26 | 27 | @Override 28 | protected boolean considerGuests() { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/ContainerFinalizeService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.apache.commons.lang3.tuple.Pair; 4 | import org.nrg.containers.model.container.auto.Container; 5 | import org.nrg.xft.security.UserI; 6 | 7 | import javax.annotation.Nullable; 8 | import java.util.List; 9 | 10 | public interface ContainerFinalizeService { 11 | Pair finalizeContainer(Container toFinalize, 12 | UserI userI, 13 | boolean isFailed, final List wrapupContainers); 14 | 15 | void sendContainerStatusUpdateEmail(UserI user, 16 | boolean completionStatus, 17 | String pipelineName, 18 | String xnatId, 19 | String xnatLabel, 20 | String project, 21 | @Nullable List filePaths); 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/jms/requests/ContainerRequest.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.jms.requests; 2 | 3 | 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | abstract public class ContainerRequest { 7 | public static String inQueueStatusPrefix = "_"; 8 | 9 | /** 10 | * Returns true if workflow status indicates that the request is in the JMS queue 11 | * @param workflowStatus the workflow status 12 | * @return T/F 13 | */ 14 | public boolean inJMSQueue(String workflowStatus) { 15 | // We'd prefer to store the workflow & user as part of the request but since they're not serializable, we don't 16 | // have access to them. Thus, whatever calls this method has to do the work of retrieving the workflow status 17 | return StringUtils.startsWith(workflowStatus, inQueueStatusPrefix); 18 | } 19 | 20 | /** 21 | * Return the status that'll indicate that this request is in the JMS queue 22 | * @param workflowStatus the current status 23 | * @return the status indicating queued 24 | */ 25 | public String makeJMSQueuedStatus(String workflowStatus) { 26 | return inQueueStatusPrefix + workflowStatus; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootCategory=DEBUG, container-service-test 2 | 3 | log4j.appender.container-service-test=org.apache.log4j.FileAppender 4 | log4j.appender.container-service-test.File=container-service-test.log 5 | log4j.appender.container-service-test.layout=org.apache.log4j.PatternLayout 6 | log4j.appender.container-service-test.layout.ConversionPattern=%d{ABSOLUTE} %5p %40.40c:%4L [%tid] - %m%n 7 | 8 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 9 | log4j.appender.stdout.Target=System.out 10 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 11 | log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L [%tid] - %m%n 12 | 13 | # Root logger option 14 | log4j.rootLogger=INFO, stdout 15 | 16 | log4j.logger.org.nrg.containers=DEBUG 17 | log4j.logger.org.nrg.transporter=DEBUG 18 | 19 | #log4j.logger.org.nrg.framework.orm=DEBUG 20 | 21 | # Log everything. Good for troubleshooting 22 | #log4j.logger.org.hibernate=DEBUG 23 | 24 | # Log all JDBC parameters 25 | #log4j.logger.org.hibernate.type=ALL 26 | 27 | #log4j.logger.org.springframework.security=DEBUG 28 | #log4j.logger.org.nrg.xdat=DEBUG -------------------------------------------------------------------------------- /src/test/resources/setupCommand/command-with-setup-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "command-with-setup-command", 3 | "description": "A command with a setup command", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "ls /input", 7 | "inputs": [], 8 | "mounts": [ 9 | { 10 | "name": "input", 11 | "path": "/input" 12 | } 13 | ], 14 | "xnat": [ 15 | { 16 | "name": "wrapper", 17 | "label": "Command with setup command", 18 | "description": "Insert a setup command before the command", 19 | "external-inputs": [ 20 | { 21 | "name": "resource", 22 | "description": "A fake resource, which will be mounted", 23 | "type": "Resource", 24 | "required": true, 25 | "provides-files-for-command-mount": "input", 26 | "via-setup-command": "xnat/test-setup-command:latest:setup-command" 27 | } 28 | ], 29 | "derived-inputs": [], 30 | "output-handlers": [] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/Dockerfile.main: -------------------------------------------------------------------------------- 1 | FROM busybox:latest 2 | COPY main-command-script.sh /usr/local/bin/ 3 | LABEL org.nrg.commands="[{\"inputs\": [], \"name\": \"command-with-wrapup-command\", \"command-line\": \"main-command-script.sh\", \"outputs\": [{\"mount\": \"out\", \"name\": \"output\"}], \"image\": \"xnat/test-wrapup-command-main:latest\", \"xnat\": [{\"output-handlers\": [{\"accepts-command-output\": \"output\", \"label\": \"WRAPUP\", \"via-wrapup-command\": \"xnat/test-wrapup-command:latest:wrapup-command\", \"name\": \"output-handler\", \"as-a-child-of\": \"session\"}], \"derived-inputs\": [{\"type\": \"Resource\", \"name\": \"resource\", \"provides-files-for-command-mount\": \"in\", \"description\": \"A fake resource, which will be mounted\", \"derived-from-wrapper-input\": \"session\"}], \"name\": \"wrapper\", \"external-inputs\": [{\"required\": true, \"type\": \"Session\", \"name\": \"session\", \"description\": \"A session\"}], \"description\": \"Use a wrapup command on the output handler\"}], \"mounts\": [{\"path\": \"/input\", \"name\": \"in\"}, {\"writable\": true, \"path\": \"/output\", \"name\": \"out\"}], \"type\": \"docker\", \"description\": \"A command with a wrapup command\"}]" 4 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/configuration/ProjectEnabledReport.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.configuration; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.google.auto.value.AutoValue; 6 | 7 | import java.io.Serializable; 8 | 9 | @AutoValue 10 | public abstract class ProjectEnabledReport implements Serializable { 11 | private static final long serialVersionUID = -6360752238770532920L; 12 | 13 | @JsonProperty("enabled-for-site") public abstract Boolean enabledForSite(); 14 | @JsonProperty("enabled-for-project") public abstract Boolean enabledForProject(); 15 | @JsonProperty("project") public abstract String project(); 16 | 17 | @JsonCreator 18 | public static ProjectEnabledReport create(@JsonProperty("enabled-for-site") final Boolean enabledForSite, 19 | @JsonProperty("enabled-for-project") final Boolean enabledForProject, 20 | @JsonProperty("project") final String project) { 21 | return new AutoValue_ProjectEnabledReport(enabledForSite, enabledForProject, project); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/EnvironmentVariableResolvedSecret.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import org.nrg.containers.exceptions.ContainerServiceSecretException; 4 | 5 | import java.util.Optional; 6 | 7 | @ResolverFor(destination = EnvironmentVariableSecretDestination.class) 8 | public class EnvironmentVariableResolvedSecret extends ResolvedSecret.WithValue { 9 | @SuppressWarnings("unused") 10 | public EnvironmentVariableResolvedSecret( 11 | final Secret secret, 12 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType") final Optional value 13 | ) throws ContainerServiceSecretException { 14 | super(secret, value); 15 | } 16 | 17 | public EnvironmentVariableResolvedSecret(final Secret secret, final String value){ 18 | super(secret, value); 19 | } 20 | 21 | public String envName() { 22 | return destination().identifier(); 23 | } 24 | 25 | public String envValue() { 26 | return value(); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "EnvironmentVariableResolvedSecret{" + 32 | "envName=\"" + envName() + "\"" + 33 | "}"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/CommandConfigurationTestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.mockito.Mockito; 5 | import org.nrg.config.services.ConfigService; 6 | import org.nrg.containers.services.ContainerConfigService; 7 | import org.nrg.containers.services.OrchestrationEntityService; 8 | import org.nrg.containers.services.OrchestrationProjectEntityService; 9 | import org.nrg.containers.services.impl.ContainerConfigServiceImpl; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.Import; 13 | 14 | @Configuration 15 | @Import({ObjectMapperConfig.class}) 16 | public class CommandConfigurationTestConfig { 17 | @Bean 18 | public ConfigService configService() { 19 | return Mockito.mock(ConfigService.class); 20 | } 21 | 22 | @Bean 23 | public ContainerConfigService containerConfigService(ConfigService configService, ObjectMapper mapper) { 24 | return new ContainerConfigServiceImpl(configService, mapper, Mockito.mock(OrchestrationProjectEntityService.class), Mockito.mock(OrchestrationEntityService.class)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/api/KubernetesClient.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.api; 2 | 3 | import io.kubernetes.client.openapi.ApiClient; 4 | import org.nrg.containers.exceptions.ContainerBackendException; 5 | import org.nrg.containers.exceptions.ContainerException; 6 | import org.nrg.containers.model.container.auto.Container; 7 | import org.nrg.framework.exceptions.NotFoundException; 8 | 9 | import java.time.OffsetDateTime; 10 | 11 | public interface KubernetesClient { 12 | ApiClient getBackendClient(); 13 | void setBackendClient(ApiClient apiClient); 14 | String getNamespace(); 15 | void setNamespace(String namespace); 16 | 17 | void start(); 18 | void stop(); 19 | 20 | String ping() throws ContainerBackendException; 21 | String getLog(String podName, final LogType logType, final Boolean withTimestamp, final OffsetDateTime since) throws ContainerBackendException; 22 | 23 | String createJob(final Container toCreate, final DockerControlApi.NumReplicas numReplicas, String serverContainerUser, final String gpuVendor) 24 | throws ContainerBackendException, ContainerException; 25 | void unsuspendJob(final String jobName) throws ContainerBackendException; 26 | void removeJob(String jobName) throws NotFoundException, ContainerBackendException; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/ServiceTaskEvent.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | import org.nrg.containers.model.container.auto.Container; 5 | import org.nrg.containers.model.container.auto.ServiceTask; 6 | import org.nrg.framework.event.EventI; 7 | 8 | import javax.annotation.Nonnull; 9 | 10 | @AutoValue 11 | public abstract class ServiceTaskEvent implements EventI { 12 | private static final long serialVersionUID = 5423148597895355320L; 13 | 14 | public static final String QUEUE = "serviceTaskEventQueue"; 15 | 16 | public abstract ServiceTask task(); 17 | public abstract Container service(); 18 | public abstract EventType eventType(); 19 | 20 | public enum EventType { 21 | ProcessTask, 22 | Restart, 23 | Waiting 24 | } 25 | 26 | public static ServiceTaskEvent create(final @Nonnull ServiceTask task, final @Nonnull Container service) { 27 | return create(task, service, EventType.ProcessTask); 28 | } 29 | public static ServiceTaskEvent create(final @Nonnull ServiceTask task, final @Nonnull Container service, 30 | final EventType eventType) { 31 | return new AutoValue_ServiceTaskEvent(task, service, eventType); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/command/entity/CommandType.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.command.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | 6 | import java.util.EnumSet; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | public enum CommandType { 11 | 12 | DOCKER("docker"), 13 | DOCKER_SETUP("docker-setup"), 14 | DOCKER_WRAPUP("docker-wrapup"); 15 | 16 | private final String name; 17 | 18 | @JsonCreator 19 | CommandType(final String name) { 20 | this.name = name; 21 | } 22 | 23 | @JsonValue 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public String shortName() { 29 | // Remove docker 30 | return name.substring("docker".length()); 31 | } 32 | 33 | public static List names() { 34 | return EnumSet.allOf(CommandType.class).stream().map(CommandType::getName).collect(Collectors.toList()); 35 | } 36 | 37 | public static CommandType withName(final String name) { 38 | for (final CommandType type : EnumSet.allOf(CommandType.class)) { 39 | if (type.name.equals(name)) { 40 | return type; 41 | } 42 | } 43 | return null; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/impl/OrchestrationProjectEntityServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services.impl; 2 | 3 | import org.nrg.containers.daos.OrchestrationProjectEntityDao; 4 | import org.nrg.containers.model.orchestration.entity.OrchestrationProjectEntity; 5 | import org.nrg.containers.services.OrchestrationProjectEntityService; 6 | import org.nrg.framework.orm.hibernate.AbstractHibernateEntityService; 7 | import org.springframework.stereotype.Service; 8 | 9 | import javax.annotation.Nullable; 10 | import javax.transaction.Transactional; 11 | 12 | @Transactional 13 | @Service 14 | public class OrchestrationProjectEntityServiceImpl extends AbstractHibernateEntityService implements OrchestrationProjectEntityService { 15 | @Override 16 | @Nullable 17 | public OrchestrationProjectEntity find(String project) { 18 | return getDao().findByUniqueProperty("projectId", project); 19 | } 20 | 21 | @Override 22 | public synchronized void checkAndDisable(String project, long wrapperId) { 23 | OrchestrationProjectEntity ope = find(project); 24 | if (ope == null) { 25 | return; 26 | } 27 | if (ope.getOrchestrationEntity().usesWrapper(wrapperId)) { 28 | delete(ope); 29 | } 30 | flush(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/ScanArchiveEventToLaunchCommands.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | import com.google.auto.value.AutoAnnotation; 4 | import com.google.auto.value.AutoValue; 5 | import org.nrg.containers.model.xnat.Scan; 6 | import org.nrg.framework.event.EventI; 7 | import org.nrg.xft.security.UserI; 8 | 9 | import java.util.HashSet; 10 | 11 | @AutoValue 12 | public abstract class ScanArchiveEventToLaunchCommands implements EventI { 13 | private static final long serialVersionUID = 6052791990440957304L; 14 | 15 | public static final String QUEUE = "scanArchiveEventToLaunchCommandsQueue"; 16 | 17 | public abstract Scan scan(); 18 | public abstract String project(); 19 | public abstract UserI user(); 20 | 21 | public static ScanArchiveEventToLaunchCommands create(final Scan scan, 22 | final String project, 23 | final UserI user) { 24 | return new AutoValue_ScanArchiveEventToLaunchCommands(scan, project, user); 25 | } 26 | 27 | public static ScanArchiveEventToLaunchCommands create(final Scan scan, 28 | final UserI user) { 29 | return create(scan, scan.getProject(user, false, new HashSet<>()).getId(), user); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/configuration/PluginVersionCheck.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.configuration; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.google.auto.value.AutoValue; 5 | 6 | import javax.annotation.Nullable; 7 | import java.io.Serializable; 8 | 9 | @AutoValue 10 | public abstract class PluginVersionCheck implements Serializable { 11 | private static final long serialVersionUID = 270432226185468124L; 12 | 13 | @JsonProperty("compatible") public abstract Boolean compatible(); 14 | @Nullable @JsonProperty("xnat-version-detected") public abstract String xnatVersionDetected(); 15 | @Nullable @JsonProperty("min-xnat-version-required") public abstract String xnatVersionRequired(); 16 | @Nullable @JsonProperty("message") public abstract String message(); 17 | 18 | public static Builder builder() {return new AutoValue_PluginVersionCheck.Builder();} 19 | 20 | @AutoValue.Builder 21 | public abstract static class Builder { 22 | public abstract Builder compatible(Boolean compatible); 23 | 24 | public abstract Builder xnatVersionDetected(String xnatVersionDetected); 25 | 26 | public abstract Builder xnatVersionRequired(String xnatVersionRequired); 27 | 28 | public abstract Builder message(String message); 29 | 30 | public abstract PluginVersionCheck build(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/orchestration/auto/OrchestrationProject.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.orchestration.auto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | @JsonInclude 10 | public class OrchestrationProject { 11 | private List availableOrchestrations = new ArrayList<>(); 12 | private Long selectedOrchestrationId; 13 | 14 | public OrchestrationProject() {} 15 | 16 | public OrchestrationProject(List availableOrchestrations, Long selectedOrchestrationId) { 17 | this.availableOrchestrations = availableOrchestrations; 18 | this.selectedOrchestrationId = selectedOrchestrationId; 19 | } 20 | 21 | public List getAvailableOrchestrations() { 22 | return availableOrchestrations; 23 | } 24 | 25 | public void setAvailableOrchestrations(List availableOrchestrations) { 26 | this.availableOrchestrations = availableOrchestrations; 27 | } 28 | 29 | @Nullable 30 | public Long getSelectedOrchestrationId() { 31 | return selectedOrchestrationId; 32 | } 33 | 34 | public void setSelectedOrchestrationId(Long selectedOrchestrationId) { 35 | this.selectedOrchestrationId = selectedOrchestrationId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/command/entity/CommandVisibility.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.command.entity; 2 | 3 | import java.util.EnumSet; 4 | import java.util.List; 5 | import java.util.stream.Collectors; 6 | import com.fasterxml.jackson.annotation.JsonCreator; 7 | import com.fasterxml.jackson.annotation.JsonValue; 8 | import org.omg.CORBA.PRIVATE_MEMBER; 9 | 10 | 11 | public enum CommandVisibility { 12 | 13 | PUBLIC_CONTAINER("public"), 14 | PRIVATE_CONTAINER("private"), 15 | PROTECTED_CONTAINER("protected"); 16 | 17 | private final String visibilityType; 18 | 19 | @JsonCreator 20 | CommandVisibility(final String visibilityType) { 21 | this.visibilityType = visibilityType; 22 | } 23 | 24 | @JsonValue 25 | public String getVisibilityType() { 26 | return visibilityType; 27 | } 28 | 29 | public static List visibilityTypes() { 30 | return EnumSet.allOf(CommandVisibility.class).stream().map(CommandVisibility::getVisibilityType).collect(Collectors.toList()); 31 | } 32 | 33 | public static CommandVisibility withVisibilityType(final String visibilityType) { 34 | for (final CommandVisibility type : EnumSet.allOf(CommandVisibility.class)) { 35 | if (type.visibilityType.equals(visibilityType)) { 36 | return type; 37 | } 38 | } 39 | return null; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/ObjectMapperConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.databind.SerializationFeature; 7 | import com.fasterxml.jackson.datatype.guava.GuavaModule; 8 | import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 12 | 13 | @Configuration 14 | public class ObjectMapperConfig { 15 | @Bean 16 | public Jackson2ObjectMapperBuilder objectMapperBuilder() { 17 | return new Jackson2ObjectMapperBuilder() 18 | .serializationInclusion(JsonInclude.Include.NON_NULL) 19 | .failOnEmptyBeans(false) 20 | .featuresToEnable(JsonParser.Feature.ALLOW_SINGLE_QUOTES, JsonParser.Feature.ALLOW_YAML_COMMENTS) 21 | .featuresToDisable(SerializationFeature.FAIL_ON_EMPTY_BEANS, SerializationFeature.WRITE_NULL_MAP_VALUES) 22 | .modulesToInstall(new Hibernate5Module(), new GuavaModule()); 23 | } 24 | 25 | @Bean 26 | public ObjectMapper objectMapper() { 27 | return objectMapperBuilder().build(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/DockerServerEntityTestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import org.mockito.Mockito; 4 | import org.nrg.containers.api.KubernetesClientFactoryImpl; 5 | import org.nrg.containers.daos.DockerServerEntityRepository; 6 | import org.nrg.containers.services.DockerServerEntityService; 7 | import org.nrg.containers.services.DockerServerService; 8 | import org.nrg.containers.services.impl.DockerServerServiceImpl; 9 | import org.nrg.containers.services.impl.HibernateDockerServerEntityService; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.Import; 13 | 14 | @Configuration 15 | @Import({HibernateConfig.class, ObjectMapperConfig.class}) 16 | public class DockerServerEntityTestConfig { 17 | @Bean 18 | public DockerServerEntityService dockerServerEntityService() { 19 | return new HibernateDockerServerEntityService(); 20 | } 21 | 22 | @Bean 23 | public DockerServerService dockerServerService(final DockerServerEntityService dockerServerEntityService) { 24 | return new DockerServerServiceImpl(dockerServerEntityService, Mockito.mock(KubernetesClientFactoryImpl.class)); 25 | } 26 | 27 | @Bean 28 | public DockerServerEntityRepository dockerServerEntityRepository() { 29 | return new DockerServerEntityRepository(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/xnat/turbine/modules/screens/AdminContainerService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * org.nrg.xnat.turbine.modules.screens.AdminContainerService 3 | * XNAT http://www.xnat.org 4 | * Copyright (c) 2014, Washington University School of Medicine 5 | * All Rights Reserved 6 | * 7 | * Released under the Simplified BSD. 8 | * 9 | * Last modified 7/10/13 9:04 PM 10 | */ 11 | package org.nrg.xnat.turbine.modules.screens; 12 | 13 | import org.apache.turbine.util.RunData; 14 | import org.apache.velocity.context.Context; 15 | import org.nrg.xdat.turbine.modules.screens.AdminScreen; 16 | 17 | public class AdminContainerService extends AdminScreen { 18 | @Override 19 | protected void doBuildTemplate(RunData data, Context context) throws Exception { 20 | /* 21 | ArcArchivespecification arcSpec = ArcSpecManager.GetInstance(); 22 | if (arcSpec == null) { 23 | arcSpec = ArcSpecManager.initialize(TurbineUtils.getUser(data)); 24 | } 25 | if (!ArcSpecManager.HasPersisted()) { 26 | context.put("initialize", true); 27 | } else { 28 | context.put("initialize", false); 29 | } 30 | context.put("arc", arcSpec); 31 | setDefaultTabs("siteInfo", "fileSystem", "registration", "notifications", "anonymization", "applet", "dicomReceiver"); 32 | cacheTabs(context, "configuration"); 33 | context.put("features", Features.getAllFeatures()); 34 | */ 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.appender.containers=org.apache.log4j.DailyRollingFileAppender 2 | log4j.appender.containers.File=${xnat.home}/logs/containers.log 3 | log4j.appender.containers.DatePattern='.'yyyy-MM-dd 4 | log4j.appender.containers.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.containers.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 6 | 7 | log4j.appender.spotify=org.apache.log4j.DailyRollingFileAppender 8 | log4j.appender.spotify.File=${xnat.home}/logs/containers-spotify.log 9 | log4j.appender.spotify.DatePattern='.'yyyy-MM-dd 10 | log4j.appender.spotify.layout=org.apache.log4j.PatternLayout 11 | log4j.appender.spotify.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 12 | 13 | log4j.appender.services=org.apache.log4j.DailyRollingFileAppender 14 | log4j.appender.services.File=${xnat.home}/logs/containers-services-commandresolution.log 15 | log4j.appender.services.DatePattern='.'yyyy-MM-dd 16 | log4j.appender.services.layout=org.apache.log4j.PatternLayout 17 | log4j.appender.services.layout.ConversionPattern=%d [%t] %-5p %c - %m%n 18 | 19 | log4j.category.org.nrg.containers=DEBUG, containers 20 | log4j.additivity.org.nrg.containers=false 21 | 22 | log4j.category.org.mandas=INFO, spotify 23 | log4j.additivity.org.mandas=false 24 | 25 | log4j.category.org.nrg.containers.services.impl.CommandResolutionServiceImpl=INFO, services 26 | log4j.additivity.org.nrg.containers.services.impl.CommandResolutionServiceImpl=false 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/SessionMergeOrArchiveEvent.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | import org.nrg.framework.event.EventI; 5 | import org.nrg.xdat.om.XnatImagesessiondata; 6 | import org.nrg.xft.security.UserI; 7 | 8 | @AutoValue 9 | public abstract class SessionMergeOrArchiveEvent implements EventI { 10 | private static final long serialVersionUID = 5840070113252303212L; 11 | 12 | public static final String QUEUE = "sessionMergeOrArchiveEventQueue"; 13 | 14 | public abstract XnatImagesessiondata session(); 15 | public abstract String project(); 16 | public abstract UserI user(); 17 | public abstract String eventId(); 18 | 19 | public static SessionMergeOrArchiveEvent create(final XnatImagesessiondata session, 20 | final String project, 21 | final UserI user, 22 | final String eventId) { 23 | return new AutoValue_SessionMergeOrArchiveEvent(session, project, user, eventId); 24 | } 25 | 26 | public static SessionMergeOrArchiveEvent create(final XnatImagesessiondata session, 27 | final UserI userI, 28 | final String eventId) { 29 | return create(session, session.getProject(), userI, eventId); 30 | } 31 | } -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testCommandWithGenericResourcesGpu/debug_command_with_gpu.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "debug-gpu", 3 | "description": "Used to ensure we can request GPU resources. Will not run.", 4 | "label": "Debug - GPU", 5 | "version": "1.0", 6 | "schema-version": "1.0", 7 | "type": "docker", 8 | "command-line": "#COMMAND#", 9 | "image": "xnat/debug-command:latest", 10 | "override-entrypoint": true, 11 | "generic-resources": { 12 | "gpu": "1" 13 | }, 14 | "inputs": [ 15 | { 16 | "name": "command", 17 | "description": "The command to run", 18 | "type": "string", 19 | "required": true, 20 | "default-value": "echo hello world", 21 | "replacement-key": "#COMMAND#" 22 | } 23 | ], 24 | "xnat": [ 25 | { 26 | "name": "debug-gpu", 27 | "description": "Run the debug gpu container with a project", 28 | "label": "Debug - GPU", 29 | "contexts": ["xnat:projectData"], 30 | "external-inputs": [ 31 | { 32 | "name": "project", 33 | "description": "Input project", 34 | "type": "Project", 35 | "required": true, 36 | "load-children": false 37 | } 38 | ], 39 | "derived-inputs": [], 40 | "output-handlers": [] 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/daos/OrchestrationEntityDao.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.daos; 2 | 3 | 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.hibernate.Criteria; 6 | import org.hibernate.criterion.Order; 7 | import org.hibernate.criterion.Restrictions; 8 | import org.nrg.containers.model.orchestration.entity.OrchestrationEntity; 9 | import org.nrg.framework.generics.GenericUtils; 10 | import org.nrg.framework.orm.hibernate.AbstractHibernateDAO; 11 | import org.springframework.stereotype.Repository; 12 | 13 | import java.util.List; 14 | 15 | @Slf4j 16 | @Repository 17 | public class OrchestrationEntityDao extends AbstractHibernateDAO { 18 | public List findAllWithDisabledAndOrder() { 19 | final Criteria criteria = getSession().createCriteria(getParameterizedType()); 20 | criteria.addOrder(Order.asc("id")); 21 | return GenericUtils.convertToTypedList(criteria.list(), getParameterizedType()); 22 | } 23 | 24 | public List findEnabledUsingWrapper(long wrapperId) { 25 | final Criteria criteria = getSession().createCriteria(getParameterizedType()); 26 | criteria.add(Restrictions.eq("enabled", true)); 27 | final Criteria wrapperCriteria = criteria.createCriteria("wrapperList"); 28 | final Criteria commandCriteria = wrapperCriteria.createCriteria("commandWrapperEntity"); 29 | commandCriteria.add(Restrictions.eq("id", wrapperId)); 30 | return GenericUtils.convertToTypedList(criteria.list(), getParameterizedType()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/CommandEntityService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.command.entity.CommandEntity; 4 | import org.nrg.containers.model.command.entity.CommandWrapperEntity; 5 | import org.nrg.framework.exceptions.NotFoundException; 6 | import org.nrg.framework.exceptions.NrgRuntimeException; 7 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public interface CommandEntityService extends BaseHibernateService { 13 | List findByProperties(Map properties); 14 | 15 | CommandWrapperEntity addWrapper(CommandEntity commandEntity, CommandWrapperEntity wrapperToAdd); 16 | CommandWrapperEntity retrieveWrapper(long wrapperId); 17 | CommandWrapperEntity retrieveWrapper(long commandId, String wrapperName); 18 | CommandWrapperEntity getWrapper(long wrapperId) throws NotFoundException; 19 | CommandWrapperEntity getWrapper(long commandId, String wrapperName) throws NotFoundException; 20 | CommandWrapperEntity update(CommandWrapperEntity updates) throws NotFoundException; 21 | void deleteWrapper(long wrapperId); 22 | 23 | long getWrapperId(long commandId, String wrapperName) throws NotFoundException; 24 | 25 | CommandEntity getCommandByWrapperId(long wrapperId) throws NotFoundException; 26 | List getByImage(String image); 27 | void deleteByImage(String image); 28 | 29 | void throwExceptionIfCommandExists(CommandEntity commandEntity) throws NrgRuntimeException; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/OrchestrationService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.command.auto.Command; 4 | import org.nrg.containers.model.orchestration.auto.Orchestration; 5 | import org.nrg.containers.model.orchestration.auto.OrchestrationProject; 6 | import org.nrg.framework.exceptions.NotFoundException; 7 | import org.nrg.xft.security.UserI; 8 | 9 | import javax.annotation.Nullable; 10 | import java.util.List; 11 | 12 | public interface OrchestrationService { 13 | Orchestration createOrUpdate(Orchestration orchestration) throws NotFoundException; 14 | 15 | @Nullable 16 | Orchestration retrieve(final long orchestrationId) throws NotFoundException; 17 | 18 | @Nullable 19 | Orchestration findWhereWrapperIsFirst(Orchestration.OrchestrationIdentifier oi); 20 | 21 | @Nullable 22 | Command.CommandWrapper findNextWrapper(long orchestrationId, int currentStepIdx, long currentWrapperId) 23 | throws NotFoundException; 24 | 25 | void setEnabled(long id, boolean enabled, UserI user) throws NotFoundException, ContainerConfigService.CommandConfigurationException; 26 | 27 | List getAllPojos(); 28 | 29 | OrchestrationProject getAvailableForProject(String project); 30 | 31 | @Nullable 32 | Orchestration findForProject(String project); 33 | 34 | void setProjectOrchestration(String project, long orchestrationId, UserI user) throws NotFoundException, ContainerConfigService.CommandConfigurationException; 35 | 36 | void removeProjectOrchestration(String project); 37 | 38 | void delete(long id); 39 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/Secret.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.util.Objects; 8 | 9 | public class Secret { 10 | @Nonnull @JsonProperty("source") private final SecretSource source; 11 | @Nonnull @JsonProperty("destination") private final SecretDestination destination; 12 | 13 | @JsonCreator 14 | public Secret( 15 | @Nonnull @JsonProperty("source") final SecretSource source, 16 | @Nonnull @JsonProperty("destination") final SecretDestination destination 17 | ) { 18 | this.source = source; 19 | this.destination = destination; 20 | } 21 | 22 | public SecretSource source() { 23 | return source; 24 | } 25 | 26 | public SecretDestination destination() { 27 | return destination; 28 | } 29 | 30 | @Override 31 | public boolean equals(final Object o) { 32 | if (this == o) return true; 33 | if (o == null || getClass() != o.getClass()) return false; 34 | final Secret that = (Secret) o; 35 | return Objects.equals(source, that.source) && 36 | Objects.equals(destination, that.destination); 37 | } 38 | 39 | @Override 40 | public int hashCode() { 41 | return Objects.hash(source, destination); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "Secret{" + 47 | "source=" + source + 48 | ", destination=" + destination + 49 | "}"; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/model/xnat/InnerTestPojo.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.xnat; 2 | 3 | import com.google.common.base.MoreObjects; 4 | 5 | import java.util.Objects; 6 | 7 | public class InnerTestPojo { 8 | private String innerKey1; 9 | private String innerKey2; 10 | 11 | public InnerTestPojo() {} 12 | 13 | public InnerTestPojo(final String innerKey1, final String innerKey2) { 14 | this.innerKey1 = innerKey1; 15 | this.innerKey2 = innerKey2; 16 | } 17 | 18 | public String getInnerKey1() { 19 | return innerKey1; 20 | } 21 | 22 | public void setInnerKey1(final String innerKey1) { 23 | this.innerKey1 = innerKey1; 24 | } 25 | 26 | 27 | public String getInnerKey2() { 28 | return innerKey2; 29 | } 30 | 31 | public void setInnerKey2(final String innerKey2) { 32 | this.innerKey2 = innerKey2; 33 | } 34 | 35 | @Override 36 | public boolean equals(final Object o) { 37 | if (this == o) return true; 38 | if (o == null || getClass() != o.getClass()) return false; 39 | final InnerTestPojo that = (InnerTestPojo) o; 40 | return Objects.equals(this.innerKey1, that.innerKey1) && 41 | Objects.equals(this.innerKey2, that.innerKey2); 42 | } 43 | 44 | @Override 45 | public int hashCode() { 46 | return Objects.hash(innerKey1, innerKey2); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return MoreObjects.toStringHelper(this) 52 | .add("innerKey1", innerKey1) 53 | .add("innerKey2", innerKey2) 54 | .toString(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/jms/tasks/QueueManager.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.jms.tasks; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.containers.services.ContainerService; 5 | import org.nrg.xdat.security.helpers.Users; 6 | import org.nrg.xft.schema.XFTManager; 7 | import org.nrg.xft.security.UserI; 8 | import org.nrg.xnat.services.XnatAppInfo; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Slf4j 13 | @Component 14 | public class QueueManager implements Runnable { 15 | 16 | private final ContainerService containerService; 17 | private final XnatAppInfo xnatAppInfo; 18 | private boolean haveLoggedXftInitFailure = false; 19 | 20 | @Autowired 21 | public QueueManager(final ContainerService containerService, final XnatAppInfo appInfo) { 22 | this.containerService = containerService; 23 | this.xnatAppInfo = appInfo; 24 | } 25 | 26 | @Override 27 | public void run() { 28 | if (!xnatAppInfo.isPrimaryNode()) { 29 | return; 30 | } 31 | 32 | if (!XFTManager.isInitialized()) { 33 | if (!haveLoggedXftInitFailure) { 34 | log.info("XFT is not initialized, skipping queue manager task"); 35 | haveLoggedXftInitFailure = true; 36 | } 37 | return; 38 | } 39 | 40 | log.trace("Queue manager task running"); 41 | UserI user = Users.getAdminUser(); 42 | containerService.checkQueuedContainerJobs(user); 43 | containerService.checkWaitingContainerJobs(user); 44 | log.trace("Queue manager task done"); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/initialization/tasks/CheckContainerServiceVersion.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.initialization.tasks; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.containers.model.configuration.PluginVersionCheck; 5 | import org.nrg.containers.services.ContainerService; 6 | import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; 7 | import org.nrg.xnat.initialization.tasks.InitializingTaskException; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Slf4j 12 | @Component 13 | public class CheckContainerServiceVersion extends AbstractInitializingTask { 14 | private final ContainerService containerService; 15 | 16 | @Autowired 17 | public CheckContainerServiceVersion(final ContainerService containerService) { 18 | this.containerService = containerService; 19 | } 20 | 21 | @Override 22 | public String getTaskName() { 23 | return "Check XNAT version compatibility with this plugin."; 24 | } 25 | 26 | @Override 27 | protected void callImpl() throws InitializingTaskException { 28 | 29 | log.debug("Checking Container Service plugin / XNAT compatibility."); 30 | PluginVersionCheck versionCheck = containerService.checkXnatVersion(); 31 | if(versionCheck != null && versionCheck.compatible() == false){ 32 | log.error(versionCheck.message()); 33 | log.error("XNAT version: " + versionCheck.xnatVersionDetected()); 34 | } else if(log.isDebugEnabled() && versionCheck != null){ 35 | if(versionCheck.compatible()) log.debug("Version compatibility check: Passed"); 36 | } 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/kubernetes/KubernetesPodPhase.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.kubernetes; 2 | 3 | import org.apache.commons.lang.WordUtils; 4 | 5 | import javax.annotation.Nullable; 6 | import java.util.Collections; 7 | import java.util.Map; 8 | import java.util.function.Function; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | 12 | /** 13 | * See kubernetes docs 14 | * Pod Lifecycle: Pod Phase 15 | */ 16 | public enum KubernetesPodPhase { 17 | PENDING, 18 | RUNNING, 19 | SUCCEEDED, 20 | FAILED, 21 | UNKNOWN; 22 | 23 | private static final Map ENUM_MAP; 24 | static { 25 | Map map = Stream.of(KubernetesPodPhase.values()) 26 | .collect(Collectors.toMap(KubernetesPodPhase::toString, Function.identity())); 27 | ENUM_MAP = Collections.unmodifiableMap(map); 28 | } 29 | 30 | @Nullable 31 | public static KubernetesPodPhase fromString(String phase) { 32 | return ENUM_MAP.get(phase); 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return WordUtils.capitalizeFully(this.name()); 38 | } 39 | 40 | public static boolean isSuccessful(String phase) { 41 | final KubernetesPodPhase enumPhase = KubernetesPodPhase.fromString(phase); 42 | return enumPhase != null && enumPhase.isSuccessful(); 43 | } 44 | 45 | public boolean isSuccessful() { 46 | return SUCCEEDED == this; 47 | } 48 | 49 | public boolean isTerminal() { 50 | return SUCCEEDED == this || FAILED == this; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/scripts/xnat/plugin/containerService/containerConfig.css: -------------------------------------------------------------------------------- 1 | .imageContainer { 2 | margin: 0 0 2.5em; 3 | } 4 | .imageContainer:last-of-type { 5 | margin: 0; 6 | } 7 | .imageContainer h3 { 8 | border: none; 9 | /*margin-bottom: 10px;*/ 10 | overflow: auto; 11 | padding: 20px 0 0 2em; 12 | position: relative; 13 | 14 | } 15 | .imageContainer h3:before { 16 | font-family: "FontAwesome"; 17 | content: "\f0a0"; 18 | font-size: 1.5em; 19 | color: #808080; 20 | position: absolute; 21 | top: .9em; left: 1px; 22 | } 23 | .commandConfigInputRow { 24 | padding: 5px 0; 25 | } 26 | .commandConfigInputMain { 27 | display: inline-block; 28 | width: 70%; 29 | } 30 | .commandConfigInputToggle { 31 | display: inline-block; 32 | width: 30%; 33 | } 34 | 35 | .imageActionList { 36 | margin: .25em 0; 37 | padding: 0 0 0 2em; 38 | } 39 | .imageActionList li + li { 40 | margin-top: .5em; 41 | } 42 | 43 | /* enhancement to core XNAT switchbox style. Will be committed back into core code. */ 44 | label.switchbox.disabled { 45 | cursor: not-allowed; 46 | } 47 | label.switchbox.disabled > input + .switchbox-outer { 48 | background: #f3f3f3; 49 | border-color: #d3d3d3; 50 | box-shadow: none; 51 | } 52 | 53 | /* modal divider */ 54 | 55 | p.divider, 56 | div.xnat-dialog > .body p.divider { 57 | border-top: 1px solid #ccc; 58 | margin-top: 1em; 59 | margin-bottom: 2em; 60 | padding-top: 1em; 61 | } 62 | 63 | div.swarm-constraint { 64 | border: 1px solid #ccc; 65 | padding: 5px; 66 | margin: 5px; 67 | border-radius: 10px; 68 | } 69 | div.swarm-constraint a.close{ 70 | float: right; 71 | margin: 0 3px 0 0; 72 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/utils/ShellSplitter.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.utils; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * Intended to split a string into tokens the way a shell would. 8 | * Taken from https://gist.github.com/raymyers/8077031 9 | * 10 | * For use cases, see tests in org.nrg.containers.utils.ShellSplitterTest. 11 | */ 12 | public class ShellSplitter { 13 | public static List shellSplit(CharSequence string) { 14 | final List tokens = new ArrayList<>(); 15 | 16 | boolean escaping = false; 17 | char quoteChar = ' '; 18 | boolean quoting = false; 19 | StringBuilder current = new StringBuilder() ; 20 | for (int i = 0; i 0) { 34 | tokens.add(current.toString()); 35 | current = new StringBuilder(); 36 | } 37 | } else { 38 | current.append(c); 39 | } 40 | } 41 | if (current.length() > 0) { 42 | tokens.add(current.toString()); 43 | } 44 | return tokens; 45 | } 46 | } -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/testSessionScanMult/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "session1", 3 | "type": "Session", 4 | "label": "session1", 5 | "uri": "/experiments/session1", 6 | "scans": [ 7 | { 8 | "id": "scan1", 9 | "type": "Scan", 10 | "scan-type": "SCANTYPE", 11 | "uri": "/experiments/session1/scans/scan1", 12 | "frames": "12", 13 | "series-description": "great!", 14 | "modality": "super!", 15 | "quality": "the best!", 16 | "note": "I love it - me", 17 | "resources": [ 18 | { 19 | "id": 0, 20 | "type": "Resource", 21 | "label": "DICOM", 22 | "directory": "this will be overwritten at runtime", 23 | "uri": "/experiments/session1/scans/scan1/resources/0" 24 | } 25 | ] 26 | }, 27 | { 28 | "id": "scan2", 29 | "type": "Scan", 30 | "scan-type": "OTHER_SCANTYPE", 31 | "uri": "/experiments/session1/scans/scan2", 32 | "frames": "12", 33 | "series-description": "great!", 34 | "modality": "super!", 35 | "quality": "the best!", 36 | "note": "I love it - me", 37 | "resources": [ 38 | { 39 | "id": 0, 40 | "type": "Resource", 41 | "label": "DICOM", 42 | "directory": "this will be overwritten at runtime", 43 | "uri": "/experiments/session1/scans/scan2/resources/0" 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/ContainerPropertiesWithSecretValues.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.containers.model.container.auto.Container; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | @Slf4j 11 | public class ContainerPropertiesWithSecretValues { 12 | // TODO add more secret properties as they are implemented 13 | private final Map environmentVariables; 14 | 15 | private ContainerPropertiesWithSecretValues(final Map environmentVariables) { 16 | this.environmentVariables = environmentVariables; 17 | } 18 | 19 | public static ContainerPropertiesWithSecretValues prepareSecretsForLaunch(final Container container) { 20 | 21 | // Secrets 22 | final Map, List> secretsByType = container.secretsByType(); 23 | 24 | final Map environmentVariables = new HashMap<>(container.environmentVariables()); 25 | environmentVariables.putAll(SecretUtils.extractEnvironmentVariableSecrets(secretsByType)); 26 | 27 | // TODO add handling for more secret types as they are implemented 28 | 29 | secretsByType.forEach((clazz, resolvedSecrets) -> 30 | log.warn("Have not implemented handling for resolved secret type {}. Ignoring {} secret values.", 31 | clazz.getSimpleName(), resolvedSecrets.size() 32 | ) 33 | ); 34 | 35 | return new ContainerPropertiesWithSecretValues(environmentVariables); 36 | } 37 | 38 | public Map environmentVariables() { 39 | return environmentVariables; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/OrchestrationEntityService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.command.auto.Command; 4 | import org.nrg.containers.model.command.entity.CommandWrapperEntity; 5 | import org.nrg.containers.model.orchestration.auto.Orchestration; 6 | import org.nrg.containers.model.orchestration.auto.OrchestrationProject; 7 | import org.nrg.containers.model.orchestration.entity.OrchestrationEntity; 8 | import org.nrg.framework.exceptions.NotFoundException; 9 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 10 | 11 | import javax.annotation.Nullable; 12 | import java.util.List; 13 | 14 | public interface OrchestrationEntityService extends BaseHibernateService { 15 | 16 | @Nullable 17 | Orchestration findWhereWrapperIsFirst(String project, long wrapperId); 18 | 19 | Orchestration createOrUpdate(Orchestration orchestration, List wrapperList) 20 | throws NotFoundException; 21 | 22 | @Nullable 23 | Command.CommandWrapper findNextWrapper(long orchestrationId, int currentStepIdx, long currentWrapperId) 24 | throws NotFoundException; 25 | 26 | List setEnabled(long id, boolean enabled) throws NotFoundException; 27 | 28 | void setEnabled(OrchestrationEntity oe, boolean enabled); 29 | 30 | List getAllPojos(); 31 | 32 | OrchestrationProject getAvailableForProject(String project); 33 | 34 | List setProjectOrchestration(String project, long orchestrationId) throws NotFoundException; 35 | 36 | void removeProjectOrchestration(String project); 37 | 38 | void checkAndDisable(long wrapperId); 39 | 40 | @Nullable 41 | Orchestration findForProject(String project); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/SecretUtils.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | import java.util.stream.Collectors; 6 | import java.util.stream.Stream; 7 | 8 | public class SecretUtils { 9 | 10 | /** 11 | * Remove environment variable resolved secrets from resolved secrets map, return them as a name: value map 12 | * @param resolvedSecretsByClass Map from resolved secret classes to a list of secrets of that type 13 | * @return Map of environment variable secrets: name to value 14 | */ 15 | public static Map extractEnvironmentVariableSecrets(final Map, List> resolvedSecretsByClass) { 16 | return extractResolvedSecretType(EnvironmentVariableResolvedSecret.class, resolvedSecretsByClass) 17 | .collect(Collectors.toMap(EnvironmentVariableResolvedSecret::envName, EnvironmentVariableResolvedSecret::envValue)); 18 | } 19 | 20 | /** 21 | * Remove and return a particular type of ResolvedSecrets from the resolved secrets by type map 22 | * @param type A subclass of {@link ResolvedSecret} 23 | * @param resolvedSecretsByClass Map from resolved secret classes to a list of secrets of that type 24 | * @return A stream of the ResolvedSecrets of the specified type 25 | */ 26 | public static Stream extractResolvedSecretType( 27 | Class type, final Map, List> resolvedSecretsByClass 28 | ) { 29 | final List secretTs = resolvedSecretsByClass.remove(type); 30 | return secretTs == null ? Stream.empty() : secretTs.stream().map(type::cast); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/launching-containers-for-users.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | You can launch containers through the ordinary XNAT UI. 4 | 5 | First, a command must be added to your XNAT, and its wrapper must be enabled in your project. Then when you are on a page where a command and wrapper are available, a Run Containers menu will appear in the Actions box. 6 | 7 | SCREENSHOT Page with Run containers menu 8 | 9 | Inside the Run Containers menu are all the different command wrappers that can be used to launch a container with the object you are viewing. 10 | 11 | SCREENSHOT Open Run containers menu, show some wrappers inside 12 | 13 | Click on one of those command wrappers, and you will be presented with a Launch Container dialog. Here you can provide or customize the values of various arguments that will be used to launch the container. 14 | 15 | SCREENSHOT Launch container dialog 16 | 17 | After you submit, the container will be launched. 18 | 19 | ## A note on scan-level launching 20 | 21 | Some command wrappers are configured to launch on a particular scan. There is no User Interface in any existing XNAT version to launch containers from these command wrappers, because there are no buttons on the scan listings that can be customized in this way. 22 | 23 | However, there is an XNAT plugin that can provide this function: the [selectable table plugin](https://bitbucket.org/xnatdev/selectable-table-plugin). You can download this plugin jar (from the [downloads page](https://bitbucket.org/xnatdev/selectable-table-plugin/downloads/) in the repo) and install it into your XNAT. It will customize the scan tables and enable you to launch any command wrappers that are defined to launch on scans. You can even launch in bulk on multiple scans at once. 24 | 25 | SCREENSHOT Scan table with selectable table installed 26 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/api/KubernetesClientFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.api; 2 | 3 | import org.nrg.containers.exceptions.NoContainerServerException; 4 | import org.nrg.framework.services.NrgEventServiceI; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.io.IOException; 8 | import java.util.concurrent.ExecutorService; 9 | 10 | @Service 11 | public class KubernetesClientFactoryImpl implements KubernetesClientFactory { 12 | private final ExecutorService executorService; 13 | private final NrgEventServiceI eventService; 14 | 15 | private volatile KubernetesClientImpl kubernetesClient = null; 16 | 17 | public KubernetesClientFactoryImpl(final ExecutorService executorService, 18 | final NrgEventServiceI eventService) { 19 | this.executorService = executorService; 20 | this.eventService = eventService; 21 | } 22 | 23 | @Override 24 | public KubernetesClient getKubernetesClient() throws NoContainerServerException { 25 | if (kubernetesClient == null) { 26 | synchronized (this) { 27 | if (kubernetesClient == null) { 28 | try { 29 | kubernetesClient = new KubernetesClientImpl(executorService, eventService); 30 | } catch (IOException e) { 31 | throw new NoContainerServerException("Could not create kubernetes client", e); 32 | } 33 | } 34 | } 35 | } 36 | return kubernetesClient; 37 | } 38 | 39 | @Override 40 | public synchronized void shutdown() { 41 | if (kubernetesClient != null) { 42 | kubernetesClient.stop(); 43 | kubernetesClient = null; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/CommandTestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import org.mockito.Mockito; 4 | import org.nrg.containers.api.ContainerControlApi; 5 | import org.nrg.containers.services.ContainerEntityService; 6 | import org.nrg.framework.services.SerializerService; 7 | import org.nrg.xdat.preferences.SiteConfigPreferences; 8 | import org.nrg.xdat.security.services.PermissionsServiceI; 9 | import org.nrg.xdat.services.AliasTokenService; 10 | import org.nrg.xnat.services.archive.CatalogService; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.Import; 14 | 15 | @Configuration 16 | @Import({CommandConfig.class, HibernateConfig.class}) 17 | public class CommandTestConfig { 18 | @Bean 19 | public ContainerControlApi controlApi() { 20 | return Mockito.mock(ContainerControlApi.class); 21 | } 22 | 23 | @Bean 24 | public AliasTokenService aliasTokenService() { 25 | return Mockito.mock(AliasTokenService.class); 26 | } 27 | 28 | @Bean 29 | public SiteConfigPreferences siteConfigPreferences() { 30 | return Mockito.mock(SiteConfigPreferences.class); 31 | } 32 | 33 | @Bean 34 | public SerializerService serializerService() { 35 | return Mockito.mock(SerializerService.class); 36 | } 37 | 38 | @Bean 39 | public ContainerEntityService mockContainerEntityService() { 40 | return Mockito.mock(ContainerEntityService.class); 41 | } 42 | 43 | @Bean 44 | public PermissionsServiceI permissionsService() { 45 | return Mockito.mock(PermissionsServiceI.class); 46 | } 47 | 48 | @Bean 49 | public CatalogService catalogService() { 50 | return Mockito.mock(CatalogService.class); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XNAT Container Service 2 | 3 | [XNAT](http://www.xnat.org/) plugin for controlling containers (primarily [Docker](https://www.docker.com/) containers). 4 | 5 | To use it, you will need an XNAT running 1.8.0+. Get the `containers--fat` jar (through one of the methods below) and put it into your `{xnat.home}/plugins` directory. Restart tomcat and you are ready to run containers. See the [guide to getting started](https://wiki.xnat.org/display/CS/Getting+Started). 6 | 7 | This document is cross-posted on the [XNAT wiki](https://wiki.xnat.org/display/CS/Introduction) and the README in the [source repository](https://bitbucket.org/xnatdev/container-service). 8 | 9 | ## Getting the jar 10 | ### Download 11 | Releases are posted to the repository's [Downloads page](https://bitbucket.org/xnatdev/container-service/downloads/) on Bitbucket. Download the version you want (probably the latest release) and [deploy it to XNAT](#deploy-to-XNAT). 12 | 13 | ### Build the jar 14 | If you clone the source repository, you can build an XNAT plugin jar by running 15 | 16 | ``` 17 | [container-service] $ ./gradlew fatJar 18 | ``` 19 | 20 | The jar will be created as `build/libs/containers-${VERSION}-fat.jar` 21 | 22 | ## Deploy to XNAT 23 | One you [have a jar](#getting-the-jar), copy it to the `${xnat.home}/plugins` directory, and restart tomcat. 24 | 25 | Where is `${xnat.home}`? If you are using a VM generated by the [XNAT Vagrant project](https://bitbucket.org/xnatdev/xnat_vagrant), `xnat.home` is in the `~/${PROJECT}` directory (and the default value for `${PROJECT}` is `xnat`). If you aren't using the Vagrant project, or even if you are and you're still confused, then `${xnat.home}/logs` is where XNAT writes its logs; you'll want the `plugins` directory which should be right next to the `logs` directory. 26 | 27 | ## Contributing 28 | See [CONTRIBUTING.md](CONTRIBUTING.md). -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/ContainerEntityService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.events.model.ContainerEvent; 4 | import org.nrg.containers.model.container.entity.ContainerEntity; 5 | import org.nrg.containers.model.container.entity.ContainerEntityHistory; 6 | import org.nrg.framework.exceptions.NotFoundException; 7 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 8 | import org.nrg.xft.security.UserI; 9 | 10 | import java.util.List; 11 | 12 | public interface ContainerEntityService extends BaseHibernateService { 13 | ContainerEntity save(final ContainerEntity toCreate, 14 | final UserI userI); 15 | 16 | ContainerEntity retrieve(final String containerId); 17 | ContainerEntity get(final String containerId) throws NotFoundException; 18 | void delete(final String containerId); 19 | 20 | List getAll(Boolean nonfinalized, String project); 21 | List getAll(Boolean nonfinalized); 22 | 23 | 24 | List retrieveServices(); 25 | List retrieveNonfinalizedServices(); 26 | List retrieveContainersInFinalizingState(); 27 | List retrieveServicesInWaitingState(); 28 | 29 | int howManyContainersAreBeingFinalized(); 30 | 31 | List retrieveSetupContainersForParent(long parentId); 32 | List retrieveWrapupContainersForParent(long parentId); 33 | 34 | ContainerEntity addContainerEventToHistory(final ContainerEvent containerEvent, final UserI userI); 35 | ContainerEntityHistory addContainerHistoryItem(final ContainerEntity containerEntity, 36 | final ContainerEntityHistory history, final UserI userI); 37 | 38 | int howManyContainersAreWaiting(); 39 | } 40 | -------------------------------------------------------------------------------- /scripts/set_up_kubernetes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -o pipefail 4 | 5 | _usage=" 6 | Usage: $(basename $0) KUBECONFIG_OUT [NAMESPACE] 7 | 8 | Configure Kubernetes cluster namespace and service account for Container Service 9 | 10 | The following cluster configuration will be performed: 11 | * Create a namespace. You can pass an optional input to the script which will be used for the 12 | name of the Namespace, or use the default \"container-service\". 13 | * Create a ServiceAccount within the Namespace, named \"\${namespace}-account\". 14 | The Container Service will use this ServiceAccount to connect to Kubernetes. 15 | * Create Roles for the API permissions the Container Service needs 16 | * Assign those Roles to the ServiceAccount through RoleBindings. 17 | 18 | The credentials for the ServiceAccount will be written to a kubeconfig file at the path you 19 | specify. 20 | 21 | Requirements: 22 | * \`base64' must be installed 23 | * \`kubectl' must be installed 24 | * When \`kubectl' is invoked, the account must have create permissions on 25 | Namespaces, ServiceAccounts, Roles, ClusterRoles, and RoleBindings. 26 | An admin account, in short. 27 | " 28 | 29 | # Input 30 | kubeconfig=${1:?$_usage} 31 | namespace=${2:-"container-service"} 32 | 33 | # Help 34 | if [ $kubeconfig = "-h" -o $kubeconfig = "--help" ]; then 35 | echo $_usage 36 | exit 0 37 | fi 38 | 39 | # Find directory where this script is running so we can locate partner scripts. 40 | # Taken from https://stackoverflow.com/a/53122736 41 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 42 | 43 | # Apply cluster configuration 44 | echo "Configuring cluster" 45 | bash ${__dir}/configure_cluster_namespace_roles.sh "$namespace" 46 | 47 | # Create kubeconfig 48 | echo "Writing values to kubeconfig ${kubeconfig}" 49 | bash ${__dir}/create_kubeconfig.sh "$kubeconfig" "$namespace" 50 | 51 | echo "Done" 52 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/EnvironmentVariableSecretDestination.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | 7 | import java.util.Collections; 8 | import java.util.Map; 9 | import java.util.Objects; 10 | 11 | public class EnvironmentVariableSecretDestination implements SecretDestination { 12 | @JsonIgnore public static final String JSON_TYPE_NAME = "environment-variable"; 13 | 14 | @JsonProperty("identifier") private final String envName; 15 | 16 | @JsonCreator 17 | public EnvironmentVariableSecretDestination(@JsonProperty("identifier") final String envName) { 18 | this.envName = envName; 19 | } 20 | 21 | public String envName() { 22 | return envName; 23 | } 24 | 25 | @Override 26 | @JsonIgnore 27 | public String type() { 28 | return JSON_TYPE_NAME; 29 | } 30 | 31 | @Override 32 | public String identifier() { 33 | return envName; 34 | } 35 | 36 | @Override 37 | public Map otherProperties() { 38 | return Collections.emptyMap(); 39 | } 40 | 41 | @Override 42 | public boolean equals(final Object o) { 43 | if (this == o) return true; 44 | if (o == null || getClass() != o.getClass()) return false; 45 | final EnvironmentVariableSecretDestination that = (EnvironmentVariableSecretDestination) o; 46 | return Objects.equals(envName, that.envName); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return Objects.hash(envName); 52 | } 53 | 54 | @Override 55 | public String toString() { 56 | return "EnvironmentVariable{" + 57 | "name=\"" + envName + "\"" + 58 | "}"; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/resources/scripts/xnat/plugin/containerService/commandUI.css: -------------------------------------------------------------------------------- 1 | 2 | .panel .standard-settings { 3 | margin-bottom: 2em; 4 | } 5 | .panel .child-input { 6 | padding-bottom: 1em; 7 | } 8 | .panel .advanced-settings-container { 9 | } 10 | .panel .advanced-settings-toggle { 11 | border-bottom: 1px solid #ccc; 12 | text-align: center; 13 | } 14 | .panel .advanced-settings-toggle:before { 15 | background-color: #f0f0f0; 16 | content: "Advanced Settings"; 17 | color: #848484; 18 | cursor: pointer; 19 | display: inline-block; 20 | font-size: 10px; 21 | padding: 2px 6px; 22 | text-transform: uppercase; 23 | } 24 | .panel .advanced-settings-toggle.active:before { 25 | background-color: #e4efff; 26 | color: #4080cc; 27 | } 28 | .panel .advanced-settings { 29 | border: solid #ccc; 30 | border-width: 0 1px 1px; 31 | display: none; 32 | padding: 18px 0 6px; 33 | } 34 | .bulkRootList { 35 | height: 140px; 36 | overflow-y: auto; 37 | } 38 | 39 | .bulk-inputs { 40 | margin-bottom:15px; 41 | } 42 | 43 | #containerMenu > .bd { 44 | border-top: none; 45 | } 46 | 47 | 48 | #actionsMenu li.wrapped a { 49 | white-space: normal; 50 | width: 180px; 51 | } 52 | 53 | div.swarm-constraints .panel-element .element-label{ 54 | width:50%; 55 | } 56 | div.swarm-constraints .panel-element .element-wrapper { 57 | width:50%; 58 | } 59 | span.swarm-constraints-heading { 60 | background-color: #f0f0f0; 61 | color: #848484; 62 | display: inline-block; 63 | font-size: 10px; 64 | padding: 2px 6px; 65 | text-transform: uppercase; 66 | } 67 | 68 | kbd { 69 | padding: .2rem .4rem; 70 | font-size: 87.5%; 71 | color: #fff; 72 | background-color: #212529; 73 | border-radius: .2rem; 74 | display: inline-block; 75 | margin-top: 3px; 76 | } 77 | 78 | .spinner { 79 | margin: 0 10px 10px; 80 | } -------------------------------------------------------------------------------- /src/test/resources/wrapupCommand/command-with-wrapup-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "command-with-wrapup-command", 3 | "description": "A command with a wrapup command", 4 | "type": "docker", 5 | "image": "xnat/test-wrapup-command-main:latest", 6 | "command-line": "main-command-script.sh", 7 | "inputs": [], 8 | "mounts": [ 9 | { 10 | "name": "in", 11 | "path": "/input" 12 | }, 13 | { 14 | "name": "out", 15 | "path": "/output", 16 | "writable": true 17 | } 18 | ], 19 | "outputs": [ 20 | { 21 | "name": "output", 22 | "mount": "out" 23 | } 24 | ], 25 | "xnat": [ 26 | { 27 | "name": "wrapper", 28 | "description": "Use a wrapup command on the output handler", 29 | "external-inputs": [ 30 | { 31 | "name": "session", 32 | "description": "A session", 33 | "type": "Session", 34 | "required": true 35 | } 36 | ], 37 | "derived-inputs": [ 38 | { 39 | "name": "resource", 40 | "description": "A fake resource, which will be mounted", 41 | "type": "Resource", 42 | "provides-files-for-command-mount": "in", 43 | "derived-from-wrapper-input": "session" 44 | } 45 | ], 46 | "output-handlers": [ 47 | { 48 | "name": "output-handler", 49 | "accepts-command-output": "output", 50 | "via-wrapup-command": "xnat/test-wrapup-command:latest:wrapup-command", 51 | "as-a-child-of": "session", 52 | "label": "WRAPUP" 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/project-mount-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "find", 3 | "description": "find all the files and directories in the input mount.", 4 | "type": "docker", 5 | "version": "1.0", 6 | "schema-version": "1.0", 7 | "image": "busybox:latest", 8 | "command-line": "find /input -type f > /output/files.txt; find /input -type d > /output/dirs.txt", 9 | "override-entrypoint": true, 10 | "mounts": [ 11 | { 12 | "name": "input", 13 | "writable": false, 14 | "path": "/input" 15 | }, 16 | { 17 | "name": "output", 18 | "writable": true, 19 | "path": "/output" 20 | } 21 | ], 22 | "inputs": [], 23 | "outputs": [ 24 | { 25 | "name": "outputs", 26 | "description": "The files produced by the command.", 27 | "mount": "output", 28 | "required": true 29 | } 30 | ], 31 | "xnat": [ 32 | { 33 | "name": "find-in-project", 34 | "description": "Proof of concept that we can mount an entire project from the archive into a container.", 35 | "contexts": ["xnat:projectData"], 36 | "external-inputs": [ 37 | { 38 | "name": "project", 39 | "description": "A project", 40 | "type": "Project", 41 | "required": true, 42 | "provides-files-for-command-mount": "input" 43 | } 44 | ], 45 | "output-handlers": [ 46 | { 47 | "name": "file-and-dir-lists", 48 | "accepts-command-output": "outputs", 49 | "as-a-child-of": "project", 50 | "type": "Resource", 51 | "label": "FILES_AND_DIRS" 52 | } 53 | ] 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The XNAT web application and server platform are written and maintained by the 2 | Neuroinformatics Research Group at the Washington University School of 3 | Medicine. They are made available for general use under the 2-clause or 4 | Simplified BSD license: 5 | 6 | ============================================================================== 7 | 8 | Copyright (c) 2017, Washington University School of Medicine 9 | All rights reserved. 10 | 11 | Redistribution and use in source and binary forms, with or without 12 | modification, are permitted provided that the following conditions are met: 13 | 14 | 1. Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | 2. Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 27 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | The views and conclusions contained in the software and documentation are those 32 | of the authors and should not be interpreted as representing official policies, 33 | either expressed or implied, of the FreeBSD Project. 34 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/KubernetesStatusChangeEvent.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | import lombok.Value; 4 | import org.nrg.containers.model.kubernetes.KubernetesPodPhase; 5 | 6 | import java.time.OffsetDateTime; 7 | import java.util.Collections; 8 | import java.util.Map; 9 | 10 | @Value 11 | public class KubernetesStatusChangeEvent implements ContainerEvent { 12 | String jobName; 13 | String podName; 14 | String containerId; 15 | KubernetesPodPhase podPhase; 16 | String podPhaseReason; 17 | KubernetesContainerState containerState; 18 | String containerStateReason; 19 | Integer exitCode; 20 | OffsetDateTime timestamp; 21 | String nodeId; 22 | 23 | @Override 24 | public String backendId() { 25 | return jobName; 26 | } 27 | 28 | @Override 29 | public String status() { 30 | return podPhase.toString(); 31 | } 32 | 33 | @Override 34 | public String externalTimestamp() { 35 | return timestamp == null ? null : timestamp.toString(); 36 | } 37 | 38 | @Override 39 | public Map attributes() { 40 | // We don't get any attributes from kubernetes 41 | return Collections.emptyMap(); 42 | } 43 | 44 | @Override 45 | public boolean isExitStatus() { 46 | return podPhase.isTerminal(); 47 | } 48 | 49 | @Override 50 | public String exitCode() { 51 | return exitCode == null ? null : String.valueOf(exitCode); 52 | } 53 | 54 | @Override 55 | public String details() { 56 | final String details; 57 | if (podPhaseReason != null && 58 | containerStateReason != null) { 59 | details = "Pod: " + podPhaseReason + " Container: " + containerStateReason; 60 | } else if (podPhaseReason != null) { 61 | details = podPhaseReason; 62 | } else { 63 | details = containerStateReason; 64 | } 65 | return details; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/exceptions/CommandInputResolutionException.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.exceptions; 2 | 3 | import org.nrg.containers.model.command.auto.Command.Input; 4 | 5 | public class CommandInputResolutionException extends CommandResolutionException { 6 | private final Input input; 7 | private final String value; 8 | 9 | public CommandInputResolutionException(final String message, final Throwable cause) { 10 | super(message, cause); 11 | this.input = null; 12 | this.value = null; 13 | } 14 | 15 | public CommandInputResolutionException(final String message, final Input input) { 16 | super(message); 17 | this.input = input; 18 | this.value = null; 19 | } 20 | 21 | public CommandInputResolutionException(final String message, final Input input, final Throwable cause) { 22 | super(message, cause); 23 | this.input = input; 24 | this.value = null; 25 | } 26 | 27 | public CommandInputResolutionException(final String message, final String value) { 28 | super(message); 29 | this.input = null; 30 | this.value = value; 31 | } 32 | 33 | public CommandInputResolutionException(final String message, final String value, final Throwable cause) { 34 | super(message, cause); 35 | this.input = null; 36 | this.value = value; 37 | } 38 | 39 | public CommandInputResolutionException(final String message, final Input input, final String value) { 40 | super(message); 41 | this.input = input; 42 | this.value = value; 43 | } 44 | 45 | public CommandInputResolutionException(final String message, final Input input, final String value, final Throwable cause) { 46 | super(message, cause); 47 | this.input = input; 48 | this.value = value; 49 | } 50 | 51 | public Input getInput() { 52 | return input; 53 | } 54 | public String getValue() { 55 | return value; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/orchestration/entity/OrchestratedWrapperEntity.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.orchestration.entity; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.containers.model.command.entity.CommandWrapperEntity; 5 | import org.nrg.framework.orm.hibernate.AbstractHibernateEntity; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.FetchType; 9 | import javax.persistence.ManyToOne; 10 | 11 | @Entity 12 | @Slf4j 13 | public class OrchestratedWrapperEntity extends AbstractHibernateEntity { 14 | private OrchestrationEntity orchestrationEntity; 15 | private CommandWrapperEntity commandWrapperEntity; 16 | private int orchestratedOrder; 17 | 18 | public int getOrchestratedOrder() { 19 | return orchestratedOrder; 20 | } 21 | 22 | public void setOrchestratedOrder(int order) { 23 | this.orchestratedOrder = order; 24 | } 25 | 26 | @ManyToOne(fetch = FetchType.LAZY) 27 | public OrchestrationEntity getOrchestrationEntity() { 28 | return orchestrationEntity; 29 | } 30 | 31 | public void setOrchestrationEntity(OrchestrationEntity orchestrationEntity) { 32 | this.orchestrationEntity = orchestrationEntity; 33 | } 34 | 35 | @ManyToOne(fetch = FetchType.LAZY, optional = false) 36 | public CommandWrapperEntity getCommandWrapperEntity() { 37 | return commandWrapperEntity; 38 | } 39 | 40 | public void setCommandWrapperEntity(CommandWrapperEntity commandEntity) { 41 | this.commandWrapperEntity = commandEntity; 42 | } 43 | 44 | public long wrapperId() { 45 | return commandWrapperEntity.getId(); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object o) { 50 | if (this == o) return true; 51 | if (!(o instanceof OrchestratedWrapperEntity)) return false; 52 | return getId() == ((OrchestratedWrapperEntity) o).getId(); 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | return getClass().hashCode(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/services/CommandLabelServiceTest.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.nrg.containers.config.CommandLabelServiceTestConfig; 7 | import org.nrg.containers.model.command.auto.Command; 8 | import org.nrg.containers.model.image.docker.DockerImage; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 | 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | import static org.hamcrest.Matchers.hasSize; 17 | import static org.hamcrest.Matchers.is; 18 | import static org.junit.Assert.assertThat; 19 | 20 | @RunWith(SpringJUnit4ClassRunner.class) 21 | @ContextConfiguration(classes = CommandLabelServiceTestConfig.class) 22 | public class CommandLabelServiceTest { 23 | 24 | @Autowired private CommandLabelService commandLabelService; 25 | @Autowired private ObjectMapper objectMapper; 26 | 27 | @Test 28 | public void testCommandLabelsCanDeserialize() throws Exception { 29 | final String commandName = "command"; 30 | final String imageName = "an image"; 31 | final Command command = Command.builder() 32 | .name(commandName) 33 | .image(imageName) 34 | .build(); 35 | 36 | final String dockerImageLabel = objectMapper.writeValueAsString(Collections.singletonList(command)); 37 | final DockerImage dockerImage = DockerImage.builder() 38 | .imageId("image id") 39 | .addLabel(CommandLabelService.LABEL_KEY, dockerImageLabel) 40 | .build(); 41 | 42 | final List commandsFromLabels = commandLabelService.parseLabels(imageName, dockerImage); 43 | assertThat(commandsFromLabels, hasSize(1)); 44 | assertThat(commandsFromLabels.get(0).name(), is(commandName)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/api/KubernetesClientTest.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.api; 2 | 3 | import io.kubernetes.client.openapi.models.V1Affinity; 4 | import io.kubernetes.client.openapi.models.V1NodeSelectorRequirement; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.junit.Test; 7 | 8 | import java.util.Collections; 9 | 10 | import static org.hamcrest.CoreMatchers.is; 11 | import static org.junit.Assert.assertThat; 12 | 13 | @Slf4j 14 | public class KubernetesClientTest { 15 | 16 | @Test 17 | public void parseSwarmConstraint() throws Exception { 18 | final String constraintKey = "a-key"; 19 | final String constraintValue = "a-value"; 20 | final String swarmComparator = "=="; 21 | final String expectedKubernetesOperator = "In"; 22 | 23 | final String swarmConstraint = constraintKey + swarmComparator + constraintValue; 24 | 25 | // Test the underlying method that takes one constraint string 26 | final KubernetesClientImpl.ParsedConstraint parsedConstraint = KubernetesClientImpl.parseSwarmConstraint(swarmConstraint); 27 | 28 | assertThat(parsedConstraint.label(), is(constraintKey)); 29 | assertThat(parsedConstraint.operator(), is(expectedKubernetesOperator)); 30 | assertThat(parsedConstraint.value(), is(constraintValue)); 31 | 32 | // Test the method that takes a list and puts it into the kubernetes object format 33 | final V1Affinity affinity = KubernetesClientImpl.parseSwarmConstraints(Collections.singletonList(swarmConstraint)); 34 | 35 | // Dig out the properties at the bottom of the nested objects 36 | V1NodeSelectorRequirement nodeSelector = affinity.getNodeAffinity().getRequiredDuringSchedulingIgnoredDuringExecution().getNodeSelectorTerms().get(0).getMatchExpressions().get(0); 37 | assertThat(nodeSelector.getKey(), is(constraintKey)); 38 | assertThat(nodeSelector.getOperator(), is(expectedKubernetesOperator)); 39 | assertThat(nodeSelector.getValues(), is(Collections.singletonList(constraintValue))); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/resources/commandLaunchTest/testScanUpload/fakeDirForCopy/scan.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | session1 4 | usable 5 | 3 6 | containerUpload 7 | T1w 8 | 9 | 2.4 10 | 0.00316 11 | 1.0 12 | 8 13 | GR_IR 14 | ORIGINAL\PRIMARY\M\ND\NORM 15 | SP_MP_OSP 16 | i- 17 | 2013-04-29T00:00:00 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/params-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "param-test", 3 | "description": "Test resolving params", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "echo #REQUIRED_NO_FLAG# #REQUIRED_WITH_FLAG# #NOT_REQUIRED#", 7 | "inputs": [ 8 | { 9 | "name": "REQUIRED_NO_FLAG", 10 | "type": "string", 11 | "required": true 12 | }, 13 | { 14 | "name": "REQUIRED_WITH_FLAG", 15 | "type": "string", 16 | "required": true, 17 | "command-line-flag": "--flag" 18 | }, 19 | { 20 | "name": "NOT_REQUIRED", 21 | "type": "string", 22 | "required": false 23 | } 24 | ], 25 | "xnat": [ 26 | { 27 | "name": "blank-wrapper", 28 | "label": "Param test: blank", 29 | "description": "Test resolving the command by itself", 30 | "external-inputs": [], 31 | "derived-inputs": [], 32 | "output-handlers": [] 33 | }, 34 | { 35 | "name": "identity-wrapper", 36 | "label": "Param test: identity", 37 | "description": "Test resolving the command with wrapper params", 38 | "external-inputs": [ 39 | { 40 | "name": "required-no-flag", 41 | "type": "string", 42 | "required": true, 43 | "provides-value-to-command-input": "REQUIRED_NO_FLAG" 44 | }, 45 | { 46 | "name": "required-with-flag", 47 | "type": "string", 48 | "required": true, 49 | "provides-value-to-command-input": "REQUIRED_WITH_FLAG" 50 | }, 51 | { 52 | "name": "not-required", 53 | "type": "string", 54 | "required": false, 55 | "provides-value-to-command-input": "NOT_REQUIRED" 56 | } 57 | ], 58 | "derived-inputs": [] 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/DockerHubService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.exceptions.NotUniqueException; 4 | import org.nrg.containers.model.dockerhub.DockerHubBase.DockerHub; 5 | import org.nrg.containers.model.dockerhub.DockerHubEntity; 6 | import org.nrg.framework.exceptions.NotFoundException; 7 | import org.nrg.framework.orm.hibernate.BaseHibernateService; 8 | 9 | import java.util.List; 10 | 11 | public interface DockerHubService extends BaseHibernateService { 12 | DockerHubEntity retrieve(String name) throws NotUniqueException; 13 | DockerHubEntity get(String name) throws NotUniqueException, NotFoundException; 14 | DockerHub retrieveHub(long id); 15 | DockerHub retrieveHub(String name) throws NotUniqueException; 16 | DockerHub getHub(long hubId) throws NotFoundException; 17 | DockerHub getHub(String hubName) throws NotFoundException, NotUniqueException; 18 | DockerHub getByUrl(final String url); 19 | List getHubs(); 20 | 21 | void setDefault(DockerHub dockerHub, String username, String reason); 22 | DockerHub create(DockerHub dockerHub); 23 | DockerHubEntity createAndSetDefault(DockerHubEntity dockerHubEntity, String username, String reason); 24 | DockerHub createAndSetDefault(DockerHub dockerHub, String username, String reason); 25 | void update(DockerHub dockerHub); 26 | void updateAndSetDefault(DockerHubEntity dockerHubEntity, String username, String reason); 27 | void updateAndSetDefault(DockerHub dockerHub, String username, String reason); 28 | void setDefault(long id, String username, String reason); 29 | void delete(long id) throws DockerHubDeleteDefaultException; 30 | void delete(String name) throws DockerHubDeleteDefaultException, NotUniqueException; 31 | 32 | void delete(DockerHubEntity entity) throws DockerHubDeleteDefaultException; 33 | 34 | long getDefaultHubId(); 35 | DockerHub getDefault(); 36 | 37 | class DockerHubDeleteDefaultException extends RuntimeException { 38 | public DockerHubDeleteDefaultException(final String message) { 39 | super(message); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/html2confluence.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import re 4 | import sys 5 | 6 | inputAndOutputFile = sys.argv[1] 7 | 8 | xnatIssueLinkRe = re.compile(r'.+?<\/a>') 9 | xnatIssueLinkReplacement = "Neuroinformatics Research Group JIRAkey,summary,type,created,updated,due,assignee,reporter,priority,status,resolutioncd48cfbe-36e3-3ab6-af43-5d0331c561fb\\1" 10 | 11 | headerAnchorRe = re.compile(r'') 12 | headerAnchorReplacement = "\\2" 13 | 14 | anchorLinkRe = re.compile(r'(.*?)<\/a>') 15 | anchorLinkReplacement = "" 16 | 17 | wikiLinkRe = re.compile(r'(.+?)') 18 | def wikiLinkReplacement(matchobj): 19 | return "".format(matchobj.group(1).replace('+', ' '), matchobj.group(2)) 20 | 21 | regexAndReplacements = ((xnatIssueLinkRe, xnatIssueLinkReplacement), 22 | (headerAnchorRe, headerAnchorReplacement), 23 | (anchorLinkRe, anchorLinkReplacement), 24 | (wikiLinkRe, wikiLinkReplacement)) 25 | 26 | with open(inputAndOutputFile, "r") as f: 27 | confluenceOriginal = [line.rstrip("\n") for line in f.readlines()] 28 | 29 | confluenceProcessed = [] 30 | for line in confluenceOriginal: 31 | for regex, replacement in regexAndReplacements: 32 | line = regex.sub(replacement, line) 33 | confluenceProcessed.append(line) 34 | 35 | with open(inputAndOutputFile, "w") as f: 36 | f.write('\n'.join(confluenceProcessed)) 37 | -------------------------------------------------------------------------------- /docs/upload-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | die(){ 4 | popd > /dev/null 5 | echo >&2 "$@" 6 | exit -1 7 | } 8 | 9 | username=${1?Please pass XNAT Wiki username and password as arguments to this script} 10 | password=${2?Please pass XNAT Wiki username and password as arguments to this script} 11 | 12 | docsdir=$(dirname "$0") 13 | pushd $docsdir > /dev/null 14 | 15 | mostRecentReleaseTag=$(git describe --abbrev=0 --tags) 16 | 17 | # Make container-service-api.md from template and swagger.json 18 | echo "Making container-service-api.md from template" 19 | ./make-container-service-api-md.py 20 | 21 | # For each markdown file... 22 | # Convert it to HTML and confluence HTML 23 | # Upload it to wiki 24 | for mdFilePath in $(ls *.md); do 25 | mdName=$(basename $mdFilePath) 26 | base=${mdName%.md} 27 | htmlName="${base}.html" 28 | confluenceName="${htmlName}.confluence" 29 | 30 | echo 31 | echo "Next file: $mdName" 32 | 33 | # Check if the file should be skipped 34 | if $(head -1 $mdName | grep 'NO UPLOAD' > /dev/null); then 35 | echo "${mdName} has requested NO UPLOAD. Skipping." 36 | continue 37 | fi 38 | 39 | if [[ "$base" == "container-service-api" ]]; then 40 | fileToCompare="swagger.json" 41 | else 42 | fileToCompare=${mdName} 43 | fi 44 | d=$(git diff ${mostRecentReleaseTag} ${fileToCompare}) || (echo "Failed to check if $fileToCompare has changed since last release. Skipping."; continue) 45 | 46 | if [[ -n "$d" ]]; then 47 | echo "$fileToCompare has changed since last release. Proceeding with conversion and upload." 48 | else 49 | echo "$fileToCompare has not changed since last release. Skipping." 50 | continue 51 | fi 52 | 53 | echo "Converting ${mdName} from markdown to confluence HTML" 54 | ./md2confluencehtml.sh ${mdName} || die "Failed to convert ${mdName} from markdown to confluence HTML" 55 | 56 | echo "Uploading converted ${mdName} to wiki" 57 | ./upload-confluence.py ${username} ${password} ${confluenceName} || die "Failed to upload ${confluenceName} to wiki" 58 | 59 | done 60 | 61 | echo "Cleaning up" 62 | rm *.html 63 | rm *.html.confluence 64 | rm container-service-api.md 65 | 66 | popd > /dev/null 67 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/utils/ShellSplitterTest.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.utils; 2 | 3 | import org.hamcrest.Matchers; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | 9 | import static org.hamcrest.Matchers.equalTo; 10 | import static org.hamcrest.Matchers.is; 11 | import static org.junit.Assert.assertThat; 12 | 13 | public class ShellSplitterTest { 14 | @Test 15 | public void blankYieldsEmptyArgs() { 16 | assertThat(ShellSplitter.shellSplit(""), is(Matchers.empty())); 17 | } 18 | 19 | @Test 20 | public void whitespacesOnlyYieldsEmptyArgs() { 21 | assertThat(ShellSplitter.shellSplit(" \t \n"), is(Matchers.empty())); 22 | } 23 | 24 | @Test 25 | public void normalTokens() { 26 | assertThat(ShellSplitter.shellSplit("a\tbee cee"), is(equalTo(Arrays.asList("a", "bee", "cee")))); 27 | } 28 | 29 | @Test 30 | public void doubleQuotes() { 31 | assertThat(ShellSplitter.shellSplit("\"hello world\""), is(equalTo(Collections.singletonList("hello world")))); 32 | } 33 | 34 | @Test 35 | public void singleQuotes() { 36 | assertThat(ShellSplitter.shellSplit("'hello world'"), is(equalTo(Collections.singletonList("hello world")))); 37 | } 38 | 39 | 40 | @Test 41 | public void escapedDoubleQuotes() { 42 | assertThat(ShellSplitter.shellSplit("\"\\\"hello world\\\""), is(equalTo(Collections.singletonList("\"hello world\"")))); 43 | } 44 | 45 | @Test 46 | public void noEscapeWithinSingleQuotes() { 47 | assertThat(ShellSplitter.shellSplit("'hello \\\" world'"), is(equalTo(Collections.singletonList("hello \\\" world")))); 48 | } 49 | 50 | @Test 51 | public void backToBackQuotedStringsShouldFormSingleToken() { 52 | assertThat(ShellSplitter.shellSplit("\"foo\"'bar'baz"), is(equalTo(Collections.singletonList("foobarbaz")))); 53 | assertThat(ShellSplitter.shellSplit("\"three\"' 'four"), is(equalTo(Collections.singletonList("three four")))); 54 | } 55 | 56 | @Test 57 | public void escapedSpacesDoNotBreakUpTokens() { 58 | assertThat(ShellSplitter.shellSplit("three\\ four"), is(equalTo(Collections.singletonList("three four")))); 59 | } 60 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/services/ContainerConfigService.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.services; 2 | 3 | import org.nrg.containers.model.configuration.CommandConfigurationInternal; 4 | 5 | import java.util.List; 6 | 7 | public interface ContainerConfigService { 8 | String TOOL_ID = "container-service"; 9 | String DEFAULT_DOCKER_HUB_PATH = "default-docker-hub-id"; 10 | String WRAPPER_CONFIG_PATH_TEMPLATE = "wrapper-%d"; 11 | 12 | long getDefaultDockerHubId(); 13 | void setDefaultDockerHubId(long hubId, String username, String reason); 14 | 15 | void configureForSite(CommandConfigurationInternal commandConfiguration, long wrapperId, String username, String reason) throws CommandConfigurationException; 16 | void configureForProject(CommandConfigurationInternal commandConfiguration, String project, long wrapperId, String username, String reason) throws CommandConfigurationException; 17 | 18 | CommandConfigurationInternal getSiteConfiguration(long wrapperId); 19 | CommandConfigurationInternal getProjectConfiguration(String project, long wrapperId); 20 | 21 | void deleteSiteConfiguration(long commandId, final String username) throws CommandConfigurationException; 22 | void deleteProjectConfiguration(String project, long wrapperId, final String username) throws CommandConfigurationException; 23 | 24 | void enableForSite(long wrapperId, final String username, final String reason) throws CommandConfigurationException; 25 | 26 | void disableForSite(long wrapperId, final String username, final String reason) throws CommandConfigurationException; 27 | boolean isEnabledForSite(long wrapperId); 28 | void enableForProject(String project, long wrapperId, final String username, final String reason) throws CommandConfigurationException; 29 | void disableForProject(String project, long wrapperId, final String username, final String reason) throws CommandConfigurationException; 30 | boolean isEnabledForProject(String project, long wrapperId); 31 | boolean isEnabled(String project, long wrapperId); 32 | List getProjects(long wrapperId, String status); 33 | 34 | class CommandConfigurationException extends Exception { 35 | public CommandConfigurationException(final String message, final Throwable e) { 36 | super(message, e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/utils/JsonStringToDateSerializer.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.utils; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.JsonSerializer; 6 | import com.fasterxml.jackson.databind.SerializerProvider; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | import java.io.IOException; 10 | import java.text.SimpleDateFormat; 11 | import java.util.Date; 12 | import java.util.regex.Pattern; 13 | 14 | import static org.nrg.containers.utils.JsonDateSerializer.DATE_FORMAT; 15 | 16 | /** 17 | * @author Mohana Ramaratnam 18 | * 19 | */ 20 | public class JsonStringToDateSerializer extends JsonSerializer { 21 | private static final Pattern NUMERIC_TIMESTAMP = Pattern.compile("^\\d{12,}$"); // Twelve or more digits and that's all 22 | 23 | private static final int NUM_DIGITS_IN_NANOSECOND_VALUE = 16; 24 | private static final int NUM_DIGITS_TO_TRUNCATE_NANO_TO_MILLI = 6; 25 | 26 | @Override 27 | public void serialize(String value, JsonGenerator jgen, SerializerProvider provider) throws IOException, 28 | JsonProcessingException { 29 | if (StringUtils.isBlank(value)) { 30 | return; 31 | } 32 | 33 | if (!NUMERIC_TIMESTAMP.matcher(value).matches()) { 34 | // Timestamp isn't numeric. Return as-is. 35 | jgen.writeString(value); 36 | return; 37 | } 38 | 39 | if (value.length() >= NUM_DIGITS_IN_NANOSECOND_VALUE) { 40 | // This value is in nanoseconds - convert to milliseconds 41 | value = value.substring(0, value.length() - NUM_DIGITS_TO_TRUNCATE_NANO_TO_MILLI); 42 | } 43 | 44 | long longVal = Long.parseLong(value); 45 | 46 | Date longAsDate = new Date(longVal); 47 | jgen.writeString(DATE_FORMAT.format(longAsDate)); 48 | } 49 | 50 | public static void main(String[] args) { 51 | long longVal = Long.parseLong("1542051871478"); 52 | Date longAsDate = new Date(longVal); 53 | SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS Z"); 54 | //System.out.println("String to Date: " + format.format(longAsDate)); 55 | try { 56 | Date d = format.parse("2018-11-03T12:45:38.615-05:00"); 57 | long milliseconds = d.getTime(); 58 | } catch (Exception e) { 59 | e.printStackTrace(); 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/SpringJUnit4ClassRunnerFactory.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import org.junit.runner.Runner; 4 | import org.junit.runners.model.FrameworkMethod; 5 | import org.junit.runners.model.InitializationError; 6 | import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParameters; 7 | import org.junit.runners.parameterized.ParametersRunnerFactory; 8 | import org.junit.runners.parameterized.TestWithParameters; 9 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 10 | 11 | public class SpringJUnit4ClassRunnerFactory implements ParametersRunnerFactory { 12 | //https://gist.github.com/bademux/5ed07e564c879994b93c 13 | //https://stackoverflow.com/questions/28560734/how-to-run-junit-springjunit4classrunner-with-parametrized 14 | //https://knowjavathings.blogspot.com/2014/02/junit4-for-both-parameterized-and.html 15 | // So as to use parameterized tests, we cannot also use @RunWith(SpringJUnit4ClassRunner.class), use this instead 16 | 17 | @Override 18 | public Runner createRunnerForTestWithParameters(final TestWithParameters test) throws InitializationError { 19 | final LocalBlockJUnit4ClassRunnerWithParameters testRunner = new LocalBlockJUnit4ClassRunnerWithParameters(test); 20 | 21 | return new SpringJUnit4ClassRunner(test.getTestClass().getJavaClass()) { 22 | @Override 23 | protected Object createTest() throws Exception { 24 | final Object testInstance = testRunner.createTest(); 25 | getTestContextManager().prepareTestInstance(testInstance); 26 | return testInstance; 27 | } 28 | 29 | @Override 30 | protected String getName() { 31 | return testRunner.getRealName(); 32 | } 33 | 34 | @Override 35 | protected String testName(FrameworkMethod method) { 36 | return method.getName() + testRunner.getRealName(); 37 | } 38 | }; 39 | } 40 | 41 | private class LocalBlockJUnit4ClassRunnerWithParameters extends BlockJUnit4ClassRunnerWithParameters { 42 | 43 | public LocalBlockJUnit4ClassRunnerWithParameters(final TestWithParameters test) throws InitializationError { 44 | super(test); 45 | } 46 | 47 | String getRealName() { 48 | return getName(); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the XNAT Container Service 2 | 3 | First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | We welcome any [bug reports](#report-an-issue), feature requests, or [questions](#ask-a-question). And we super-duper welcome any [pull requests](#make-a-pull-request). 6 | 7 | This document is a little sparse now, but will hopefully evolve in the future. 8 | 9 | ## Report an issue 10 | 11 | If you have (or want to open) an account on the [XNAT JIRA](https://issues.xnat.org), you could make an issue there; just add it to the [Container Service project](https://issues.xnat.org/projects/CS). 12 | Alternatively, [ask a question on the discussion group](#ask-a-question) 13 | 14 | ## Ask a question 15 | 16 | First, check the [XNAT Discussion Board](https://groups.google.com/forum/#!forum/xnat_discussion). It is possible that someone has already asked your question and gotten an answer, so it could save you a lot of time to search around first. And if no one has asked your question, the discussion board is a great first place to ask. 17 | 18 | 19 | ## Run the Tests 20 | The various unit tests can be run with: 21 | ``` 22 | [container-service]$ ./gradlew unitTest 23 | ``` 24 | 25 | If you have a docker server that you can use for testing, there are some additional tests that can run. Make sure your docker environment is all set up (you can test this by making sure `$ docker version` works). Then run all the tests with 26 | ``` 27 | [container-service]$ ./gradlew test 28 | ``` 29 | In order to synchronize your Docker VM clock, you may need to initially run the command as: 30 | ``` 31 | [container-service]$ docker run --rm --privileged alpine:latest hwclock -s && ./gradlew clean test 32 | ``` 33 | We do not have any tests that can integrate with a running XNAT. All of the tests in this library use bespoke databases and mocked interfaces any time the code intends to communicate with XNAT. We welcome your contributions! 34 | 35 | ## Make a Pull Request 36 | If you want to contribute code, we will be very happy to have it. 37 | 38 | The first thing you should know is that we do almost all of our work on the `dev` branch, or on smaller "feature" branches that come from and merge into `dev`. So if you want to do something with the code, you, too, should start a new branch from `dev` on our [BitBucket repo](https://bitbucket.org/xnatdev/container-service). 39 | 40 | 41 | 42 | 43 | And thanks again! 44 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/command/auto/ResolvedInputValue.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.command.auto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.auto.value.AutoValue; 7 | import org.nrg.containers.model.command.entity.CommandInputEntity; 8 | import org.nrg.containers.model.xnat.XnatModelObject; 9 | 10 | import javax.annotation.Nullable; 11 | import java.io.Serializable; 12 | 13 | @AutoValue 14 | public abstract class ResolvedInputValue implements Serializable { 15 | private static final long serialVersionUID = 3736382357316063998L; 16 | 17 | @JsonProperty("type") public abstract String type(); 18 | @Nullable @JsonProperty("value") public abstract String value(); 19 | @Nullable @JsonProperty("value-label") public abstract String valueLabel(); 20 | @JsonIgnore @Nullable public abstract XnatModelObject xnatModelObject(); 21 | @Nullable @JsonProperty("json-value") public abstract String jsonValue(); 22 | 23 | @JsonCreator 24 | public static ResolvedInputValue create(@JsonProperty("type") final String type, 25 | @JsonProperty("value") final String value, 26 | @JsonProperty("value-label") final String valueLabel, 27 | @JsonProperty("json-value") final String jsonValue) { 28 | return builder() 29 | .type(type) 30 | .value(value) 31 | .valueLabel(valueLabel) 32 | .jsonValue(jsonValue == null ? value : jsonValue) 33 | .build(); 34 | } 35 | 36 | public static Builder builder() { 37 | return new AutoValue_ResolvedInputValue.Builder() 38 | .type(CommandInputEntity.DEFAULT_TYPE.getName()); 39 | } 40 | 41 | @AutoValue.Builder 42 | public static abstract class Builder { 43 | public abstract Builder type(String type); 44 | public abstract Builder value(String value); 45 | public abstract Builder valueLabel(String valueLabel); 46 | public abstract Builder xnatModelObject(XnatModelObject xnatModelObject); 47 | public abstract Builder jsonValue(String jsonValue); 48 | 49 | public abstract ResolvedInputValue build(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/set-up-vms-for-swarm-dev.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | * Make local directories 4 | * One to serve as the XNAT archive, which I will call "archive" 5 | * Another for the build directory, which I will call "build" 6 | * Set up XNAT VM (use XNAT Vagrant project) 7 | * Start from xnat-release config 8 | * Edit local.yaml 9 | * Pointing to 1.7.3 war 10 | * Add shares 11 | * archive to `/data/xnat/archive` 12 | * build to `/data/xnat/build` 13 | * Run vagrant setup 14 | * If you are on the Ubuntu 14.04 VM, update docker. If you are using 16.04 or later, you can skip this. 15 | * Fix bad puppet PGP key 16 | * `sudo gpg --keyserver pgp.mit.edu --recv-key 7F438280EF8D349F` 17 | * `sudo gpg -a --export EF8D349F | sudo apt-key add -` 18 | * `sudo apt-get update` 19 | * `sudo apt-get upgrade` 20 | * Say "No" to everything 21 | * Find version with `docker version`. (Right now it is 17.05.0-ce) 22 | * If you are using the Ubuntu 16.04 VM, find the docker version 23 | * SSH onto machine 24 | * `docker version` 25 | * Note docker version for later. 26 | * set up swarm 27 | * `docker swarm init --advertise-addr ${YOUR_VM_IP}` 28 | * Note the swarm join command that gets spit out. We will use it later. 29 | * `docker node update --availability drain xnat` 30 | * Set up Worker node(s) 31 | * Go to [boot2docker releases page](https://github.com/boot2docker/boot2docker/releases) 32 | * Find boot2docker release with the same version of docker as you noted earlier 33 | * `docker-machine create --boot2docker-url ${above url} worker${n}` (where n is an index, recommended if you are making more than one worker) 34 | * ssh to node just created 35 | * Use swarm join command that got spit out when we made the swarm 36 | * Share local XNAT archive (do this for each worker node) 37 | * In virtualbox, change VM settings. Add shares to archive and build (use the local paths on your machine) 38 | * ssh to VM 39 | * `sudo mkdir -p /data/xnat/archive` 40 | * `sudo mkdir /data/xnat/build` 41 | * `sudo chown -R docker /data` 42 | * Share archive to worker node: `sudo mount -t vboxsf -o uid=1000,gid=100 archive /data/xnat/archive` 43 | * Share build to worker node: `sudo mount -t vboxsf -o uid=1000,gid=100 build /data/xnat/build` -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/DockerRestApiTestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.mockito.Mockito; 5 | import org.nrg.containers.rest.DockerRestApi; 6 | import org.nrg.containers.services.DockerService; 7 | import org.nrg.framework.services.ContextService; 8 | import org.nrg.xdat.security.services.RoleHolder; 9 | import org.nrg.xdat.security.services.UserManagementServiceI; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.Import; 14 | import org.springframework.security.authentication.TestingAuthenticationProvider; 15 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 16 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 17 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 18 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 19 | 20 | @Configuration 21 | @EnableWebMvc 22 | @EnableWebSecurity 23 | @Import(RestApiTestConfig.class) 24 | public class DockerRestApiTestConfig extends WebSecurityConfigurerAdapter { 25 | @Bean 26 | public DockerRestApi dockerRestApi(final DockerService dockerService, 27 | final ObjectMapper objectMapper, 28 | final UserManagementServiceI userManagementService, 29 | final RoleHolder roleHolder) { 30 | return new DockerRestApi(dockerService, objectMapper, userManagementService, roleHolder); 31 | } 32 | 33 | @Bean 34 | public DockerService dockerService() { 35 | return Mockito.mock(DockerService.class); 36 | } 37 | 38 | @Bean 39 | public ContextService contextService(final ApplicationContext applicationContext) { 40 | final ContextService contextService = new ContextService(); 41 | contextService.setApplicationContext(applicationContext); 42 | return contextService; 43 | } 44 | 45 | @Override 46 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 47 | auth.authenticationProvider(new TestingAuthenticationProvider()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/swarm-mode-vs-container-mode.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | This document sketches out the differences in the container service backend that will be implemented to enable "swarm mode", i.e. to run containers that are scheduled by a swarm rather than directly on a docker server. 4 | 5 | ## Server configuration 6 | Our current model has XNAT configured to communicate with a single docker server. We will not change that one-to-one connection at this time. Instead, we will add a global flag to the server configuration called "Swarm mode". The system admin can set this flag to indicate that the docker server they have configured is a swarm manager and they wish to launch their jobs as swarm services. As of version 2.0.2, the system admin can configure [swarm node constraints](https://docs.docker.com/engine/swarm/services/#placement-constraints), which can be configured to be passed along to users upon launch (e.g., if you want users to be able to select a high-memory node, etc). 7 | 8 | When this flag is set, all jobs will be launched as swarm services; when it is not set, all jobs will be launched as containers directly on the configured server. 9 | 10 | ## Commands 11 | Right now I anticipate no differences to the command format. Users will not need to make any changes to existing commands. 12 | 13 | ## Launching containers 14 | The actions taken by users to launch jobs will not change, although if swarm constraints are configured to be user-settable by the system admin, then these options will be selectable as advanced options from the container launch screen. 15 | 16 | The only difference will be in the back-end code to create the job. In the current code, after resolving the command using the runtime parameter values, the container service uses that resolved command to create and launch a container on the server. After this update, the container service will still resolve the command the same way, but will then check the global flag indicating whether the server is operating in swarm mode. If the server is not in swarm mode, then the resolved command will be used to create a container just as is done now. If the server is in swarm mode, then the resolved command will be used to create a service spec with one replica and no restart condition. In this way, the service will be used to create a task that replicates the existing mode of launching a job as a container with a finite life cycle. We are using the swarm merely as a job scheduler. 17 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/events/model/DockerContainerEvent.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.events.model; 2 | 3 | import com.google.auto.value.AutoValue; 4 | import com.google.common.collect.ImmutableMap; 5 | 6 | import java.io.Serializable; 7 | import java.util.Date; 8 | import java.util.Map; 9 | import java.util.regex.Pattern; 10 | 11 | @AutoValue 12 | public abstract class DockerContainerEvent implements ContainerEvent, Serializable { 13 | private static final long serialVersionUID = -6676309798270638416L; 14 | 15 | private static final Pattern ignoreStatusPattern = Pattern.compile("kill|destroy"); 16 | private static final Pattern exitStatusPattern = Pattern.compile("die"); 17 | 18 | public abstract String status(); 19 | public abstract String backendId(); 20 | public abstract String externalTimestamp(); 21 | public abstract ImmutableMap attributes(); 22 | 23 | public boolean isIgnoreStatus() { 24 | // These statuses come after "die" and thus we want to ignore them 25 | final String status = status(); 26 | return status != null && ignoreStatusPattern.matcher(status).matches(); 27 | } 28 | 29 | public boolean isExitStatus() { 30 | final String status = status(); 31 | return status != null && exitStatusPattern.matcher(status).matches(); 32 | } 33 | 34 | public String exitCode() { 35 | return isExitStatus() ? 36 | (attributes().getOrDefault("exitCode", "")) : 37 | null; 38 | } 39 | 40 | @Override 41 | public String details() { 42 | return null; 43 | } 44 | 45 | public static DockerContainerEvent create(final String status, 46 | final String containerId, 47 | final Date time, 48 | final Long timeNano, 49 | final Map attributes) { 50 | final ImmutableMap attributesCopy = attributes == null ? 51 | ImmutableMap.of() : 52 | ImmutableMap.copyOf(attributes); 53 | final String externalTimestamp = timeNano != null ? String.valueOf(timeNano) : time.toInstant().toString(); 54 | return new AutoValue_DockerContainerEvent(status, containerId, externalTimestamp, attributesCopy); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/jms/utils/QueueUtils.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.jms.utils; 2 | 3 | import java.util.Enumeration; 4 | import java.util.function.Supplier; 5 | 6 | import javax.jms.Destination; 7 | import javax.jms.JMSException; 8 | import javax.jms.Message; 9 | import javax.jms.QueueBrowser; 10 | import javax.jms.Session; 11 | 12 | import org.apache.commons.lang3.StringUtils; 13 | import org.nrg.xdat.XDAT; 14 | import org.springframework.jms.core.BrowserCallback; 15 | import org.springframework.jms.core.JmsTemplate; 16 | 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.util.ClassUtils; 19 | 20 | @Slf4j 21 | public class QueueUtils { 22 | /* 23 | * Get the count of the current messages in this queue. 24 | */ 25 | public static int count(String destination){ 26 | int count = XDAT.getContextService().getBean(JmsTemplate.class).browse(destination, new BrowserCallback() { 27 | public Integer doInJms(final Session session, final QueueBrowser browser) throws JMSException { 28 | Enumeration enumeration = browser.getEnumeration(); 29 | int counter = 0; 30 | while (enumeration.hasMoreElements()) { 31 | Message msg = (Message) enumeration.nextElement(); 32 | //System.out.println(String.format("\tFound : %s", msg)); 33 | counter += 1; 34 | } 35 | return counter; 36 | } 37 | }); 38 | 39 | log.debug("There are {} messages in queue {}", count, destination); 40 | return count; 41 | } 42 | 43 | public static void sendJmsRequest(final String destination, final String message) { 44 | sendJmsRequest(XDAT.getContextService().getBean(JmsTemplate.class), destination, message); 45 | } 46 | 47 | public static void sendJmsRequest(final JmsTemplate template, final String queueName, final Object request) { 48 | final Destination destination = XDAT.getContextService().getBeanSafely(queueName, Destination.class); 49 | if (destination == null) { 50 | log.error("Unable to find destination for queue name {}", queueName); 51 | return; 52 | } 53 | template.convertAndSend(destination, request, (processor) -> { 54 | processor.setStringProperty("taskId", queueName); 55 | return processor; 56 | }); 57 | 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/initialization/tasks/UpdateVisibilityOfExistingCommands.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.initialization.tasks; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.containers.model.command.entity.CommandVisibility; 5 | import org.nrg.framework.orm.DatabaseHelper; 6 | import org.nrg.xft.schema.XFTManager; 7 | import org.nrg.xnat.initialization.tasks.AbstractInitializingTask; 8 | import org.nrg.xnat.initialization.tasks.InitializingTaskException; 9 | import org.nrg.xnat.services.XnatAppInfo; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.jdbc.core.JdbcTemplate; 12 | import org.springframework.stereotype.Component; 13 | 14 | @Component 15 | @Slf4j 16 | public class UpdateVisibilityOfExistingCommands extends AbstractInitializingTask { 17 | 18 | @Autowired 19 | public UpdateVisibilityOfExistingCommands(final XnatAppInfo appInfo, final JdbcTemplate template) { 20 | super(); 21 | this.appInfo = appInfo; 22 | this.databaseHelper = new DatabaseHelper(template); 23 | this.jdbcTemplate = template; 24 | } 25 | 26 | @Override 27 | public String getTaskName() { 28 | return "UpdateVisibilityOfExistingCommands"; 29 | } 30 | 31 | @Override 32 | protected void callImpl() throws InitializingTaskException { 33 | try { 34 | if (!appInfo.isInitialized() || !XFTManager.isComplete()) { 35 | throw new InitializingTaskException(InitializingTaskException.Level.RequiresInitialization); 36 | } 37 | jdbcTemplate.execute("UPDATE " + COMMAND_ENTITY_TABLE_NAME + " SET " + VISIBILITY_COLUMN + " = 0 where " + VISIBILITY_COLUMN + " is null"); 38 | log.debug("Table " + COMMAND_ENTITY_TABLE_NAME + " updated. Set the " + VISIBILITY_COLUMN + " column value to " + CommandVisibility.PUBLIC_CONTAINER); 39 | } catch(Exception e) { 40 | log.error("Encountered while setting visibility column value",e); 41 | throw new InitializingTaskException(InitializingTaskException.Level.Error); 42 | } 43 | } 44 | 45 | private final JdbcTemplate jdbcTemplate; 46 | private final DatabaseHelper databaseHelper; 47 | private final XnatAppInfo appInfo; 48 | private final String COMMAND_ENTITY_TABLE_NAME = "xhbm_command_entity"; 49 | private final String VISIBILITY_COLUMN = "visibility_type"; 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/test/java/org/nrg/containers/config/ContainerEntityTestConfig.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.config; 2 | 3 | import org.mockito.Mockito; 4 | import org.nrg.containers.api.ContainerControlApi; 5 | import org.nrg.containers.daos.ContainerEntityRepository; 6 | import org.nrg.containers.services.ContainerEntityService; 7 | import org.nrg.containers.services.impl.HibernateContainerEntityService; 8 | import org.nrg.framework.services.NrgEventServiceI; 9 | import org.nrg.framework.services.SerializerService; 10 | import org.nrg.prefs.services.NrgPreferenceService; 11 | import org.nrg.xdat.preferences.SiteConfigPreferences; 12 | import org.nrg.xdat.security.services.PermissionsServiceI; 13 | import org.nrg.xnat.services.archive.CatalogService; 14 | import org.springframework.context.annotation.Bean; 15 | import org.springframework.context.annotation.Configuration; 16 | import org.springframework.context.annotation.Import; 17 | import reactor.bus.EventBus; 18 | 19 | @Configuration 20 | @Import({HibernateConfig.class, ObjectMapperConfig.class}) 21 | public class ContainerEntityTestConfig { 22 | @Bean 23 | public EventBus eventBus() { 24 | return Mockito.mock(EventBus.class); 25 | } 26 | 27 | @Bean 28 | public ContainerControlApi containerControlApi() { 29 | return Mockito.mock(ContainerControlApi.class); 30 | } 31 | 32 | @Bean 33 | public SiteConfigPreferences siteConfigPreferences() { 34 | return Mockito.mock(SiteConfigPreferences.class); 35 | } 36 | 37 | @Bean 38 | public NrgEventServiceI nrgEventService() { 39 | return Mockito.mock(NrgEventServiceI.class); 40 | } 41 | 42 | @Bean 43 | public NrgPreferenceService nrgPreferenceService() { 44 | return Mockito.mock(NrgPreferenceService.class); 45 | } 46 | 47 | @Bean 48 | public SerializerService serializerService() { 49 | return Mockito.mock(SerializerService.class); 50 | } 51 | 52 | @Bean 53 | public PermissionsServiceI permissionsService() { 54 | return Mockito.mock(PermissionsServiceI.class); 55 | } 56 | 57 | @Bean 58 | public CatalogService catalogService() { 59 | return Mockito.mock(CatalogService.class); 60 | } 61 | 62 | @Bean 63 | public ContainerEntityService containerEntityService() { 64 | return new HibernateContainerEntityService(); 65 | } 66 | 67 | @Bean 68 | public ContainerEntityRepository containerEntityRepository() { 69 | return new ContainerEntityRepository(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/model/command/auto/PreresolvedInputTreeNode.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.model.command.auto; 2 | 3 | import com.google.auto.value.AutoValue; 4 | import com.google.common.collect.Lists; 5 | import org.nrg.containers.model.command.auto.Command.CommandInput; 6 | import org.nrg.containers.model.command.auto.Command.CommandWrapperDerivedInput; 7 | import org.nrg.containers.model.command.auto.Command.CommandWrapperExternalInput; 8 | import org.nrg.containers.model.command.auto.Command.Input; 9 | 10 | import java.io.Serializable; 11 | import java.util.List; 12 | 13 | @AutoValue 14 | public abstract class PreresolvedInputTreeNode implements Serializable { 15 | private static final long serialVersionUID = -2965708997601698228L; 16 | 17 | public abstract T input(); 18 | public abstract List> children(); 19 | 20 | public static PreresolvedInputTreeNode create(final CommandWrapperExternalInput externalInput) { 21 | return new AutoValue_PreresolvedInputTreeNode<>(externalInput, Lists.>newArrayList()); 22 | } 23 | 24 | public static PreresolvedInputTreeNode create(final CommandWrapperDerivedInput derivedInput, 25 | final PreresolvedInputTreeNode parent) { 26 | final PreresolvedInputTreeNode node = 27 | new AutoValue_PreresolvedInputTreeNode<>(derivedInput, Lists.>newArrayList()); 28 | 29 | parent.children().add(node); 30 | 31 | return node; 32 | } 33 | 34 | public static PreresolvedInputTreeNode create(final CommandInput commandInput) { 35 | return new AutoValue_PreresolvedInputTreeNode<>(commandInput, Lists.>newArrayList()); 36 | } 37 | 38 | public static PreresolvedInputTreeNode create(final CommandInput commandInput, 39 | final PreresolvedInputTreeNode parent) { 40 | final PreresolvedInputTreeNode node = 41 | new AutoValue_PreresolvedInputTreeNode<>(commandInput, Lists.>newArrayList()); 42 | parent.children().add(node); 43 | return node; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/secrets/SystemPropertySecretSource.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.secrets; 2 | 3 | import com.fasterxml.jackson.annotation.JsonAnyGetter; 4 | import com.fasterxml.jackson.annotation.JsonCreator; 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.util.Collections; 10 | import java.util.Map; 11 | import java.util.Objects; 12 | import java.util.Optional; 13 | 14 | public class SystemPropertySecretSource implements SecretSource.ValueObtainingSecretSource { 15 | @JsonIgnore public static final String JSON_TYPE_NAME = "system-property"; 16 | 17 | @JsonProperty("identifier") private final String systemPropertyName; 18 | 19 | @JsonCreator 20 | public SystemPropertySecretSource(@JsonProperty("identifier") final String systemPropertyName) { 21 | this.systemPropertyName = systemPropertyName; 22 | } 23 | 24 | public String systemPropertyName() { 25 | return systemPropertyName; 26 | } 27 | 28 | @Override 29 | @JsonIgnore 30 | public String type() { 31 | return JSON_TYPE_NAME; 32 | } 33 | 34 | @Override 35 | @JsonIgnore 36 | public String identifier() { 37 | return systemPropertyName; 38 | } 39 | 40 | @JsonAnyGetter 41 | @Override 42 | public Map otherProperties() { 43 | return Collections.emptyMap(); 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return "SystemProperty{\"" + systemPropertyName + "\"}"; 49 | } 50 | 51 | @Override 52 | public boolean equals(final Object o) { 53 | if (this == o) return true; 54 | if (o == null || getClass() != o.getClass()) return false; 55 | final SystemPropertySecretSource that = (SystemPropertySecretSource) o; 56 | return Objects.equals(systemPropertyName, that.systemPropertyName); 57 | } 58 | 59 | @Override 60 | public int hashCode() { 61 | return Objects.hash(systemPropertyName); 62 | } 63 | 64 | @Component 65 | public static class ValueObtainer extends SecretValueObtainer { 66 | public ValueObtainer() { 67 | super(SystemPropertySecretSource.class); 68 | } 69 | 70 | @Override 71 | public Optional obtainValue(final ValueObtainingSecretSource source) { 72 | return Optional.ofNullable(System.getProperty(source.identifier())); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/test/resources/ecatHeaderDump/command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecat-dump", 3 | "description": "Runs ECAT dump tools", 4 | "type": "docker", 5 | "image": "ecat-dump", 6 | "command-line": "lmhdr /input >> /output/ecat-dump.txt && lshdr /input >> /output/ecat-dump.txt", 7 | "mounts": [ 8 | { 9 | "name": "ecat-in", 10 | "writable": "false", 11 | "path": "/input" 12 | }, 13 | { 14 | "name": "dump-out", 15 | "writable": "true", 16 | "path": "/output" 17 | } 18 | ], 19 | "inputs": [], 20 | "outputs": [ 21 | { 22 | "name": "dump-out", 23 | "description": "The ECAT file dump", 24 | "required": true, 25 | "mount": "dump-out", 26 | "path": "ecat-dump.txt" 27 | } 28 | ], 29 | "xnat": [ 30 | { 31 | "name": "ecat-dump scan", 32 | "label": "ecat-dump - scan", 33 | "description": "", 34 | "contexts": ["xnat:imageScanData"], 35 | "external-inputs": [ 36 | { 37 | "name": "scan", 38 | "description": "Input scan", 39 | "type": "Scan", 40 | "required": true, 41 | "matcher": "'ECAT' in @.resources[*].label" 42 | } 43 | ], 44 | "derived-inputs": [ 45 | { 46 | "name": "scan-ecats", 47 | "description": "The scan's ecat resource", 48 | "type": "Resource", 49 | "derived-from-wrapper-input": "scan", 50 | "matcher": "@.label == 'ECAT'" 51 | }, 52 | { 53 | "name": "scan-ecat-file", 54 | "description": "The scan's ecat resource's files", 55 | "type": "File[]", 56 | "derived-from-wrapper-input": "scan-ecats", 57 | "matcher": "@.name =~ /.*v/", 58 | "provides-files-for-command-mount": "ecat-in" 59 | } 60 | ], 61 | "output-handlers": [ 62 | { 63 | "name": "headerdump-resource", 64 | "accepts-command-output": "dump-out", 65 | "as-a-child-of": "scan", 66 | "type": "Resource", 67 | "label": "HEADERDUMP" 68 | } 69 | ] 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /src/main/java/org/nrg/containers/jms/listeners/ContainerFinalizingRequestListener.java: -------------------------------------------------------------------------------- 1 | package org.nrg.containers.jms.listeners; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.nrg.containers.config.ContainersConfig; 5 | import org.nrg.containers.exceptions.ContainerException; 6 | import org.nrg.containers.jms.requests.ContainerFinalizingRequest; 7 | import org.nrg.containers.jms.utils.QueueUtils; 8 | import org.nrg.containers.model.container.auto.Container; 9 | import org.nrg.containers.services.ContainerService; 10 | import org.nrg.framework.exceptions.NotFoundException; 11 | import org.nrg.xdat.security.services.UserManagementServiceI; 12 | import org.nrg.xdat.security.user.exceptions.UserInitException; 13 | import org.nrg.xdat.security.user.exceptions.UserNotFoundException; 14 | import org.nrg.xft.security.UserI; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.jms.annotation.JmsListener; 17 | import org.springframework.stereotype.Component; 18 | 19 | @Slf4j 20 | @Component 21 | public class ContainerFinalizingRequestListener { 22 | private final ContainerService containerService; 23 | private final UserManagementServiceI userManagementServiceI; 24 | 25 | @Autowired 26 | public ContainerFinalizingRequestListener(ContainerService containerService, 27 | UserManagementServiceI userManagementServiceI) { 28 | this.containerService = containerService; 29 | this.userManagementServiceI = userManagementServiceI; 30 | } 31 | 32 | @JmsListener(id = ContainersConfig.FINALIZING_QUEUE_CONTAINER_ID, 33 | containerFactory = ContainersConfig.FINALIZING_QUEUE_LISTENER_FACTORY, 34 | destination = ContainerFinalizingRequest.DESTINATION) 35 | public void onRequest(ContainerFinalizingRequest request) 36 | throws UserNotFoundException, NotFoundException, UserInitException, ContainerException { 37 | Container container = containerService.get(request.getId()); 38 | UserI user = userManagementServiceI.getUser(request.getUsername()); 39 | if (log.isDebugEnabled()) { 40 | log.debug("Consuming finalizing queue: count {}, exitcode {}, is successful {}, id {}, username {}, status {}", 41 | QueueUtils.count(request.getDestination()), request.getExitCodeString(), request.isSuccessful(), request.getId(), request.getUsername(), container.status()); 42 | } 43 | containerService.consumeFinalize(request.getExitCodeString(), request.isSuccessful(), container, user); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/commandResolutionTest/select-command.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "param-test", 3 | "description": "Test resolving params", 4 | "type": "docker", 5 | "image": "busybox:latest", 6 | "command-line": "echo #MULTI_FLAG1# #MULTI_FLAG2# #MULTI_QSPACE# #MULTI_COMMA# #MULTI_SPACE# #MULTI_DEFAULT#", 7 | "inputs": [ 8 | { 9 | "name": "MULTI_FLAG1", 10 | "type": "select-many", 11 | "required": true, 12 | "command-line-flag": "--flag", 13 | "command-line-separator": "=", 14 | "multiple-delimiter": "flag", 15 | "default-value": "[\"scan1\",\"scan2\"]", 16 | "select-values": ["scan1","junk","scan2"] 17 | }, 18 | { 19 | "name": "MULTI_FLAG2", 20 | "type": "select-many", 21 | "required": true, 22 | "command-line-flag": "--flag", 23 | "multiple-delimiter": "flag", 24 | "default-value": "[\"scan1\",\"scan2\"]", 25 | "select-values": ["scan1","junk","scan2"] 26 | }, 27 | { 28 | "name": "MULTI_QSPACE", 29 | "type": "select-many", 30 | "required": true, 31 | "multiple-delimiter": "quoted-space", 32 | "default-value": "[\"scan1\",\"scan2\"]", 33 | "select-values": ["scan1","junk","scan2"] 34 | }, 35 | { 36 | "name": "MULTI_COMMA", 37 | "type": "select-many", 38 | "required": true, 39 | "multiple-delimiter": "comma", 40 | "default-value": "[\"scan1\",\"scan2\"]", 41 | "select-values": ["scan1","junk","scan2"] 42 | }, 43 | { 44 | "name": "MULTI_SPACE", 45 | "type": "select-many", 46 | "required": true, 47 | "multiple-delimiter": "space", 48 | "default-value": "[\"scan1\",\"scan2\"]", 49 | "select-values": ["scan1","junk","scan2"] 50 | }, 51 | { 52 | "name": "MULTI_DEFAULT", 53 | "type": "select-many", 54 | "required": true, 55 | "default-value": "scan1", 56 | "select-values": ["scan1","junk","scan2"] 57 | } 58 | ], 59 | "xnat": [ 60 | { 61 | "name": "multiple", 62 | "label": "Dummy: Session multiple scan", 63 | "description": "run the dummy command with a session with multiple scans", 64 | "external-inputs": [], 65 | "derived-inputs": [], 66 | "output-handlers": [] 67 | } 68 | ] 69 | } 70 | --------------------------------------------------------------------------------