From: Tom Clegg Date: Mon, 5 Sep 2016 17:47:54 +0000 (-0400) Subject: 9799: Merge branch 'master' into 9799-nonadmin-logs X-Git-Tag: 1.1.0~753^2 X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/dcb6eaa5012bf1eea607c61209bee18723769c24?hp=af0ece600054c97aae9661ed06731af47873a7ff 9799: Merge branch 'master' into 9799-nonadmin-logs Conflicts: apps/workbench/test/integration/application_layout_test.rb --- diff --git a/apps/workbench/app/controllers/projects_controller.rb b/apps/workbench/app/controllers/projects_controller.rb index 8ec8d5e1dd..16212a8d0a 100644 --- a/apps/workbench/app/controllers/projects_controller.rb +++ b/apps/workbench/app/controllers/projects_controller.rb @@ -69,7 +69,7 @@ class ProjectsController < ApplicationController pane_list << { :name => 'Pipeline_templates', - :filters => [%w(uuid is_a arvados#pipelineTemplate)] + :filters => [%w(uuid is_a) + [%w(arvados#pipelineTemplate arvados#workflow)]] } pane_list << { diff --git a/apps/workbench/app/controllers/workflows_controller.rb b/apps/workbench/app/controllers/workflows_controller.rb index 94ae8a95ea..a3ba7d66a5 100644 --- a/apps/workbench/app/controllers/workflows_controller.rb +++ b/apps/workbench/app/controllers/workflows_controller.rb @@ -1,2 +1,6 @@ class WorkflowsController < ApplicationController + skip_around_filter :require_thread_api_token, if: proc { |ctrl| + Rails.configuration.anonymous_user_token and + 'show' == ctrl.action_name + } end diff --git a/apps/workbench/app/models/container_work_unit.rb b/apps/workbench/app/models/container_work_unit.rb index c8af093e8f..2580105f9f 100644 --- a/apps/workbench/app/models/container_work_unit.rb +++ b/apps/workbench/app/models/container_work_unit.rb @@ -144,7 +144,7 @@ class ContainerWorkUnit < ProxyWorkUnit def template_uuid properties = get(:properties) if properties - properties[:workflow_uuid] + properties[:template_uuid] end end diff --git a/apps/workbench/app/views/application/_name_and_description.html.erb b/apps/workbench/app/views/application/_name_and_description.html.erb index 68a201f195..78b6f8b135 100644 --- a/apps/workbench/app/views/application/_name_and_description.html.erb +++ b/apps/workbench/app/views/application/_name_and_description.html.erb @@ -1,11 +1,2 @@ -<% if @object.respond_to? :name %> -

- <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New #{controller.model_class.to_s.underscore.gsub("_"," ")}" } %> -

-<% end %> - -<% if @object.respond_to? :description %> -
- <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %> -
-<% end %> +<%= render partial: 'object_name' %> +<%= render partial: 'object_description' %> diff --git a/apps/workbench/app/views/application/_object_description.html.erb b/apps/workbench/app/views/application/_object_description.html.erb new file mode 100644 index 0000000000..726094074b --- /dev/null +++ b/apps/workbench/app/views/application/_object_description.html.erb @@ -0,0 +1,5 @@ +<% if @object.respond_to? :description %> +
+ <%= render_editable_attribute @object, 'description', nil, { 'data-emptytext' => "(No description provided)", 'data-toggle' => 'manual' } %> +
+<% end %> diff --git a/apps/workbench/app/views/application/_object_name.html.erb b/apps/workbench/app/views/application/_object_name.html.erb new file mode 100644 index 0000000000..b303853369 --- /dev/null +++ b/apps/workbench/app/views/application/_object_name.html.erb @@ -0,0 +1,5 @@ +<% if @object.respond_to? :name %> +

+ <%= render_editable_attribute @object, 'name', nil, { 'data-emptytext' => "New #{controller.model_class.to_s.underscore.gsub("_"," ")}" } %> +

+<% end %> diff --git a/apps/workbench/app/views/container_requests/_name_and_description.html.erb b/apps/workbench/app/views/container_requests/_name_and_description.html.erb new file mode 100644 index 0000000000..f40951928a --- /dev/null +++ b/apps/workbench/app/views/container_requests/_name_and_description.html.erb @@ -0,0 +1,21 @@ +<% + wu = @object.work_unit + template_uuid = wu.template_uuid + template = Workflow.find?(template_uuid) if template_uuid + div_class = "col-sm-12" + div_class = "col-sm-6" if template +%> + +
+ <%= render partial: 'object_name' %> + <%= render partial: 'object_description' %> +
+ +<% if template %> +
+ This container request was created from the workflow <%= link_to_if_arvados_object template, friendly_name: true %>
+ <% if template.modified_at && (template.modified_at > @object.created_at) %> + Note: This workflow has been modified since this container request was created. + <% end %> +
+<% end %> diff --git a/apps/workbench/app/views/projects/_show_pipeline_templates.html.erb b/apps/workbench/app/views/projects/_show_pipeline_templates.html.erb index 402ce26f59..d51e1a39c7 100644 --- a/apps/workbench/app/views/projects/_show_pipeline_templates.html.erb +++ b/apps/workbench/app/views/projects/_show_pipeline_templates.html.erb @@ -1,4 +1,5 @@ <%= render_pane 'tab_contents', to_string: true, locals: { - filters: [['uuid', 'is_a', ["arvados#pipelineTemplate"]]], - sortable_columns: { 'name' => 'pipeline_templates.name', 'description' => 'pipeline_templates.description' } + limit: 50, + filters: [['uuid', 'is_a', ["arvados#pipelineTemplate", "arvados#workflow"]]], + sortable_columns: { 'name' => 'pipeline_templates.name, workflows.name', 'description' => 'pipeline_templates.description, workflows.description' } }.merge(local_assigns) %> diff --git a/apps/workbench/test/integration/anonymous_access_test.rb b/apps/workbench/test/integration/anonymous_access_test.rb index 6e28e4efb4..aae8c41896 100644 --- a/apps/workbench/test/integration/anonymous_access_test.rb +++ b/apps/workbench/test/integration/anonymous_access_test.rb @@ -167,24 +167,40 @@ class AnonymousAccessTest < ActionDispatch::IntegrationTest assert_no_selector 'a', text: 'Re-run options' end - test "anonymous user accesses pipeline templates tab in shared project" do - visit PUBLIC_PROJECT - click_link 'Data collections' - assert_text 'GNU General Public License' + [ + 'pipelineTemplate', + 'workflow' + ].each do |type| + test "anonymous user accesses pipeline templates tab in shared project and click on #{type}" do + visit PUBLIC_PROJECT + click_link 'Data collections' + assert_text 'GNU General Public License' - assert_selector 'a', text: 'Pipeline templates' + assert_selector 'a', text: 'Pipeline templates' - click_link 'Pipeline templates' - assert_text 'Pipeline template in publicly accessible project' + click_link 'Pipeline templates' + assert_text 'Pipeline template in publicly accessible project' + assert_text 'Workflow with input specifications' - within first('tr[data-kind="arvados#pipelineTemplate"]') do - click_link 'Show' - end + if type == 'pipelineTemplate' + within first('tr[data-kind="arvados#pipelineTemplate"]') do + click_link 'Show' + end - # in template page - assert_text 'Public Projects Unrestricted public data' - assert_text 'script version' - assert_no_selector 'a', text: 'Run this pipeline' + # in template page + assert_text 'Public Projects Unrestricted public data' + assert_text 'script version' + assert_no_selector 'a', text: 'Run this pipeline' + else + within first('tr[data-kind="arvados#workflow"]') do + click_link 'Show' + end + + # in workflow page + assert_text 'Public Projects Unrestricted public data' + assert_text 'this workflow has inputs specified' + end + end end test "anonymous user accesses subprojects tab in shared project" do diff --git a/apps/workbench/test/integration/work_units_test.rb b/apps/workbench/test/integration/work_units_test.rb index f842d12d25..7d19fcc9d7 100644 --- a/apps/workbench/test/integration/work_units_test.rb +++ b/apps/workbench/test/integration/work_units_test.rb @@ -125,6 +125,7 @@ class WorkUnitsTest < ActionDispatch::IntegrationTest # in the process page now assert_text process_txt + assert_selector 'a', text: template_name end end end diff --git a/apps/workbench/test/integration_helper.rb b/apps/workbench/test/integration_helper.rb index 785912d324..c94fc619f6 100644 --- a/apps/workbench/test/integration_helper.rb +++ b/apps/workbench/test/integration_helper.rb @@ -5,10 +5,20 @@ require 'uri' require 'yaml' def available_port for_what - Addrinfo.tcp("0.0.0.0", 0).listen do |srv| - port = srv.connect_address.ip_port - STDERR.puts "Using port #{port} for #{for_what}" - return port + begin + Addrinfo.tcp("0.0.0.0", 0).listen do |srv| + port = srv.connect_address.ip_port + # Selenium needs an additional locking port, check if it's available + # and retry if necessary. + if for_what == 'selenium' + locking_port = port - 1 + Addrinfo.tcp("0.0.0.0", locking_port).listen.close + end + STDERR.puts "Using port #{port} for #{for_what}" + return port + end + rescue Errno::EADDRINUSE, Errno::EACCES + retry end end diff --git a/build/README b/build/README new file mode 100644 index 0000000000..418254457d --- /dev/null +++ b/build/README @@ -0,0 +1,30 @@ +Scripts in this directory: + +run-tests.sh Run unit and integration test suite. + +run-build-test-packages-one-target.sh Entry point, wraps + run-build-packages-one-target.sh to + perform package building and testing + inside Docker. + +run-build-packages-one-target.sh Build packages for one target inside Docker. + +run-build-packages-all-targets.sh Run run-build-packages-one-target.sh + for every target. + +run-build-packages.sh Actually build packages. Intended to run + inside Docker container with proper + build environment. + +run-build-packages-sso.sh Build single-sign-on server packages. + +run-build-packages-python-and-ruby.sh Build Python and Ruby packages suitable + for upload to PyPi and Rubygems. + +run-build-docker-images.sh Build arvbox Docker images. + +run-build-docker-jobs-image.sh Build arvados/jobs Docker image. + +run-library.sh A library of functions shared by the + various scripts in this + directory. \ No newline at end of file diff --git a/build/run-build-packages.sh b/build/run-build-packages.sh index ae2de696cf..7e4a3d4c11 100755 --- a/build/run-build-packages.sh +++ b/build/run-build-packages.sh @@ -103,7 +103,8 @@ case "$TARGET" in rsa uritemplate httplib2 ws4py pykka six pyexecjs jsonschema \ ciso8601 pycrypto backports.ssl_match_hostname llfuse==0.41.1 \ 'pycurl<7.21.5' contextlib2 pyyaml 'rdflib>=4.2.0' \ - shellescape mistune typing avro ruamel.ordereddict) + shellescape mistune typing avro ruamel.ordereddict + cachecontrol cwltest) PYTHON3_BACKPORTS=(docker-py==1.7.2 six requests websocket-client) ;; debian8) @@ -113,7 +114,8 @@ case "$TARGET" in rsa uritemplate httplib2 ws4py pykka six pyexecjs jsonschema \ ciso8601 pycrypto backports.ssl_match_hostname llfuse==0.41.1 \ 'pycurl<7.21.5' pyyaml 'rdflib>=4.2.0' \ - shellescape mistune typing avro ruamel.ordereddict) + shellescape mistune typing avro ruamel.ordereddict + cachecontrol cwltest) PYTHON3_BACKPORTS=(docker-py==1.7.2 six requests websocket-client) ;; ubuntu1204) @@ -123,7 +125,8 @@ case "$TARGET" in rsa uritemplate httplib2 ws4py pykka six pyexecjs jsonschema \ ciso8601 pycrypto backports.ssl_match_hostname llfuse==0.41.1 \ contextlib2 'pycurl<7.21.5' pyyaml 'rdflib>=4.2.0' \ - shellescape mistune typing avro isodate ruamel.ordereddict) + shellescape mistune typing avro isodate ruamel.ordereddict + cachecontrol cwltest) PYTHON3_BACKPORTS=(docker-py==1.7.2 six requests websocket-client) ;; ubuntu1404) @@ -131,7 +134,8 @@ case "$TARGET" in PYTHON_BACKPORTS=(pyasn1==0.1.7 pyasn1-modules==0.0.5 llfuse==0.41.1 ciso8601 \ google-api-python-client==1.4.2 six uritemplate oauth2client==1.5.2 httplib2 \ rsa 'pycurl<7.21.5' backports.ssl_match_hostname pyyaml 'rdflib>=4.2.0' \ - shellescape mistune typing avro ruamel.ordereddict) + shellescape mistune typing avro ruamel.ordereddict + cachecontrol cwltest) PYTHON3_BACKPORTS=(docker-py==1.7.2 requests websocket-client) ;; centos6) @@ -151,7 +155,7 @@ case "$TARGET" in python-daemon lockfile llfuse==0.41.1 'pbr<1.0' pyyaml \ 'rdflib>=4.2.0' shellescape mistune typing avro requests \ isodate pyparsing sparqlwrapper html5lib==0.9999999 keepalive \ - ruamel.ordereddict) + ruamel.ordereddict cachecontrol cwltest) PYTHON3_BACKPORTS=(docker-py==1.7.2 six requests websocket-client) export PYCURL_SSL_LIBRARY=nss ;; @@ -171,7 +175,7 @@ case "$TARGET" in python-daemon llfuse==0.41.1 'pbr<1.0' pyyaml \ 'rdflib>=4.2.0' shellescape mistune typing avro \ isodate pyparsing sparqlwrapper html5lib==0.9999999 keepalive \ - ruamel.ordereddict) + ruamel.ordereddict cachecontrol cwltest) PYTHON3_BACKPORTS=(docker-py==1.7.2 six requests websocket-client) export PYCURL_SSL_LIBRARY=nss ;; @@ -459,15 +463,14 @@ fpm_build $WORKSPACE/sdk/cwl "${PYTHON2_PKG_PREFIX}-arvados-cwl-runner" 'Curover # So we build this thing separately. # # Ward, 2016-03-17 -fpm_build schema_salad "" "" python 1.16.20160810195039 +fpm_build schema_salad "" "" python 1.17.20160820171034 # And schema_salad now depends on ruamel-yaml, which apparently has a braindead setup.py that requires special arguments to build (otherwise, it aborts with 'error: you have to install with "pip install ."'). Sigh. # Ward, 2016-05-26 -# ...and schema_salad 1.12.20160610104117 doesn't work with ruamel-yaml > 0.11.11. -fpm_build ruamel.yaml "" "" python 0.11.11 --python-setup-py-arguments "--single-version-externally-managed" +fpm_build ruamel.yaml "" "" python 0.12.4 --python-setup-py-arguments "--single-version-externally-managed" # And for cwltool we have the same problem as for schema_salad. Ward, 2016-03-17 -fpm_build cwltool "" "" python 1.0.20160811184335 +fpm_build cwltool "" "" python 1.0.20160901133827 # FPM eats the trailing .0 in the python-rdflib-jsonld package when built with 'rdflib-jsonld>=0.3.0'. Force the version. Ward, 2016-03-25 fpm_build rdflib-jsonld "" "" python 0.3.0 diff --git a/doc/sdk/java/index.html.textile.liquid b/doc/sdk/java/index.html.textile.liquid index 11b1172399..48f72d3a7d 100644 --- a/doc/sdk/java/index.html.textile.liquid +++ b/doc/sdk/java/index.html.textile.liquid @@ -60,7 +60,7 @@ h3. Implementing your code to use SDK $ARVADOS_HOME/sdk/java/ArvadosSDKJavaExampleWithPrompt.java can be used to make calls to API server interactively. -Please use these implementations to see how you would want use the SDK from your java program. +Please use these implementations to see how you would use the SDK from your java program. Also, refer to $ARVADOS_HOME/arvados/sdk/java/src/test/java/org/arvados/sdk/java/ArvadosTest.java for more sample API invocation examples. @@ -73,7 +73,7 @@ make various call requests. * To compile the examples
-$ javac -cp $ARVADOS_HOME/sdk/java/target/arvados-sdk-1.0-jar-with-dependencies.jar \
+$ javac -cp $ARVADOS_HOME/sdk/java/target/arvados-sdk-1.1-jar-with-dependencies.jar \
 ArvadosSDKJavaExample*.java
 This results in the generation of the ArvadosSDKJavaExample*.class files
 in the same directory as the java files
@@ -83,9 +83,9 @@ in the same directory as the java files
 * To run the samples
 
 
-$ java -cp .:$ARVADOS_HOME/sdk/java/target/arvados-sdk-1.0-jar-with-dependencies.jar \
+$ java -cp .:$ARVADOS_HOME/sdk/java/target/arvados-sdk-1.1-jar-with-dependencies.jar \
 ArvadosSDKJavaExample
-$ java -cp .:$ARVADOS_HOME/sdk/java/target/arvados-sdk-1.0-jar-with-dependencies.jar \
+$ java -cp .:$ARVADOS_HOME/sdk/java/target/arvados-sdk-1.1-jar-with-dependencies.jar \
 ArvadosSDKJavaExampleWithPrompt
 
diff --git a/sdk/cwl/arvados_cwl/fsaccess.py b/sdk/cwl/arvados_cwl/fsaccess.py index ae4532bec8..e44e7a9282 100644 --- a/sdk/cwl/arvados_cwl/fsaccess.py +++ b/sdk/cwl/arvados_cwl/fsaccess.py @@ -84,7 +84,7 @@ class CollectionFsAccess(cwltool.stdfsaccess.StdFsAccess): collection, rest = self.get_collection(fn) if collection: if rest: - return isinstance(collection.find(rest), arvados.collection.Collection) + return isinstance(collection.find(rest), arvados.collection.RichCollectionBase) else: return True else: @@ -99,7 +99,7 @@ class CollectionFsAccess(cwltool.stdfsaccess.StdFsAccess): dir = collection if dir is None: raise IOError(errno.ENOENT, "Directory '%s' in '%s' not found" % (rest, collection.portable_data_hash())) - if not isinstance(dir, arvados.collection.Collection): + if not isinstance(dir, arvados.collection.RichCollectionBase): raise IOError(errno.ENOENT, "Path '%s' in '%s' is not a Directory" % (rest, collection.portable_data_hash())) return [abspath(l, fn) for l in dir.keys()] else: diff --git a/sdk/cwl/setup.py b/sdk/cwl/setup.py index e22a74facc..86b24a394e 100644 --- a/sdk/cwl/setup.py +++ b/sdk/cwl/setup.py @@ -32,7 +32,7 @@ setup(name='arvados-cwl-runner', # Make sure to update arvados/build/run-build-packages.sh as well # when updating the cwltool version pin. install_requires=[ - 'cwltool==1.0.20160811184335', + 'cwltool==1.0.20160901133827', 'arvados-python-client>=0.1.20160714204738', ], data_files=[ diff --git a/sdk/cwl/tests/test_container.py b/sdk/cwl/tests/test_container.py index e06003769b..7dd14a0eb7 100644 --- a/sdk/cwl/tests/test_container.py +++ b/sdk/cwl/tests/test_container.py @@ -6,6 +6,8 @@ import os import functools import cwltool.process +from schema_salad.ref_resolver import Loader + if not os.getenv('ARVADOS_DEBUG'): logging.getLogger('arvados.cwl-runner').setLevel(logging.WARN) logging.getLogger('arvados.arv-run').setLevel(logging.WARN) @@ -35,7 +37,7 @@ class TestContainer(unittest.TestCase): } make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess, api_client=runner.api) arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", avsc_names=avsc_names, - basedir="", make_fs_access=make_fs_access) + basedir="", make_fs_access=make_fs_access, loader=Loader({})) arvtool.formatgraph = None for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_run", make_fs_access=make_fs_access, tmpdir="/tmp"): @@ -88,7 +90,8 @@ class TestContainer(unittest.TestCase): } make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess, api_client=runner.api) arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="containers", - avsc_names=avsc_names, make_fs_access=make_fs_access) + avsc_names=avsc_names, make_fs_access=make_fs_access, + loader=Loader({})) arvtool.formatgraph = None for j in arvtool.job({}, mock.MagicMock(), basedir="", name="test_resource_requirements", make_fs_access=make_fs_access, tmpdir="/tmp"): diff --git a/sdk/cwl/tests/test_job.py b/sdk/cwl/tests/test_job.py index 21b72d17db..19ae93af25 100644 --- a/sdk/cwl/tests/test_job.py +++ b/sdk/cwl/tests/test_job.py @@ -6,6 +6,8 @@ import os import functools import cwltool.process +from schema_salad.ref_resolver import Loader + if not os.getenv('ARVADOS_DEBUG'): logging.getLogger('arvados.cwl-runner').setLevel(logging.WARN) logging.getLogger('arvados.arv-run').setLevel(logging.WARN) @@ -28,7 +30,8 @@ class TestJob(unittest.TestCase): "arguments": [{"valueFrom": "$(runtime.outdir)"}] } make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess, api_client=runner.api) - arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="jobs", avsc_names=avsc_names, basedir="", make_fs_access=make_fs_access) + arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="jobs", avsc_names=avsc_names, + basedir="", make_fs_access=make_fs_access, loader=Loader({})) arvtool.formatgraph = None for j in arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access): j.run() @@ -80,7 +83,8 @@ class TestJob(unittest.TestCase): "baseCommand": "ls" } make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess, api_client=runner.api) - arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="jobs", avsc_names=avsc_names, make_fs_access=make_fs_access) + arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="jobs", avsc_names=avsc_names, + make_fs_access=make_fs_access, loader=Loader({})) arvtool.formatgraph = None for j in arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access): j.run() diff --git a/sdk/java/ArvadosSDKJavaExample.java b/sdk/java/ArvadosSDKJavaExample.java index 7c9c0138ea..33c5755eec 100644 --- a/sdk/java/ArvadosSDKJavaExample.java +++ b/sdk/java/ArvadosSDKJavaExample.java @@ -4,7 +4,7 @@ * */ -import org.arvados.sdk.java.Arvados; +import org.arvados.sdk.Arvados; import java.io.File; import java.util.HashMap; @@ -77,4 +77,4 @@ public class ArvadosSDKJavaExample { } } } -} \ No newline at end of file +} diff --git a/sdk/java/ArvadosSDKJavaExampleWithPrompt.java b/sdk/java/ArvadosSDKJavaExampleWithPrompt.java index 93ba3aa540..1c928aa4f8 100644 --- a/sdk/java/ArvadosSDKJavaExampleWithPrompt.java +++ b/sdk/java/ArvadosSDKJavaExampleWithPrompt.java @@ -7,7 +7,7 @@ * @author radhika */ -import org.arvados.sdk.java.Arvados; +import org.arvados.sdk.Arvados; import java.io.File; import java.util.HashMap; diff --git a/sdk/java/src/test/java/org/arvados/sdk/java/ArvadosTest.java b/sdk/java/src/test/java/org/arvados/sdk/java/ArvadosTest.java index 5176e8c7d2..ba11c988ce 100644 --- a/sdk/java/src/test/java/org/arvados/sdk/java/ArvadosTest.java +++ b/sdk/java/src/test/java/org/arvados/sdk/java/ArvadosTest.java @@ -1,4 +1,4 @@ -package org.arvados.sdk.java; +package org.arvados.sdk; import java.io.File; import java.io.FileInputStream; @@ -257,7 +257,6 @@ public class ArvadosTest { Map params = new HashMap(); params.put("pipeline_template", new String(data)); Map response = arv.call("pipeline_templates", "create", params); - assertEquals("Expected kind to be user", "arvados#pipelineTemplate", response.get("kind")); String uuid = (String)response.get("uuid"); assertNotNull("Expected uuid for pipeline template", uuid); @@ -461,4 +460,4 @@ public class ArvadosTest { assertTrue("Excected some optional parameters for list method for users", parameters.get("optional").contains("filters")); } -} \ No newline at end of file +} diff --git a/sdk/java/src/test/resources/first_pipeline.json b/sdk/java/src/test/resources/first_pipeline.json index 3caa972466..dc3b080e13 100644 --- a/sdk/java/src/test/resources/first_pipeline.json +++ b/sdk/java/src/test/resources/first_pipeline.json @@ -1,5 +1,4 @@ { - "name":"first pipeline", "components":{ "do_hash":{ "script":"hash.py", diff --git a/services/api/app/controllers/application_controller.rb b/services/api/app/controllers/application_controller.rb index 3a888184f8..3c5bf94d2c 100644 --- a/services/api/app/controllers/application_controller.rb +++ b/services/api/app/controllers/application_controller.rb @@ -14,13 +14,11 @@ class ActsAsApi::ApiTemplate end require 'load_param' -require 'record_filters' class ApplicationController < ActionController::Base include CurrentApiClient include ThemesForRails::ActionController include LoadParam - include RecordFilters respond_to :json protect_from_forgery @@ -207,11 +205,7 @@ class ApplicationController < ActionController::Base def apply_filters model_class=nil model_class ||= self.model_class - ft = record_filters @filters, model_class - if ft[:cond_out].any? - @objects = @objects.where('(' + ft[:cond_out].join(') AND (') + ')', - *ft[:param_out]) - end + @objects = model_class.apply_filters(@objects, @filters) end def apply_where_limit_order_params model_class=nil diff --git a/services/api/app/controllers/arvados/v1/groups_controller.rb b/services/api/app/controllers/arvados/v1/groups_controller.rb index a1bfb8bc5e..7a5713a03c 100644 --- a/services/api/app/controllers/arvados/v1/groups_controller.rb +++ b/services/api/app/controllers/arvados/v1/groups_controller.rb @@ -64,7 +64,7 @@ class Arvados::V1::GroupsController < ApplicationController request_filters = @filters klasses = [Group, - Job, PipelineInstance, PipelineTemplate, ContainerRequest, + Job, PipelineInstance, PipelineTemplate, ContainerRequest, Workflow, Collection, Human, Specimen, Trait] diff --git a/services/api/app/controllers/arvados/v1/jobs_controller.rb b/services/api/app/controllers/arvados/v1/jobs_controller.rb index 0c68dc26c2..243f38b78c 100644 --- a/services/api/app/controllers/arvados/v1/jobs_controller.rb +++ b/services/api/app/controllers/arvados/v1/jobs_controller.rb @@ -31,88 +31,12 @@ class Arvados::V1::JobsController < ApplicationController return super if !params[:find_or_create] return if !load_filters_param - if @filters.empty? # Translate older creation parameters into filters. - @filters = - [["repository", "=", resource_attrs[:repository]], - ["script", "=", resource_attrs[:script]], - ["script_version", "not in git", params[:exclude_script_versions]], - ].reject { |filter| filter.last.nil? or filter.last.empty? } - if !params[:minimum_script_version].blank? - @filters << ["script_version", "in git", - params[:minimum_script_version]] - else - add_default_git_filter("script_version", resource_attrs[:repository], - resource_attrs[:script_version]) - end - if image_search = resource_attrs[:runtime_constraints].andand["docker_image"] - if image_tag = resource_attrs[:runtime_constraints]["docker_image_tag"] - image_search += ":#{image_tag}" - end - image_locator = Collection. - for_latest_docker_image(image_search).andand.portable_data_hash - else - image_locator = nil - end - @filters << ["docker_image_locator", "=", image_locator] - if sdk_version = resource_attrs[:runtime_constraints].andand["arvados_sdk_version"] - add_default_git_filter("arvados_sdk_version", "arvados", sdk_version) - end - begin - load_job_specific_filters - rescue ArgumentError => error - return send_error(error.message) - end - end - - # Check specified filters for some reasonableness. - filter_names = @filters.map { |f| f.first }.uniq - ["repository", "script"].each do |req_filter| - if not filter_names.include?(req_filter) - return send_error("#{req_filter} filter required") - end - end - - # Search for a reusable Job, and return it if found. - @objects = Job. - readable_by(current_user). - where('state = ? or (owner_uuid = ? and state in (?))', - Job::Complete, current_user.uuid, [Job::Queued, Job::Running]). - where('script_parameters_digest = ?', Job.sorted_hash_digest(resource_attrs[:script_parameters])). - where('nondeterministic is distinct from ?', true). - order('state desc, created_at') # prefer Running jobs over Queued - apply_filters - @object = nil - incomplete_job = nil - @objects.each do |j| - if j.state != Job::Complete - # We'll use this if we don't find a job that has completed - incomplete_job ||= j - next - end - - if @object == false - # We have already decided not to reuse any completed job - next - elsif @object - if @object.output != j.output - # If two matching jobs produced different outputs, run a new - # job (or use one that's already running/queued) instead of - # choosing one arbitrarily. - @object = false - end - # ...and that's the only thing we need to do once we've chosen - # an @object to reuse. - elsif !Collection.readable_by(current_user).find_by_portable_data_hash(j.output) - # As soon as the output we will end up returning (if any) is - # decided, check whether it will be visible to the user; if - # not, any further investigation of reusable jobs is futile. - return super - else - @object = j - end + begin + @object = Job.find_reusable(resource_attrs, params, @filters, @read_users) + rescue ArgumentError => error + return send_error(error.message) end - @object ||= incomplete_job if @object show else @@ -220,89 +144,11 @@ class Arvados::V1::JobsController < ApplicationController protected - def add_default_git_filter(attr_name, repo_name, refspec) - # Add a filter to @filters for `attr_name` = the latest commit available - # in `repo_name` at `refspec`. No filter is added if refspec can't be - # resolved. - commits = Commit.find_commit_range(repo_name, nil, refspec, nil) - if commit_hash = commits.first - @filters << [attr_name, "=", commit_hash] - end - end - - def load_job_specific_filters - # Convert Job-specific @filters entries into general SQL filters. - script_info = {"repository" => nil, "script" => nil} - git_filters = Hash.new do |hash, key| - hash[key] = {"max_version" => "HEAD", "exclude_versions" => []} - end - @filters.select! do |(attr, operator, operand)| - if (script_info.has_key? attr) and (operator == "=") - if script_info[attr].nil? - script_info[attr] = operand - elsif script_info[attr] != operand - raise ArgumentError.new("incompatible #{attr} filters") - end - end - case operator - when "in git" - git_filters[attr]["min_version"] = operand - false - when "not in git" - git_filters[attr]["exclude_versions"] += Array.wrap(operand) - false - when "in docker", "not in docker" - image_hashes = Array.wrap(operand).flat_map do |search_term| - image_search, image_tag = search_term.split(':', 2) - Collection. - find_all_for_docker_image(image_search, image_tag, @read_users). - map(&:portable_data_hash) - end - @filters << [attr, operator.sub(/ docker$/, ""), image_hashes] - false - else - true - end - end - - # Build a real script_version filter from any "not? in git" filters. - git_filters.each_pair do |attr, filter| - case attr - when "script_version" - script_info.each_pair do |key, value| - if value.nil? - raise ArgumentError.new("script_version filter needs #{key} filter") - end - end - filter["repository"] = script_info["repository"] - begin - filter["max_version"] = resource_attrs[:script_version] - rescue - # Using HEAD, set earlier by the hash default, is fine. - end - when "arvados_sdk_version" - filter["repository"] = "arvados" - else - raise ArgumentError.new("unknown attribute for git filter: #{attr}") - end - revisions = Commit.find_commit_range(filter["repository"], - filter["min_version"], - filter["max_version"], - filter["exclude_versions"]) - if revisions.empty? - raise ArgumentError. - new("error searching #{filter['repository']} from " + - "'#{filter['min_version']}' to '#{filter['max_version']}', " + - "excluding #{filter['exclude_versions']}") - end - @filters.append([attr, "in", revisions]) - end - end - def load_filters_param begin super - load_job_specific_filters + attrs = resource_attrs rescue {} + @filters = Job.load_job_specific_filters attrs, @filters, @read_users rescue ArgumentError => error send_error(error.message) false diff --git a/services/api/app/models/arvados_model.rb b/services/api/app/models/arvados_model.rb index 7b8f12ba54..38bd5cfb34 100644 --- a/services/api/app/models/arvados_model.rb +++ b/services/api/app/models/arvados_model.rb @@ -1,10 +1,12 @@ require 'has_uuid' +require 'record_filters' class ArvadosModel < ActiveRecord::Base self.abstract_class = true include CurrentApiClient # current_user, current_api_client, etc. include DbCurrentTime + extend RecordFilters attr_protected :created_at attr_protected :modified_by_user_uuid @@ -252,6 +254,15 @@ class ArvadosModel < ActiveRecord::Base "to_tsvector('english', ' ' || #{parts.join(" || ' ' || ")})" end + def self.apply_filters query, filters + ft = record_filters filters, self + if not ft[:cond_out].any? + return query + end + query.where('(' + ft[:cond_out].join(') AND (') + ')', + *ft[:param_out]) + end + protected def ensure_ownership_path_leads_to_user diff --git a/services/api/app/models/job.rb b/services/api/app/models/job.rb index 0aaa0bd3f9..e7d1b39ce9 100644 --- a/services/api/app/models/job.rb +++ b/services/api/app/models/job.rb @@ -2,6 +2,7 @@ class Job < ArvadosModel include HasUuid include KindAndEtag include CommonApiTemplate + extend CurrentApiClient serialize :components, Hash attr_protected :arvados_sdk_version, :docker_image_locator serialize :script_parameters, Hash @@ -114,17 +115,183 @@ class Job < ArvadosModel super - ["script_parameters_digest"] end + def self.load_job_specific_filters attrs, orig_filters, read_users + # Convert Job-specific @filters entries into general SQL filters. + script_info = {"repository" => nil, "script" => nil} + git_filters = Hash.new do |hash, key| + hash[key] = {"max_version" => "HEAD", "exclude_versions" => []} + end + filters = [] + orig_filters.each do |attr, operator, operand| + if (script_info.has_key? attr) and (operator == "=") + if script_info[attr].nil? + script_info[attr] = operand + elsif script_info[attr] != operand + raise ArgumentError.new("incompatible #{attr} filters") + end + end + case operator + when "in git" + git_filters[attr]["min_version"] = operand + when "not in git" + git_filters[attr]["exclude_versions"] += Array.wrap(operand) + when "in docker", "not in docker" + image_hashes = Array.wrap(operand).flat_map do |search_term| + image_search, image_tag = search_term.split(':', 2) + Collection. + find_all_for_docker_image(image_search, image_tag, read_users). + map(&:portable_data_hash) + end + filters << [attr, operator.sub(/ docker$/, ""), image_hashes] + else + filters << [attr, operator, operand] + end + end + + # Build a real script_version filter from any "not? in git" filters. + git_filters.each_pair do |attr, filter| + case attr + when "script_version" + script_info.each_pair do |key, value| + if value.nil? + raise ArgumentError.new("script_version filter needs #{key} filter") + end + end + filter["repository"] = script_info["repository"] + if attrs[:script_version] + filter["max_version"] = attrs[:script_version] + else + # Using HEAD, set earlier by the hash default, is fine. + end + when "arvados_sdk_version" + filter["repository"] = "arvados" + else + raise ArgumentError.new("unknown attribute for git filter: #{attr}") + end + revisions = Commit.find_commit_range(filter["repository"], + filter["min_version"], + filter["max_version"], + filter["exclude_versions"]) + if revisions.empty? + raise ArgumentError. + new("error searching #{filter['repository']} from " + + "'#{filter['min_version']}' to '#{filter['max_version']}', " + + "excluding #{filter['exclude_versions']}") + end + filters.append([attr, "in", revisions]) + end + + filters + end + + def self.find_reusable attrs, params, filters, read_users + if filters.empty? # Translate older creation parameters into filters. + filters = + [["repository", "=", attrs[:repository]], + ["script", "=", attrs[:script]], + ["script_version", "not in git", params[:exclude_script_versions]], + ].reject { |filter| filter.last.nil? or filter.last.empty? } + if !params[:minimum_script_version].blank? + filters << ["script_version", "in git", + params[:minimum_script_version]] + else + filters += default_git_filters("script_version", attrs[:repository], + attrs[:script_version]) + end + if image_search = attrs[:runtime_constraints].andand["docker_image"] + if image_tag = attrs[:runtime_constraints]["docker_image_tag"] + image_search += ":#{image_tag}" + end + image_locator = Collection. + for_latest_docker_image(image_search).andand.portable_data_hash + else + image_locator = nil + end + filters << ["docker_image_locator", "=", image_locator] + if sdk_version = attrs[:runtime_constraints].andand["arvados_sdk_version"] + filters += default_git_filters("arvados_sdk_version", "arvados", sdk_version) + end + filters = load_job_specific_filters(attrs, filters, read_users) + end + + # Check specified filters for some reasonableness. + filter_names = filters.map { |f| f.first }.uniq + ["repository", "script"].each do |req_filter| + if not filter_names.include?(req_filter) + return send_error("#{req_filter} filter required") + end + end + + # Search for a reusable Job, and return it if found. + candidates = Job. + readable_by(current_user). + where('state = ? or (owner_uuid = ? and state in (?))', + Job::Complete, current_user.uuid, [Job::Queued, Job::Running]). + where('script_parameters_digest = ?', Job.sorted_hash_digest(attrs[:script_parameters])). + where('nondeterministic is distinct from ?', true). + order('state desc, created_at') # prefer Running jobs over Queued + candidates = apply_filters candidates, filters + chosen = nil + incomplete_job = nil + candidates.each do |j| + if j.state != Job::Complete + # We'll use this if we don't find a job that has completed + incomplete_job ||= j + next + end + + if chosen == false + # We have already decided not to reuse any completed job + next + elsif chosen + if chosen.output != j.output + # If two matching jobs produced different outputs, run a new + # job (or use one that's already running/queued) instead of + # choosing one arbitrarily. + chosen = false + end + # ...and that's the only thing we need to do once we've chosen + # a job to reuse. + elsif !Collection.readable_by(current_user).find_by_portable_data_hash(j.output) + # As soon as the output we will end up returning (if any) is + # decided, check whether it will be visible to the user; if + # not, any further investigation of reusable jobs is futile. + chosen = false + else + chosen = j + end + end + chosen || incomplete_job + end + + def self.default_git_filters(attr_name, repo_name, refspec) + # Add a filter to @filters for `attr_name` = the latest commit available + # in `repo_name` at `refspec`. No filter is added if refspec can't be + # resolved. + commits = Commit.find_commit_range(repo_name, nil, refspec, nil) + if commit_hash = commits.first + [[attr_name, "=", commit_hash]] + else + [] + end + end + protected def self.sorted_hash_digest h Digest::MD5.hexdigest(Oj.dump(deep_sort_hash(h))) end - def self.deep_sort_hash h - return h unless h.is_a? Hash - h.sort.collect do |k, v| - [k, deep_sort_hash(v)] - end.to_h + def self.deep_sort_hash x + if x.is_a? Hash + x.sort.collect do |k, v| + [k, deep_sort_hash(v)] + end.to_h + elsif x.is_a? Array + x.collect { |v| deep_sort_hash(v) } + else + x + end end def foreign_key_attributes diff --git a/services/api/config/application.default.yml b/services/api/config/application.default.yml index ed3b517757..99d2a10427 100644 --- a/services/api/config/application.default.yml +++ b/services/api/config/application.default.yml @@ -183,6 +183,12 @@ common: # Default lifetime for ephemeral collections: 2 weeks. default_trash_lifetime: 1209600 + # Maximum characters of (JSON-encoded) query parameters to include + # in each request log entry. When params exceed this size, they will + # be JSON-encoded, truncated to this size, and logged as + # params_truncated. + max_request_log_params_size: 2000 + # Maximum size (in bytes) allowed for a single API request. This # limit is published in the discovery document for use by clients. # Note: You must separately configure the upstream web server or diff --git a/services/api/config/initializers/lograge.rb b/services/api/config/initializers/lograge.rb index e5bd2002fa..4b1aea9e70 100644 --- a/services/api/config/initializers/lograge.rb +++ b/services/api/config/initializers/lograge.rb @@ -5,8 +5,8 @@ Server::Application.configure do exceptions = %w(controller action format id) params = event.payload[:params].except(*exceptions) params_s = Oj.dump(params) - if params_s.length > 1000 - { params_truncated: params_s[0..1000] + "[...]" } + if params_s.length > Rails.configuration.max_request_log_params_size + { params_truncated: params_s[0..Rails.configuration.max_request_log_params_size] + "[...]" } else { params: params } end diff --git a/services/api/db/migrate/20160901210110_repair_script_parameters_digest.rb b/services/api/db/migrate/20160901210110_repair_script_parameters_digest.rb new file mode 100644 index 0000000000..18eed7a13f --- /dev/null +++ b/services/api/db/migrate/20160901210110_repair_script_parameters_digest.rb @@ -0,0 +1,17 @@ +class RepairScriptParametersDigest < ActiveRecord::Migration + def up + Job.find_each do |j| + have = j.script_parameters_digest + want = j.update_script_parameters_digest + if have != want + # where().update_all() skips validations, event logging, and + # timestamp updates, and just runs SQL. (This change is + # invisible to clients.) + Job.where('id=?', j.id).update_all(script_parameters_digest: want) + end + end + end + + def down + end +end diff --git a/services/api/db/structure.sql b/services/api/db/structure.sql index bda4e5de87..1573db232e 100644 --- a/services/api/db/structure.sql +++ b/services/api/db/structure.sql @@ -2686,4 +2686,6 @@ INSERT INTO schema_migrations (version) VALUES ('20160808151559'); INSERT INTO schema_migrations (version) VALUES ('20160819195557'); -INSERT INTO schema_migrations (version) VALUES ('20160819195725'); \ No newline at end of file +INSERT INTO schema_migrations (version) VALUES ('20160819195725'); + +INSERT INTO schema_migrations (version) VALUES ('20160901210110'); \ No newline at end of file diff --git a/services/api/test/factories/api_client.rb b/services/api/test/factories/api_client.rb index 7921c35865..78c70fdaac 100644 --- a/services/api/test/factories/api_client.rb +++ b/services/api/test/factories/api_client.rb @@ -2,7 +2,7 @@ FactoryGirl.define do factory :api_client do is_trusted false to_create do |instance| - act_as_system_user do + CurrentApiClientHelper.act_as_system_user do instance.save! end end diff --git a/services/api/test/factories/api_client_authorization.rb b/services/api/test/factories/api_client_authorization.rb index 8bd569e8eb..c3883246eb 100644 --- a/services/api/test/factories/api_client_authorization.rb +++ b/services/api/test/factories/api_client_authorization.rb @@ -11,7 +11,7 @@ FactoryGirl.define do end to_create do |instance| - act_as_user instance.user do + CurrentApiClientHelper.act_as_user instance.user do instance.save! end end diff --git a/services/api/test/factories/user.rb b/services/api/test/factories/user.rb index 56e9125217..6ec9e9f05d 100644 --- a/services/api/test/factories/user.rb +++ b/services/api/test/factories/user.rb @@ -1,4 +1,6 @@ -include CurrentApiClient +class CurrentApiClientHelper + extend CurrentApiClient +end FactoryGirl.define do factory :user do @@ -6,7 +8,7 @@ FactoryGirl.define do join_groups [] end after :create do |user, evaluator| - act_as_system_user do + CurrentApiClientHelper.act_as_system_user do evaluator.join_groups.each do |g| Link.create!(tail_uuid: user.uuid, head_uuid: g.uuid, @@ -27,7 +29,7 @@ FactoryGirl.define do factory :active_user do is_active true after :create do |user| - act_as_system_user do + CurrentApiClientHelper.act_as_system_user do Link.create!(tail_uuid: user.uuid, head_uuid: Group.where('uuid ~ ?', '-f+$').first.uuid, link_class: 'permission', @@ -36,7 +38,7 @@ FactoryGirl.define do end end to_create do |instance| - act_as_system_user do + CurrentApiClientHelper.act_as_system_user do instance.save! end end diff --git a/services/api/test/fixtures/workflows.yml b/services/api/test/fixtures/workflows.yml index 87a343d9ab..e124cf85f0 100644 --- a/services/api/test/fixtures/workflows.yml +++ b/services/api/test/fixtures/workflows.yml @@ -21,7 +21,7 @@ workflow_with_no_name_and_desc: workflow_with_input_specifications: uuid: zzzzz-7fd4e-validwithinputs - owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz + owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0 name: Workflow with input specifications description: this workflow has inputs specified created_at: <%= 1.minute.ago.to_s(:db) %> diff --git a/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb b/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb index ab79d7bf80..8007fd26f8 100644 --- a/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb +++ b/services/api/test/functional/arvados/v1/job_reuse_controller_test.rb @@ -669,7 +669,7 @@ class Arvados::V1::JobReuseControllerTest < ActionController::TestCase errors = json_response.fetch("errors", []) assert(errors.any?, "no errors assigned from #{params}") refute(errors.any? { |msg| msg =~ /^#<[A-Za-z]+: / }, - "errors include raw exception") + "errors include raw exception: #{errors.inspect}") errors end diff --git a/services/api/test/test_helper.rb b/services/api/test/test_helper.rb index ef08c726ae..417ddf6bee 100644 --- a/services/api/test/test_helper.rb +++ b/services/api/test/test_helper.rb @@ -47,6 +47,7 @@ class ActiveSupport::TestCase fixtures :all include ArvadosTestSupport + include CurrentApiClient setup do Rails.logger.warn "\n\n#{'=' * 70}\n#{self.class}\##{method_name}\n#{'-' * 70}\n\n" diff --git a/services/api/test/unit/job_test.rb b/services/api/test/unit/job_test.rb index 1bc8035db6..1d3abac60a 100644 --- a/services/api/test/unit/job_test.rb +++ b/services/api/test/unit/job_test.rb @@ -456,4 +456,10 @@ class JobTest < ActiveSupport::TestCase "wrong script_parameters_digest for #{j.uuid}") end end + + test 'deep_sort_hash on array of hashes' do + a = {'z' => [[{'a' => 'a', 'b' => 'b'}]]} + b = {'z' => [[{'b' => 'b', 'a' => 'a'}]]} + assert_equal Job.deep_sort_hash(a).to_json, Job.deep_sort_hash(b).to_json + end end diff --git a/services/fuse/tests/test_mount.py b/services/fuse/tests/test_mount.py index e534e32737..39f110962b 100644 --- a/services/fuse/tests/test_mount.py +++ b/services/fuse/tests/test_mount.py @@ -1155,7 +1155,7 @@ class TokenExpiryTest(MountTestBase): re.search(r'\+A[0-9a-f]+@([0-9a-f]+)', got_loc).group(1), 16) self.assertGreaterEqual( - got_exp, want_exp-1, + got_exp, want_exp-2, msg='now+2w = {:x}, but fuse fetched locator {} (old_exp {:x})'.format( want_exp, got_loc, old_exp)) self.assertLessEqual(